From 88b67ed0a8b9ca29f9335e7bea50051f5779e084 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Mon, 15 Nov 2021 17:55:28 -0800 Subject: [PATCH 01/24] Convert the database layer to async PostgreSQL Use async PostgreSQL and asyncpg instead of psycopg2. Convert all of the relevant handlers to async. Stop using fastapi_sqlalchemy since it doesn't support async sessions and instead add our own database session middleware. After some experimentation, go with creating a new AsyncSession for each request instead of trying to use the scoped session (which causes cleanup problems with tests). Switch to a model of one transaction per request for the handlers and remove the transaction code from the storage layer accordingly. This requires adding some transaction support back into the test support libraries and the individual tests. Update dependencies. --- docs/api.rst | 4 +- requirements/dev.in | 1 + requirements/dev.txt | 240 +++++++++++++----- requirements/main.in | 3 +- requirements/main.txt | 93 ++++--- setup.cfg | 1 + src/gafaelfawr/config.py | 20 +- src/gafaelfawr/database.py | 75 +++--- src/gafaelfawr/dependencies/context.py | 10 +- src/gafaelfawr/dependencies/db_session.py | 92 +++++++ src/gafaelfawr/factory.py | 44 ++-- src/gafaelfawr/handlers/api.py | 30 +-- src/gafaelfawr/handlers/login.py | 2 +- src/gafaelfawr/main.py | 17 +- src/gafaelfawr/models/history.py | 7 + src/gafaelfawr/schema/__init__.py | 12 +- src/gafaelfawr/schema/admin_history.py | 17 +- src/gafaelfawr/schema/subtoken.py | 11 +- src/gafaelfawr/schema/token.py | 27 +- src/gafaelfawr/schema/token_auth_history.py | 28 +- src/gafaelfawr/schema/token_change_history.py | 40 +-- src/gafaelfawr/services/admin.py | 43 ++-- src/gafaelfawr/services/token.py | 132 +++++----- src/gafaelfawr/storage/admin.py | 40 ++- src/gafaelfawr/storage/history.py | 117 +++++---- src/gafaelfawr/storage/token.py | 142 ++++++----- src/gafaelfawr/storage/transaction.py | 61 ----- src/gafaelfawr/util.py | 45 +++- tests/cli_test.py | 5 +- tests/handlers/api_admins_test.py | 5 +- tests/handlers/api_history_test.py | 23 +- tests/handlers/api_tokens_test.py | 5 + tests/handlers/login_github_test.py | 5 +- tests/services/admin_test.py | 56 ++-- tests/services/token_test.py | 69 ++--- tests/settings_test.py | 4 +- tests/support/selenium.py | 68 ++--- tests/support/setup.py | 56 ++-- tests/support/tokens.py | 18 +- 39 files changed, 972 insertions(+), 696 deletions(-) create mode 100644 src/gafaelfawr/dependencies/db_session.py delete mode 100644 src/gafaelfawr/storage/transaction.py diff --git a/docs/api.rst b/docs/api.rst index 320c85920..4f0193a63 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -20,6 +20,8 @@ API reference .. automodapi:: gafaelfawr.dependencies.context +.. automodapi:: gafaelfawr.dependencies.db_session + .. automodapi:: gafaelfawr.dependencies.redis .. automodapi:: gafaelfawr.dependencies.return_url @@ -76,8 +78,6 @@ API reference .. automodapi:: gafaelfawr.storage.token -.. automodapi:: gafaelfawr.storage.transaction - .. automodapi:: gafaelfawr.templates :include-all-objects: diff --git a/requirements/dev.in b/requirements/dev.in index 07e1d55ad..9a95a07f6 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -27,6 +27,7 @@ seqdiag sphinx-automodapi sphinx-click sphinx-prompt +sqlalchemy[mypy] types-cachetools types-PyYAML diff --git a/requirements/dev.txt b/requirements/dev.txt index e03ae7b70..9e22bc5ad 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -14,9 +14,9 @@ alabaster==0.7.12 \ --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 # via sphinx -anyio==3.3.4 \ - --hash=sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66 \ - --hash=sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff +anyio==3.4.0 \ + --hash=sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d \ + --hash=sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020 # via # -c requirements/main.txt # httpcore @@ -175,9 +175,9 @@ cfgv==3.3.1 \ --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 # via pre-commit -charset-normalizer==2.0.7 \ - --hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \ - --hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b +charset-normalizer==2.0.8 \ + --hash=sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0 \ + --hash=sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405 # via # -c requirements/main.txt # httpx @@ -189,54 +189,54 @@ click==8.0.3 \ # -c requirements/main.txt # documenteer # sphinx-click -coverage[toml]==6.1.2 \ - --hash=sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954 \ - --hash=sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0 \ - --hash=sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193 \ - --hash=sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052 \ - --hash=sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e \ - --hash=sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c \ - --hash=sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d \ - --hash=sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4 \ - --hash=sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186 \ - --hash=sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d \ - --hash=sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696 \ - --hash=sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13 \ - --hash=sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2 \ - --hash=sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c \ - --hash=sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388 \ - --hash=sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9 \ - --hash=sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59 \ - --hash=sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225 \ - --hash=sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e \ - --hash=sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b \ - --hash=sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a \ - --hash=sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f \ - --hash=sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93 \ - --hash=sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758 \ - --hash=sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b \ - --hash=sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b \ - --hash=sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204 \ - --hash=sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71 \ - --hash=sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd \ - --hash=sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373 \ - --hash=sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4 \ - --hash=sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc \ - --hash=sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266 \ - --hash=sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263 \ - --hash=sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf \ - --hash=sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c \ - --hash=sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c \ - --hash=sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc \ - --hash=sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c \ - --hash=sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649 \ - --hash=sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972 \ - --hash=sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f \ - --hash=sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929 \ - --hash=sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d \ - --hash=sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de \ - --hash=sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091 \ - --hash=sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab +coverage[toml]==6.2 \ + --hash=sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0 \ + --hash=sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd \ + --hash=sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884 \ + --hash=sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48 \ + --hash=sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76 \ + --hash=sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0 \ + --hash=sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64 \ + --hash=sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685 \ + --hash=sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47 \ + --hash=sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d \ + --hash=sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840 \ + --hash=sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f \ + --hash=sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971 \ + --hash=sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c \ + --hash=sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a \ + --hash=sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de \ + --hash=sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17 \ + --hash=sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4 \ + --hash=sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521 \ + --hash=sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57 \ + --hash=sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b \ + --hash=sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282 \ + --hash=sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644 \ + --hash=sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475 \ + --hash=sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d \ + --hash=sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da \ + --hash=sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953 \ + --hash=sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2 \ + --hash=sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e \ + --hash=sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c \ + --hash=sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc \ + --hash=sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64 \ + --hash=sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74 \ + --hash=sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617 \ + --hash=sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3 \ + --hash=sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d \ + --hash=sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa \ + --hash=sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739 \ + --hash=sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8 \ + --hash=sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8 \ + --hash=sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781 \ + --hash=sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58 \ + --hash=sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9 \ + --hash=sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c \ + --hash=sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd \ + --hash=sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e \ + --hash=sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49 # via # -r requirements/dev.in # pytest-cov @@ -252,6 +252,7 @@ cryptography==36.0.0 \ --hash=sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e \ --hash=sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58 \ --hash=sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44 \ + --hash=sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6 \ --hash=sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d \ --hash=sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636 \ --hash=sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba \ @@ -311,6 +312,60 @@ graphviz==0.16 \ --hash=sha256:3cad5517c961090dfc679df6402a57de62d97703e2880a1a46147bb0dc1639eb \ --hash=sha256:d2d25af1c199cad567ce4806f0449cb74eb30cf451fd7597251e1da099ac6e57 # via diagrams +greenlet==1.1.2 \ + --hash=sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711 \ + --hash=sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd \ + --hash=sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073 \ + --hash=sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708 \ + --hash=sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67 \ + --hash=sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23 \ + --hash=sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1 \ + --hash=sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08 \ + --hash=sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd \ + --hash=sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa \ + --hash=sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8 \ + --hash=sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40 \ + --hash=sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab \ + --hash=sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6 \ + --hash=sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc \ + --hash=sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b \ + --hash=sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e \ + --hash=sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963 \ + --hash=sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3 \ + --hash=sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d \ + --hash=sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d \ + --hash=sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28 \ + --hash=sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3 \ + --hash=sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e \ + --hash=sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c \ + --hash=sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d \ + --hash=sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0 \ + --hash=sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497 \ + --hash=sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee \ + --hash=sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713 \ + --hash=sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58 \ + --hash=sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a \ + --hash=sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06 \ + --hash=sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88 \ + --hash=sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4 \ + --hash=sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5 \ + --hash=sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c \ + --hash=sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a \ + --hash=sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1 \ + --hash=sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43 \ + --hash=sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627 \ + --hash=sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b \ + --hash=sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168 \ + --hash=sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d \ + --hash=sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5 \ + --hash=sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478 \ + --hash=sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf \ + --hash=sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce \ + --hash=sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c \ + --hash=sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b + # via + # -c requirements/main.txt + # sqlalchemy h11==0.12.0 \ --hash=sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6 \ --hash=sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042 @@ -492,7 +547,9 @@ mypy==0.910 \ --hash=sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504 \ --hash=sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921 \ --hash=sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d - # via -r requirements/dev.in + # via + # -r requirements/dev.in + # sqlalchemy mypy-extensions==0.4.3 \ --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 @@ -732,14 +789,14 @@ rfc3986[idna2008]==1.5.0 \ # via # -c requirements/main.txt # httpx -selenium==4.0.0 \ - --hash=sha256:c942b166a21ce9c9065ad249b54059e926d39f9000167b5ca0fa4950d2ef9a82 +selenium==4.1.0 \ + --hash=sha256:27e7b64df961d609f3d57237caa0df123abbbe22d038f2ec9e332fb90ec1a939 # via # -r requirements/dev.in # selenium-wire -selenium-wire==4.5.5 \ - --hash=sha256:2cb654eb153d5172f976d97054c5bc9d98298563f73bbcfc6d8843553e40616a \ - --hash=sha256:3e9f282444f8c193bd13f0a70bc7fb014556179995c73c37163f159efe8008bf +selenium-wire==4.5.6 \ + --hash=sha256:ee0278803ea64502a337ed40c2fac845ef90c885513649af5c795865a294aed9 \ + --hash=sha256:efbfcf20e557792abfe96bc0f0c7ca741f907b4151069fecb5a76fcc20282389 # via -r requirements/dev.in seqdiag==2.0.0 \ --hash=sha256:3167f16b4d15f3cd20de302fa600c96e4a50c92dae873bcbcf136c7588eeaa48 \ @@ -776,9 +833,9 @@ sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via trio -sphinx==4.3.0 \ - --hash=sha256:6d051ab6e0d06cba786c4656b0fe67ba259fe058410f49e95bee6e49c4052cbf \ - --hash=sha256:7e2b30da5f39170efcd95c6270f07669d623c276521fee27ad6c380f49d2bf5b +sphinx==4.3.1 \ + --hash=sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f \ + --hash=sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45 # via # documenteer # sphinx-automodapi @@ -824,6 +881,50 @@ sphinxcontrib-serializinghtml==1.1.5 \ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 # via sphinx +sqlalchemy[mypy]==1.4.27 \ + --hash=sha256:015511c52c650eebf1059ed8a21674d9d4ae567ebfd80fc73f8252faccd71864 \ + --hash=sha256:0438bccc16349db2d5203598be6073175ce16d4e53b592d6e6cef880c197333e \ + --hash=sha256:10230364479429437f1b819a8839f1edc5744c018bfeb8d01320930f97695bc9 \ + --hash=sha256:2146ef996181e3d4dd20eaf1d7325eb62d6c8aa4dc1677c1872ddfa8561a47d9 \ + --hash=sha256:24828c5e74882cf41516740c0b150702bee4c6817d87d5c3d3bafef2e6896f80 \ + --hash=sha256:2717ceae35e71de1f58b0d1ee7e773d3aab5c403c6e79e8d262277c7f7f95269 \ + --hash=sha256:2e93624d186ea7a738ada47314701c8830e0e4b021a6bce7fbe6f39b87ee1516 \ + --hash=sha256:435b1980c1333ffe3ab386ad28d7b209590b0fa83ea8544d853e7a22f957331b \ + --hash=sha256:486f7916ef77213103467924ef25f5ea1055ae901f385fe4d707604095fdf6a9 \ + --hash=sha256:4ac8306e04275d382d6393e557047b0a9d7ddf9f7ca5da9b3edbd9323ea75bd9 \ + --hash=sha256:4d1d707b752137e6bf45720648e1b828d5e4881d690df79cca07f7217ea06365 \ + --hash=sha256:52f23a76544ed29573c0f3ee41f0ca1aedbab3a453102b60b540cc6fa55448ad \ + --hash=sha256:5beeff18b4e894f6cb73c8daf2c0d8768844ef40d97032bb187d75b1ec8de24b \ + --hash=sha256:6510f4a5029643301bdfe56b61e806093af2101d347d485c42a5535847d2c699 \ + --hash=sha256:6afa9e4e63f066e0fd90a21db7e95e988d96127f52bfb298a0e9bec6999357a9 \ + --hash=sha256:771eca9872b47a629010665ff92de1c248a6979b8d1603daced37773d6f6e365 \ + --hash=sha256:78943451ab3ffd0e27876f9cea2b883317518b418f06b90dadf19394534637e9 \ + --hash=sha256:8327e468b1775c0dfabc3d01f39f440585bf4d398508fcbbe2f0d931c502337d \ + --hash=sha256:8dbe5f639e6d035778ebf700be6d573f82a13662c3c2c3aa0f1dba303b942806 \ + --hash=sha256:9134e5810262203388b203c2022bbcbf1a22e89861eef9340e772a73dd9076fa \ + --hash=sha256:9369f927f4d19b58322cfea8a51710a3f7c47a0e7f3398d94a4632760ecd74f6 \ + --hash=sha256:987fe2f84ceaf744fa0e48805152abe485a9d7002c9923b18a4b2529c7bff218 \ + --hash=sha256:a5881644fc51af7b232ab8d64f75c0f32295dfe88c2ee188023795cdbd4cf99b \ + --hash=sha256:a81e40dfa50ed3c472494adadba097640bfcf43db160ed783132045eb2093cb1 \ + --hash=sha256:aadc6d1e58e14010ae4764d1ba1fd0928dbb9423b27a382ea3a1444f903f4084 \ + --hash=sha256:ad8ec6b69d03e395db48df8991aa15fce3cd23e378b73e01d46a26a6efd5c26d \ + --hash=sha256:b02eee1577976acb4053f83d32b7826424f8b9f70809fa756529a52c6537eda4 \ + --hash=sha256:bac949be7579fed824887eed6672f44b7c4318abbfb2004b2c6968818b535a2f \ + --hash=sha256:c035184af4e58e154b0977eea52131edd096e0754a88f7d5a847e7ccb3510772 \ + --hash=sha256:c7d0a1b1258efff7d7f2e6cfa56df580d09ba29d35a1e3f604f867e1f685feb2 \ + --hash=sha256:cc49fb8ff103900c20e4a9c53766c82a7ebbc183377fb357a8298bad216e9cdd \ + --hash=sha256:d768359daeb3a86644f3854c6659e4496a3e6bba2b4651ecc87ce7ad415b320c \ + --hash=sha256:d81c84c9d2523b3ea20f8e3aceea68615768a7464c0f9a9899600ce6592ec570 \ + --hash=sha256:ec1c908fa721f2c5684900cc8ff75555b1a5a2ae4f5a5694eb0e37a5263cea44 \ + --hash=sha256:fa52534076394af7315306a8701b726a6521b591d95e8f4e5121c82f94790e8d \ + --hash=sha256:fd421a14edf73cfe01e8f51ed8966294ee3b3db8da921cacc88e497fd6e977af + # via + # -c requirements/main.txt + # -r requirements/dev.in +sqlalchemy2-stubs==0.0.2a19 \ + --hash=sha256:2117c48ce5acfe33bf9c9bfce2a981632d931949e68fa313aa5c2a3bc980ca7a \ + --hash=sha256:aac7dca77a2c49e5f0934976421d5e25ae4dc5e27db48c01e055f81caa1e3ead + # via sqlalchemy toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f @@ -845,9 +946,9 @@ trio-websocket==0.9.2 \ --hash=sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc \ --hash=sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe # via selenium -types-cachetools==4.2.5 \ - --hash=sha256:67c1a4eeaac61a0a10c1c07bd695cd549c0f4d9449edc0271fa0e295654aad6c \ - --hash=sha256:d9faa19bb2cf818edf763b126e2d076dd6e92ce9e9cd478b4aea705e0216d0cd +types-cachetools==4.2.6 \ + --hash=sha256:1b562fe589d78fc4ec702996d5c42eecf3fd4e3b5aa9ae1be95a0fc28c390ec7 \ + --hash=sha256:4492f47ab2d95243449bb5474be8956afc33e87a3ae70e43b170f168410c199c # via -r requirements/dev.in types-pyyaml==6.0.1 \ --hash=sha256:2e27b0118ca4248a646101c5c318dc02e4ca2866d6bc42e84045dbb851555a76 \ @@ -862,6 +963,7 @@ typing-extensions==4.0.0 \ # async-timeout # gitpython # mypy + # sqlalchemy2-stubs urllib3[secure]==1.26.7 \ --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece \ --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 @@ -931,9 +1033,9 @@ zstandard==0.16.0 \ # via selenium-wire # The following packages are considered to be unsafe in a requirements file: -setuptools==59.2.0 \ - --hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \ - --hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d +setuptools==59.4.0 \ + --hash=sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d \ + --hash=sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060 # via # -c requirements/main.txt # blockdiag diff --git a/requirements/main.in b/requirements/main.in index ecd0d6e1e..08973243b 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -8,7 +8,6 @@ # These dependencies are for fastapi including some optional features. aiofiles fastapi -fastapi-sqlalchemy python-multipart starlette uvicorn[standard] @@ -18,12 +17,12 @@ jinja2<3 # Other dependencies. aioredis +asyncpg cachetools click cryptography kubernetes httpx -psycopg2 pydantic PyJWT pyyaml diff --git a/requirements/main.txt b/requirements/main.txt index 668f86b60..84ad53e1a 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -4,17 +4,17 @@ # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/main.txt requirements/main.in # -aiofiles==0.7.0 \ - --hash=sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4 \ - --hash=sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc +aiofiles==0.8.0 \ + --hash=sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937 \ + --hash=sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59 # via -r requirements/main.in aioredis==2.0.0 \ --hash=sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db \ --hash=sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb # via -r requirements/main.in -anyio==3.3.4 \ - --hash=sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66 \ - --hash=sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff +anyio==3.4.0 \ + --hash=sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d \ + --hash=sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020 # via # httpcore # starlette @@ -26,6 +26,34 @@ async-timeout==4.0.1 \ --hash=sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690 \ --hash=sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51 # via aioredis +asyncpg==0.25.0 \ + --hash=sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b \ + --hash=sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471 \ + --hash=sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6 \ + --hash=sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4 \ + --hash=sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256 \ + --hash=sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a \ + --hash=sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e \ + --hash=sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095 \ + --hash=sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e \ + --hash=sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09 \ + --hash=sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e \ + --hash=sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540 \ + --hash=sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac \ + --hash=sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92 \ + --hash=sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b \ + --hash=sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962 \ + --hash=sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9 \ + --hash=sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855 \ + --hash=sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634 \ + --hash=sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68 \ + --hash=sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5 \ + --hash=sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3 \ + --hash=sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd \ + --hash=sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4 \ + --hash=sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2 \ + --hash=sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8 + # via -r requirements/main.in cachetools==4.2.4 \ --hash=sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693 \ --hash=sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1 @@ -92,9 +120,9 @@ cffi==1.15.0 \ --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 # via cryptography -charset-normalizer==2.0.7 \ - --hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \ - --hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b +charset-normalizer==2.0.8 \ + --hash=sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0 \ + --hash=sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405 # via # httpx # requests @@ -116,6 +144,7 @@ cryptography==36.0.0 \ --hash=sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e \ --hash=sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58 \ --hash=sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44 \ + --hash=sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6 \ --hash=sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d \ --hash=sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636 \ --hash=sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba \ @@ -132,10 +161,6 @@ fastapi==0.70.0 \ # via # -r requirements/main.in # safir -fastapi-sqlalchemy==0.2.1 \ - --hash=sha256:7a9d44e46cbc73c3f5ee8c444f7e0bcd3d01370a878740abd4cd4d2e900ce9af \ - --hash=sha256:d3bfc6d9388a73a2c3726bc6bd7764cd82debfa71c16e3991c544b9701f48d96 - # via -r requirements/main.in google-auth==2.3.3 \ --hash=sha256:a348a50b027679cb7dae98043ac8dbcc1d7951f06d8387496071a1e05a2465c0 \ --hash=sha256:d83570a664c10b97a1dc6f8df87e5fdfff012f48f62be131e449c20dfc32630e @@ -236,9 +261,9 @@ jinja2==2.11.3 \ --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 # via -r requirements/main.in -kubernetes==19.15.0 \ - --hash=sha256:08c93f300a9837104282ecc81458b903a56444c5c1ec3d990d237557312af47f \ - --hash=sha256:52312adda60d92ba45b325f2c1505924656389222005f7e089718e1ad03bc07f +kubernetes==20.13.0 \ + --hash=sha256:7d32ea7f555813a141a0e912e7695d88f7213bb632a860ba79a963b43f7a6b18 \ + --hash=sha256:ce5e881c13dc56f21a243804f90bc3c507af93c380f505c00e392c823968e4de # via -r requirements/main.in markupsafe==2.0.1 \ --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ @@ -315,19 +340,6 @@ oauthlib==3.1.1 \ --hash=sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc \ --hash=sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3 # via requests-oauthlib -psycopg2==2.9.2 \ - --hash=sha256:26322c3f114de1f60c1b0febf8fdd595c221b4f624524178f515d07350a71bd1 \ - --hash=sha256:6796ac614412ce374587147150e56d03b7845c9e031b88aacdcadc880e81bb38 \ - --hash=sha256:77b9105ef37bc005b8ffbcb1ed6d8685bb0e8ce84773738aa56421a007ec5a7a \ - --hash=sha256:77d09a79f9739b97099d2952bbbf18eaa4eaf825362387acbb9552ec1b3fa228 \ - --hash=sha256:91c7fd0fe9e6c118e8ff5b665bc3445781d3615fa78e131d0b4f8c85e8ca9ec8 \ - --hash=sha256:a761b60da0ecaf6a9866985bcde26327883ac3cdb90535ab68b8d784f02b05ef \ - --hash=sha256:a84da9fa891848e0270e8e04dcca073bc9046441eeb47069f5c0e36783debbea \ - --hash=sha256:b8816c6410fa08d2a022e4e38d128bae97c1855e176a00493d6ec62ccd606d57 \ - --hash=sha256:dfc32db6ce9ecc35a131320888b547199f79822b028934bb5b332f4169393e15 \ - --hash=sha256:f65cba7924363e0d2f416041b48ff69d559548f2cb168ff972c54e09e1e64db8 \ - --hash=sha256:fd7ddab7d6afee4e21c03c648c8b667b197104713e57ec404d5b74097af21e31 - # via -r requirements/main.in pyasn1==0.4.8 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba @@ -436,9 +448,9 @@ rfc3986[idna2008]==1.5.0 \ --hash=sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835 \ --hash=sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97 # via httpx -rsa==4.7.2 \ - --hash=sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2 \ - --hash=sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9 +rsa==4.8 \ + --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \ + --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb # via google-auth safir==2.2.0 \ --hash=sha256:ac8493c9a556c50e0a4e4062f4547005f4fe73487c8e82cfb159a8866ccf02f1 \ @@ -496,20 +508,17 @@ sqlalchemy==1.4.27 \ --hash=sha256:ec1c908fa721f2c5684900cc8ff75555b1a5a2ae4f5a5694eb0e37a5263cea44 \ --hash=sha256:fa52534076394af7315306a8701b726a6521b591d95e8f4e5121c82f94790e8d \ --hash=sha256:fd421a14edf73cfe01e8f51ed8966294ee3b3db8da921cacc88e497fd6e977af - # via - # -r requirements/main.in - # fastapi-sqlalchemy + # via -r requirements/main.in starlette==0.16.0 \ --hash=sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f \ --hash=sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870 # via # -r requirements/main.in # fastapi - # fastapi-sqlalchemy # safir -structlog==21.3.0 \ - --hash=sha256:063216becff8e6f6558122a9b00734f7e50bfef309eb730c85a52c74ed861a96 \ - --hash=sha256:4da2aec0aebf6dee7beb884eb0fda26ed9d6cce5338fcd523e8597d0f1826746 +structlog==21.4.0 \ + --hash=sha256:305a66201f9605a2e8a2595271a446f258175901c09c01e4c2c2a8ac5b68edf1 \ + --hash=sha256:6ed8fadb27cf8362be0e606f5e79ccdd3b1e879aac65f9dc0ac3033fd013a7be # via # -r requirements/main.in # safir @@ -608,9 +617,9 @@ websockets==10.1 \ # via uvicorn # The following packages are considered to be unsafe in a requirements file: -setuptools==59.2.0 \ - --hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \ - --hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d +setuptools==59.4.0 \ + --hash=sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d \ + --hash=sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060 # via # google-auth # kubernetes diff --git a/setup.cfg b/setup.cfg index 50404bb99..9354ee2b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ ignore = E203, W503 disallow_untyped_defs = True disallow_incomplete_defs = True ignore_missing_imports = True +plugins = sqlalchemy.ext.mypy.plugin show_error_codes = True strict_equality = True warn_redundant_casts = True diff --git a/src/gafaelfawr/config.py b/src/gafaelfawr/config.py index b7e153848..b29d3668e 100644 --- a/src/gafaelfawr/config.py +++ b/src/gafaelfawr/config.py @@ -588,16 +588,28 @@ def from_file(cls, path: str) -> Config: oidc_secret = cls._load_secret(path).decode() # The database URL may have a separate secret in database_password, in - # which case it needs to be added to the URL. - database_url = settings.database_url + # which case it needs to be added to the URL. It also needs to be + # configured to use asyncpg. + # + # We have to avoid changing the URL if we're using SQLite, because + # urlparse cannot deal with the expected SQLite syntax of multiple + # consecutive / characters. + parsed_url = urlparse(settings.database_url) + if parsed_url.scheme == "postgresql": + parsed_url = parsed_url._replace(scheme="postgresql+asyncpg") if settings.database_password: - parsed_url = urlparse(database_url) database_password = settings.database_password.get_secret_value() database_netloc = ( f"{parsed_url.username}:{database_password}" f"@{parsed_url.hostname}" ) - database_url = parsed_url._replace(netloc=database_netloc).geturl() + parsed_url = parsed_url._replace(netloc=database_netloc) + if parsed_url.scheme == "sqlite": + database_url = settings.database_url.replace( + "sqlite:", "sqlite+aiosqlite:" + ) + else: + database_url = parsed_url.geturl() # If there is an OpenID Connect server configuration, load it from a # file in JSON format. (It contains secrets.) diff --git a/src/gafaelfawr/database.py b/src/gafaelfawr/database.py index 0c1b2370e..d371ad2a4 100644 --- a/src/gafaelfawr/database.py +++ b/src/gafaelfawr/database.py @@ -11,47 +11,49 @@ from typing import TYPE_CHECKING import structlog -from sqlalchemy import create_engine, select +from sqlalchemy import select from sqlalchemy.exc import OperationalError -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker from gafaelfawr.models.admin import Admin from gafaelfawr.schema import Admin as SQLAdmin from gafaelfawr.schema import drop_schema, initialize_schema from gafaelfawr.storage.admin import AdminStore -from gafaelfawr.storage.transaction import TransactionManager if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncEngine from structlog.stdlib import BoundLogger from gafaelfawr.config import Config -__all__ = ["create_session", "initialize_database"] +__all__ = ["check_database", "initialize_database"] -def create_session(config: Config, logger: BoundLogger) -> Session: - """Create a new database session. +def _create_session_factory(engine: AsyncEngine) -> sessionmaker: + """Create a session factory that generates async sessions.""" + return sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) - Checks that the database is available and retries in a loop for 10s if it - is not. + +async def check_database(url: str, logger: BoundLogger) -> None: + """Check that the database is accessible. Parameters ---------- config : `gafaelfawr.config.Config` The Gafaelfawr configuration. - - Returns - ------- - session : `sqlalchemy.orm.Session` - The database session. + logger : `structlog.stdlib.BoundLogger` + Logger used to report problems """ + engine = create_async_engine(url, future=True) + factory = _create_session_factory(engine) for _ in range(5): try: - engine = create_engine(config.database_url) - session = Session(bind=engine) - session.execute(select(SQLAdmin)) - return session - except OperationalError: + async with factory() as session: + async with session.begin(): + await session.execute(select(SQLAdmin).limit(1)) + return + except (ConnectionRefusedError, OperationalError): logger.info("database not ready, waiting two seconds") time.sleep(2) continue @@ -59,13 +61,12 @@ def create_session(config: Config, logger: BoundLogger) -> Session: # If we got here, we failed five times. Try one last time without # catching exceptions so that we raise the appropriate exception to our # caller. - engine = create_engine(config.database_url) - session = Session(bind=engine) - session.execute(select(SQLAdmin)) - return session + async with factory() as session: + async with session.begin(): + await session.execute(select(SQLAdmin).limit(1)) -def initialize_database(config: Config, reset: bool = False) -> None: +async def initialize_database(config: Config, reset: bool = False) -> None: """Create and initialize a new database. Parameters @@ -83,29 +84,33 @@ def initialize_database(config: Config, reset: bool = False) -> None: # pre-ping to ensure the database is available and attempts to connect # five times with a two second delay between each attempt. success = False + engine = create_async_engine(config.database_url, future=True) for _ in range(5): try: - engine = create_engine(config.database_url, pool_pre_ping=True) if reset: - drop_schema(engine) - initialize_schema(engine) + await drop_schema(engine) + await initialize_schema(engine) success = True - except OperationalError: + except (ConnectionRefusedError, OperationalError): logger.info("database not ready, waiting two seconds") time.sleep(2) continue if success: logger.info("initialized database schema") - break + break if not success: msg = "database schema initialization failed (database not reachable?)" logger.error(msg) + await engine.dispose() + return - session = Session(bind=engine) - with TransactionManager(session).transaction(): + # Add the initial admins. + factory = _create_session_factory(engine) + async with factory() as session: admin_store = AdminStore(session) - if not admin_store.list(): - for admin in config.initial_admins: - logger.info("adding initial admin %s", admin) - admin_store.add(Admin(username=admin)) - session.close() + async with session.begin(): + if not await admin_store.list(): + for admin in config.initial_admins: + logger.info("adding initial admin %s", admin) + await admin_store.add(Admin(username=admin)) + await engine.dispose() diff --git a/src/gafaelfawr/dependencies/context.py b/src/gafaelfawr/dependencies/context.py index 9cf8c1365..b09efb81f 100644 --- a/src/gafaelfawr/dependencies/context.py +++ b/src/gafaelfawr/dependencies/context.py @@ -11,14 +11,15 @@ from aioredis import Redis from fastapi import Depends, Request -from fastapi_sqlalchemy import db from httpx import AsyncClient from safir.dependencies.http_client import http_client_dependency from safir.dependencies.logger import logger_dependency +from sqlalchemy.ext.asyncio import AsyncSession from structlog.stdlib import BoundLogger from gafaelfawr.config import Config from gafaelfawr.dependencies.config import config_dependency +from gafaelfawr.dependencies.db_session import db_session_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory from gafaelfawr.models.state import State @@ -48,6 +49,9 @@ class RequestContext: redis: Redis """Connection pool to use to talk to Redis.""" + session: AsyncSession + """The database session.""" + http_client: AsyncClient """Shared HTTP client.""" @@ -61,7 +65,7 @@ def factory(self) -> ComponentFactory: return ComponentFactory( config=self.config, redis=self.redis, - session=db.session, + session=self.session, http_client=self.http_client, logger=self.logger, ) @@ -95,6 +99,7 @@ async def context_dependency( config: Config = Depends(config_dependency), logger: BoundLogger = Depends(logger_dependency), redis: Redis = Depends(redis_dependency), + session: AsyncSession = Depends(db_session_dependency), http_client: AsyncClient = Depends(http_client_dependency), ) -> RequestContext: """Provides a RequestContext as a dependency.""" @@ -103,5 +108,6 @@ async def context_dependency( config=config, logger=logger, redis=redis, + session=session, http_client=http_client, ) diff --git a/src/gafaelfawr/dependencies/db_session.py b/src/gafaelfawr/dependencies/db_session.py new file mode 100644 index 000000000..8fd28f632 --- /dev/null +++ b/src/gafaelfawr/dependencies/db_session.py @@ -0,0 +1,92 @@ +"""Manage an async database session.""" + +from typing import AsyncIterator, Optional + +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + create_async_engine, +) +from sqlalchemy.orm import sessionmaker + +__all__ = ["DatabaseSessionDependency", "db_session_dependency"] + + +class DatabaseSessionDependency: + """Manages an async per-request SQLAlchemy session. + + Notes + ----- + Creation of the database session factory has to be deferred until the + configuration has been loaded, which in turn is deferred until app + startup. An app that uses this dependency must call: + + .. code-block:: python + + await db_session_dependency.initialize(database_url) + + from its startup hook and: + + .. code-block:: python + + await db_session_dependency.aclose() + + from its shutdown hook. + """ + + def __init__(self) -> None: + self._engine: Optional[AsyncEngine] = None + self._factory: Optional[sessionmaker] = None + + async def __call__(self) -> AsyncIterator[AsyncSession]: + """Create a database session and open a transaction. + + This implements a policy of one request equals one transaction, which + is closed when that request returns. + + Returns + ------- + session : `sqlalchemy.ext.asyncio.AsyncSession` + The newly-created session. + + Notes + ----- + This creates a new session for every request rather than using + `~sqlalchemy.ext.asyncio.async_scoped_session`. Experiments with the + latter showed that it added unwanted complexity around cleanup during + shutdown (it was hard to find all of the sessions to close them + cleanly, so test shutdown produced asyncpg warnings), and (at least as + of SQLAlchemy 1.4.27) the ``close_all`` method on an + `~sqlalchemy.ext.asyncio.AsyncSession` does not work. + + This can be revisited if creating a session each time causes + performance issues, but the session shares an underlying engine and + thus connection pool so hopefully this won't be an issue. + """ + assert self._factory, "db_session_dependency not initialized" + async with self._factory() as session: + async with session.begin(): + yield session + + async def aclose(self) -> None: + """Shut down the database engine.""" + if self._engine: + await self._engine.dispose() + self._engine = None + + async def initialize(self, url: str) -> None: + """Initialize the session dependency. + + Parameters + ---------- + url : `str` + The URL for the database. Must include any required password. + """ + self._engine = create_async_engine(url, future=True) + self._factory = sessionmaker( + self._engine, expire_on_commit=False, class_=AsyncSession + ) + + +db_session_dependency = DatabaseSessionDependency() +"""The dependency that will return the async session proxy.""" diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index b549bc13d..350914c95 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -7,8 +7,9 @@ import structlog from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker -from gafaelfawr.database import create_session from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.issuer import TokenIssuer @@ -28,7 +29,6 @@ from gafaelfawr.storage.kubernetes import KubernetesStorage from gafaelfawr.storage.oidc import OIDCAuthorization, OIDCAuthorizationStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore -from gafaelfawr.storage.transaction import TransactionManager from gafaelfawr.token_cache import TokenCache from gafaelfawr.verify import TokenVerifier @@ -36,7 +36,6 @@ from typing import AsyncIterator from aioredis import Redis - from sqlalchemy.orm import Session from structlog.stdlib import BoundLogger from gafaelfawr.config import Config @@ -84,26 +83,31 @@ async def standalone(cls) -> AsyncIterator[ComponentFactory]: logger.debug("Connecting to Redis") redis = await redis_dependency(config) logger.debug("Connecting to PostgreSQL") - session = create_session(config, logger) + engine = create_async_engine(config.database_url, future=True) try: - async with AsyncClient() as client: - yield cls( - config=config, - redis=redis, - session=session, - http_client=client, - logger=logger, - ) + factory = sessionmaker( + engine, expire_on_commit=False, class_=AsyncSession + ) + async with factory() as session: + async with session.begin(): + async with AsyncClient() as client: + yield cls( + config=config, + redis=redis, + session=session, + http_client=client, + logger=logger, + ) finally: await redis_dependency.close() - session.close() + await engine.dispose() def __init__( self, *, config: Config, redis: Redis, - session: Session, + session: AsyncSession, http_client: AsyncClient, logger: BoundLogger, ) -> None: @@ -123,10 +127,7 @@ def create_admin_service(self) -> AdminService: """ admin_store = AdminStore(self._session) admin_history_store = AdminHistoryStore(self._session) - transaction_manager = TransactionManager(self._session) - return AdminService( - admin_store, admin_history_store, transaction_manager - ) + return AdminService(admin_store, admin_history_store) def create_kubernetes_service(self) -> KubernetesService: """Create a Kubernetes service.""" @@ -230,15 +231,16 @@ def create_token_service(self) -> TokenService: storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) token_cache = TokenCache(token_redis_store) - token_change_store = TokenChangeHistoryStore(self._session) - transaction_manager = TransactionManager(self._session) + is_postgres = self._config.database_url.startswith("postgresql") + token_change_store = TokenChangeHistoryStore( + self._session, is_postgres + ) return TokenService( config=self._config, token_cache=token_cache, token_db_store=token_db_store, token_redis_store=token_redis_store, token_change_store=token_change_store, - transaction_manager=transaction_manager, logger=self._logger, ) diff --git a/src/gafaelfawr/handlers/api.py b/src/gafaelfawr/handlers/api.py index c402c4f28..29e10b26a 100644 --- a/src/gafaelfawr/handlers/api.py +++ b/src/gafaelfawr/handlers/api.py @@ -63,11 +63,11 @@ summary="List all administrators", tags=["admin"], ) -def get_admins( +async def get_admins( context: RequestContext = Depends(context_dependency), ) -> List[Admin]: admin_service = context.factory.create_admin_service() - return admin_service.get_admins() + return await admin_service.get_admins() @router.post( @@ -76,13 +76,13 @@ def get_admins( summary="Add new administrator", tags=["admin"], ) -def add_admin( +async def add_admin( admin: Admin, auth_data: TokenData = Depends(authenticate_admin_write), context: RequestContext = Depends(context_dependency), ) -> None: admin_service = context.factory.create_admin_service() - admin_service.add_admin( + await admin_service.add_admin( admin.username, actor=auth_data.username, ip_address=context.request.client.host, @@ -99,7 +99,7 @@ def add_admin( summary="Delete an administrator", tags=["admin"], ) -def delete_admin( +async def delete_admin( username: str = Path( ..., title="Administrator", @@ -113,7 +113,7 @@ def delete_admin( context: RequestContext = Depends(context_dependency), ) -> None: admin_service = context.factory.create_admin_service() - success = admin_service.delete_admin( + success = await admin_service.delete_admin( username, actor=auth_data.username, ip_address=context.request.client.host, @@ -136,7 +136,7 @@ def delete_admin( summary="Get token change history", tags=["admin"], ) -def get_admin_token_change_history( +async def get_admin_token_change_history( response: Response, cursor: Optional[str] = Query( None, @@ -206,7 +206,7 @@ def get_admin_token_change_history( context: RequestContext = Depends(context_dependency), ) -> List[Dict[str, Any]]: token_service = context.factory.create_token_service() - results = token_service.get_change_history( + results = await token_service.get_change_history( auth_data, cursor=cursor, limit=limit, @@ -234,7 +234,7 @@ def get_admin_token_change_history( summary="Initialize UI", tags=["browser"], ) -def get_login( +async def get_login( auth_data: TokenData = Depends(authenticate_session_read), context: RequestContext = Depends(context_dependency), ) -> APILoginResponse: @@ -267,7 +267,7 @@ async def get_token_info( context: RequestContext = Depends(context_dependency), ) -> TokenInfo: token_service = context.factory.create_token_service() - info = token_service.get_token_info_unchecked(auth_data.token.key) + info = await token_service.get_token_info_unchecked(auth_data.token.key) if info: return info else: @@ -331,7 +331,7 @@ async def get_user_info( summary="Get token change history", tags=["user"], ) -def get_user_token_change_history( +async def get_user_token_change_history( response: Response, username: str = Path( ..., @@ -391,7 +391,7 @@ def get_user_token_change_history( context: RequestContext = Depends(context_dependency), ) -> List[Dict[str, Any]]: token_service = context.factory.create_token_service() - results = token_service.get_change_history( + results = await token_service.get_change_history( auth_data, cursor=cursor, username=username, @@ -428,7 +428,7 @@ async def get_tokens( context: RequestContext = Depends(context_dependency), ) -> List[TokenInfo]: token_service = context.factory.create_token_service() - return token_service.list_tokens(auth_data, username) + return await token_service.list_tokens(auth_data, username) @router.post( @@ -494,7 +494,7 @@ async def get_token( context: RequestContext = Depends(context_dependency), ) -> TokenInfo: token_service = context.factory.create_token_service() - info = token_service.get_token_info(key, auth_data, username) + info = await token_service.get_token_info(key, auth_data, username) if info: return info else: @@ -611,7 +611,7 @@ async def get_token_change_history( context: RequestContext = Depends(context_dependency), ) -> List[Dict[str, Any]]: token_service = context.factory.create_token_service() - results = token_service.get_change_history( + results = await token_service.get_change_history( auth_data, username=username, key=key ) if not results.entries: diff --git a/src/gafaelfawr/handlers/login.py b/src/gafaelfawr/handlers/login.py index 1a0208943..eae44412e 100644 --- a/src/gafaelfawr/handlers/login.py +++ b/src/gafaelfawr/handlers/login.py @@ -230,7 +230,7 @@ async def handle_provider_return( # Construct a token. admin_service = context.factory.create_admin_service() - if admin_service.is_admin(user_info.username): + if await admin_service.is_admin(user_info.username): scopes = sorted(scopes + ["admin:token"]) token_service = context.factory.create_token_service() try: diff --git a/src/gafaelfawr/main.py b/src/gafaelfawr/main.py index 43ad7ff34..a7da78e63 100644 --- a/src/gafaelfawr/main.py +++ b/src/gafaelfawr/main.py @@ -6,18 +6,19 @@ from importlib.metadata import metadata from pathlib import Path from typing import TYPE_CHECKING -from urllib.parse import urlparse +import structlog from fastapi import FastAPI, status from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles -from fastapi_sqlalchemy import DBSessionMiddleware from safir.dependencies.http_client import http_client_dependency from safir.middleware.x_forwarded import XForwardedMiddleware from safir.models import ErrorModel from gafaelfawr.constants import COOKIE_NAME +from gafaelfawr.database import check_database from gafaelfawr.dependencies.config import config_dependency +from gafaelfawr.dependencies.db_session import db_session_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.exceptions import PermissionDeniedError, ValidationError from gafaelfawr.handlers import ( @@ -105,14 +106,9 @@ @app.on_event("startup") async def startup_event() -> None: config = await config_dependency() - engine_args = {} - if urlparse(config.database_url).scheme == "sqlite": - engine_args["connect_args"] = {"check_same_thread": False} - app.add_middleware( - DBSessionMiddleware, - db_url=config.database_url, - engine_args=engine_args, - ) + logger = structlog.get_logger(config.safir.logger_name) + await check_database(config.database_url, logger) + await db_session_dependency.initialize(config.database_url) app.add_middleware(XForwardedMiddleware, proxies=config.proxies) app.add_middleware( StateMiddleware, cookie_name=COOKIE_NAME, state_class=State @@ -122,6 +118,7 @@ async def startup_event() -> None: @app.on_event("shutdown") async def shutdown_event() -> None: await http_client_dependency.aclose() + await db_session_dependency.aclose() await redis_dependency.close() diff --git a/src/gafaelfawr/models/history.py b/src/gafaelfawr/models/history.py index 260f5c6b9..d41904f81 100644 --- a/src/gafaelfawr/models/history.py +++ b/src/gafaelfawr/models/history.py @@ -15,6 +15,7 @@ from gafaelfawr.util import ( current_datetime, normalize_datetime, + normalize_ip_address, normalize_scopes, ) @@ -86,6 +87,9 @@ class Config: _normalize_event_time = validator( "event_time", allow_reuse=True, pre=True )(normalize_datetime) + _normalize_ip_address = validator( + "ip_address", allow_reuse=True, pre=True + )(normalize_ip_address) @dataclass @@ -322,6 +326,9 @@ class Config: _normalize_old_scopes = validator( "old_scopes", allow_reuse=True, pre=True )(normalize_scopes) + _normalize_ip_address = validator( + "ip_address", allow_reuse=True, pre=True + )(normalize_ip_address) _normalize_event_time = validator( "event_time", allow_reuse=True, pre=True )(normalize_datetime) diff --git a/src/gafaelfawr/schema/__init__.py b/src/gafaelfawr/schema/__init__.py index 2db42c469..9e9a843e1 100644 --- a/src/gafaelfawr/schema/__init__.py +++ b/src/gafaelfawr/schema/__init__.py @@ -13,7 +13,7 @@ from gafaelfawr.schema.token_change_history import TokenChangeHistory if TYPE_CHECKING: - from sqlalchemy.engine import Engine + from sqlalchemy.ext.asyncio import AsyncEngine __all__ = [ "Admin", @@ -27,11 +27,13 @@ ] -def drop_schema(engine: Engine) -> None: +async def drop_schema(engine: AsyncEngine) -> None: """Drop all tables to reset the database.""" - Base.metadata.drop_all(engine) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) -def initialize_schema(engine: Engine) -> None: +async def initialize_schema(engine: AsyncEngine) -> None: """Initialize the database with all schema.""" - Base.metadata.create_all(engine) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) diff --git a/src/gafaelfawr/schema/admin_history.py b/src/gafaelfawr/schema/admin_history.py index 558787d6b..df2ee68c3 100644 --- a/src/gafaelfawr/schema/admin_history.py +++ b/src/gafaelfawr/schema/admin_history.py @@ -6,25 +6,30 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sqlalchemy import Column, DateTime, Enum, Index, Integer, String from sqlalchemy.dialects import postgresql from gafaelfawr.models.history import AdminChange from gafaelfawr.schema.base import Base +if TYPE_CHECKING: + from datetime import datetime + __all__ = ["AdminHistory"] class AdminHistory(Base): __tablename__ = "admin_history" - id = Column(Integer, primary_key=True) - username = Column(String(64), nullable=False) - action = Column(Enum(AdminChange), nullable=False) - actor = Column(String(64), nullable=False) - ip_address = Column( + id: int = Column(Integer, primary_key=True) + username: str = Column(String(64), nullable=False) + action: AdminChange = Column(Enum(AdminChange), nullable=False) + actor: str = Column(String(64), nullable=False) + ip_address: str = Column( String(64).with_variant(postgresql.INET, "postgresql"), nullable=False ) - event_time = Column(DateTime, nullable=False) + event_time: datetime = Column(DateTime, nullable=False) __table_args__ = (Index("admin_history_by_time", "event_time", "id"),) diff --git a/src/gafaelfawr/schema/subtoken.py b/src/gafaelfawr/schema/subtoken.py index e0a90bb16..7b4febdd5 100644 --- a/src/gafaelfawr/schema/subtoken.py +++ b/src/gafaelfawr/schema/subtoken.py @@ -2,21 +2,28 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sqlalchemy import Column, ForeignKey, Index, String from gafaelfawr.schema.base import Base +if TYPE_CHECKING: + from typing import Optional + __all__ = ["Subtoken"] class Subtoken(Base): __tablename__ = "subtoken" - child = Column( + child: str = Column( String(64), ForeignKey("token.token", ondelete="CASCADE"), primary_key=True, ) - parent = Column(String(64), ForeignKey("token.token", ondelete="SET NULL")) + parent: Optional[str] = Column( + String(64), ForeignKey("token.token", ondelete="SET NULL") + ) __table_args__ = (Index("subtoken_by_parent", "parent"),) diff --git a/src/gafaelfawr/schema/token.py b/src/gafaelfawr/schema/token.py index d33ea8687..743b10c84 100644 --- a/src/gafaelfawr/schema/token.py +++ b/src/gafaelfawr/schema/token.py @@ -2,29 +2,32 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sqlalchemy import Column, DateTime, Enum, Index, String, UniqueConstraint from gafaelfawr.models.token import TokenType from gafaelfawr.schema.base import Base +if TYPE_CHECKING: + from datetime import datetime + from typing import Optional + __all__ = ["Token"] class Token(Base): __tablename__ = "token" - token = Column( - String(64).with_variant(String(64, collation="C"), "postgresql"), - primary_key=True, - ) - username = Column(String(64), nullable=False) - token_type = Column(Enum(TokenType), nullable=False) - token_name = Column(String(64)) - scopes = Column(String(512), nullable=False) - service = Column(String(64)) - created = Column(DateTime, nullable=False) - last_used = Column(DateTime) - expires = Column(DateTime) + token: str = Column(String(64, collation="C"), primary_key=True) + username: str = Column(String(64), nullable=False) + token_type: TokenType = Column(Enum(TokenType), nullable=False) + token_name: Optional[str] = Column(String(64)) + scopes: str = Column(String(512), nullable=False) + service: Optional[str] = Column(String(64)) + created: datetime = Column(DateTime, nullable=False) + last_used: Optional[datetime] = Column(DateTime) + expires: Optional[datetime] = Column(DateTime) __table_args__ = ( UniqueConstraint("username", "token_name"), diff --git a/src/gafaelfawr/schema/token_auth_history.py b/src/gafaelfawr/schema/token_auth_history.py index 69224db11..09f6846f7 100644 --- a/src/gafaelfawr/schema/token_auth_history.py +++ b/src/gafaelfawr/schema/token_auth_history.py @@ -2,28 +2,36 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sqlalchemy import Column, DateTime, Enum, Index, Integer, String from sqlalchemy.dialects import postgresql from gafaelfawr.models.token import TokenType from gafaelfawr.schema.base import Base +if TYPE_CHECKING: + from datetime import datetime + from typing import Optional + __all__ = ["TokenAuthHistory"] class TokenAuthHistory(Base): __tablename__ = "token_auth_history" - id = Column(Integer, primary_key=True) - token = Column(String(64), nullable=False) - username = Column(String(64), nullable=False) - token_type = Column(Enum(TokenType), nullable=False) - token_name = Column(String(64)) - parent = Column(String(64)) - scopes = Column(String(512)) - service = Column(String(64)) - ip_address = Column(String(64).with_variant(postgresql.INET, "postgresql")) - event_time = Column(DateTime, nullable=False) + id: int = Column(Integer, primary_key=True) + token: str = Column(String(64), nullable=False) + username: str = Column(String(64), nullable=False) + token_type: TokenType = Column(Enum(TokenType), nullable=False) + token_name: Optional[str] = Column(String(64)) + parent: Optional[str] = Column(String(64)) + scopes: Optional[str] = Column(String(512)) + service: Optional[str] = Column(String(64)) + ip_address: Optional[str] = Column( + String(64).with_variant(postgresql.INET, "postgresql") + ) + event_time: datetime = Column(DateTime, nullable=False) __table_args__ = ( Index("token_auth_history_by_time", "event_time", "id"), diff --git a/src/gafaelfawr/schema/token_change_history.py b/src/gafaelfawr/schema/token_change_history.py index f59101074..92b5a129d 100644 --- a/src/gafaelfawr/schema/token_change_history.py +++ b/src/gafaelfawr/schema/token_change_history.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sqlalchemy import Column, DateTime, Enum, Index, Integer, String from sqlalchemy.dialects import postgresql @@ -9,28 +11,34 @@ from gafaelfawr.models.token import TokenType from gafaelfawr.schema.base import Base +if TYPE_CHECKING: + from datetime import datetime + from typing import Optional + __all__ = ["TokenChangeHistory"] class TokenChangeHistory(Base): __tablename__ = "token_change_history" - id = Column(Integer, primary_key=True) - token = Column(String(64), nullable=False) - username = Column(String(64), nullable=False) - token_type = Column(Enum(TokenType), nullable=False) - token_name = Column(String(64)) - parent = Column(String(64)) - scopes = Column(String(512), nullable=False) - service = Column(String(64)) - expires = Column(DateTime) - actor = Column(String(64)) - action = Column(Enum(TokenChange), nullable=False) - old_token_name = Column(String(64)) - old_scopes = Column(String(512)) - old_expires = Column(DateTime) - ip_address = Column(String(64).with_variant(postgresql.INET, "postgresql")) - event_time = Column(DateTime, nullable=False) + id: int = Column(Integer, primary_key=True) + token: str = Column(String(64), nullable=False) + username: str = Column(String(64), nullable=False) + token_type: TokenType = Column(Enum(TokenType), nullable=False) + token_name: Optional[str] = Column(String(64)) + parent: str = Column(String(64)) + scopes: str = Column(String(512), nullable=False) + service: Optional[str] = Column(String(64)) + expires: Optional[datetime] = Column(DateTime) + actor: Optional[str] = Column(String(64)) + action: TokenChange = Column(Enum(TokenChange), nullable=False) + old_token_name: Optional[str] = Column(String(64)) + old_scopes: Optional[str] = Column(String(512)) + old_expires: Optional[datetime] = Column(DateTime) + ip_address: Optional[str] = Column( + String(64).with_variant(postgresql.INET, "postgresql") + ) + event_time: datetime = Column(DateTime, nullable=False) __table_args__ = ( Index("token_change_history_by_time", "event_time", "id"), diff --git a/src/gafaelfawr/services/admin.py b/src/gafaelfawr/services/admin.py index 7aef8d07d..0d6803d58 100644 --- a/src/gafaelfawr/services/admin.py +++ b/src/gafaelfawr/services/admin.py @@ -14,7 +14,6 @@ from gafaelfawr.storage.admin import AdminStore from gafaelfawr.storage.history import AdminHistoryStore - from gafaelfawr.storage.transaction import TransactionManager __all__ = ["AdminService"] @@ -28,21 +27,17 @@ class AdminService: The backing store for token administrators. admin_history_store : `gafaelfawr.storage.history.AdminHistoryStore` The backing store for history of changes to token administrators. - transaction_manager : `gafaelfawr.storage.transaction.TransactionManager` - Database transaction manager. """ def __init__( - self, - admin_store: AdminStore, - admin_history_store: AdminHistoryStore, - transaction_manager: TransactionManager, + self, admin_store: AdminStore, admin_history_store: AdminHistoryStore ) -> None: self._admin_store = admin_store self._admin_history_store = admin_history_store - self._transaction_manager = transaction_manager - def add_admin(self, username: str, *, actor: str, ip_address: str) -> None: + async def add_admin( + self, username: str, *, actor: str, ip_address: str + ) -> None: """Add a new administrator. Parameters @@ -59,7 +54,7 @@ def add_admin(self, username: str, *, actor: str, ip_address: str) -> None: gafaelfawr.exceptions.PermissionDeniedError If the actor is not an admin. """ - if not self.is_admin(actor) and actor != "": + if not await self.is_admin(actor) and actor != "": raise PermissionDeniedError(f"{actor} is not an admin") admin = Admin(username=username) history_entry = AdminHistoryEntry( @@ -69,11 +64,10 @@ def add_admin(self, username: str, *, actor: str, ip_address: str) -> None: ip_address=ip_address, event_time=datetime.now(timezone.utc), ) - with self._transaction_manager.transaction(): - self._admin_store.add(admin) - self._admin_history_store.add(history_entry) + await self._admin_store.add(admin) + await self._admin_history_store.add(history_entry) - def delete_admin( + async def delete_admin( self, username: str, *, actor: str, ip_address: str ) -> bool: """Delete an administrator. @@ -98,7 +92,7 @@ def delete_admin( gafaelfawr.exceptions.PermissionDeniedError If the actor is not an admin. """ - if not self.is_admin(actor) and actor != "": + if not await self.is_admin(actor) and actor != "": raise PermissionDeniedError(f"{actor} is not an admin") admin = Admin(username=username) history_entry = AdminHistoryEntry( @@ -108,18 +102,17 @@ def delete_admin( ip_address=ip_address, event_time=datetime.now(timezone.utc), ) - with self._transaction_manager.transaction(): - if self.get_admins() == [admin]: - raise PermissionDeniedError("Cannot delete the last admin") - result = self._admin_store.delete(admin) - if result: - self._admin_history_store.add(history_entry) + if await self.get_admins() == [admin]: + raise PermissionDeniedError("Cannot delete the last admin") + result = await self._admin_store.delete(admin) + if result: + await self._admin_history_store.add(history_entry) return result - def get_admins(self) -> List[Admin]: + async def get_admins(self) -> List[Admin]: """Get the current administrators.""" - return self._admin_store.list() + return await self._admin_store.list() - def is_admin(self, username: str) -> bool: + async def is_admin(self, username: str) -> bool: """Returns whether the given user is a token administrator.""" - return any((username == a.username for a in self.get_admins())) + return any((username == a.username for a in await self.get_admins())) diff --git a/src/gafaelfawr/services/token.py b/src/gafaelfawr/services/token.py index c97f5f44f..734d29b87 100644 --- a/src/gafaelfawr/services/token.py +++ b/src/gafaelfawr/services/token.py @@ -39,7 +39,6 @@ from gafaelfawr.models.token import TokenInfo from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore - from gafaelfawr.storage.transaction import TransactionManager from gafaelfawr.token_cache import TokenCache __all__ = ["TokenService"] @@ -58,8 +57,6 @@ class TokenService: The Redis backing store for tokens. token_change_store : `gafaelfawr.storage.history.TokenChangeHistoryStore` The backing store for history of changes to tokens. - transaction_manager : `gafaelfawr.storage.transaction.TransactionManager` - Database transaction manager. logger : `structlog.BoundLogger` Logger to use. """ @@ -72,7 +69,6 @@ def __init__( token_db_store: TokenDatabaseStore, token_redis_store: TokenRedisStore, token_change_store: TokenChangeHistoryStore, - transaction_manager: TransactionManager, logger: BoundLogger, ) -> None: self._config = config @@ -80,7 +76,6 @@ def __init__( self._token_db_store = token_db_store self._token_redis_store = token_redis_store self._token_change_store = token_change_store - self._transaction_manager = transaction_manager self._logger = logger async def create_session_token( @@ -134,9 +129,8 @@ async def create_session_token( ) await self._token_redis_store.store_data(data) - with self._transaction_manager.transaction(): - self._token_db_store.add(data) - self._token_change_store.add(history_entry) + await self._token_db_store.add(data) + await self._token_change_store.add(history_entry) return token @@ -226,9 +220,8 @@ async def create_user_token( ) await self._token_redis_store.store_data(data) - with self._transaction_manager.transaction(): - self._token_db_store.add(data, token_name=token_name) - self._token_change_store.add(history_entry) + await self._token_db_store.add(data, token_name=token_name) + await self._token_change_store.add(history_entry) self._logger.info( "Created new user token", @@ -303,9 +296,8 @@ async def create_token_from_admin_request( ) await self._token_redis_store.store_data(data) - with self._transaction_manager.transaction(): - self._token_db_store.add(data, token_name=request.token_name) - self._token_change_store.add(history_entry) + await self._token_db_store.add(data, token_name=request.token_name) + await self._token_change_store.add(history_entry) if data.token_type == TokenType.user: self._logger.info( @@ -351,7 +343,7 @@ async def delete_token( success : `bool` Whether the token was found and deleted. """ - info = self.get_token_info_unchecked(key, username) + info = await self.get_token_info_unchecked(key, username) if not info: return False self._check_authorization(info.username, auth_data) @@ -360,16 +352,15 @@ async def delete_token( # returned in breadth-first order, so delete them in reverse order to # delete the tokens farthest down in the tree first. This minimizes # the number of orphaned children at any given point. - children = self._token_db_store.get_children(key) + children = await self._token_db_store.get_children(key) children.reverse() - with self._transaction_manager.transaction(): - for child in children: - await self._delete_one_token(child, auth_data, ip_address) - success = await self._delete_one_token(key, auth_data, ip_address) + for child in children: + await self._delete_one_token(child, auth_data, ip_address) + success = await self._delete_one_token(key, auth_data, ip_address) return success - def get_change_history( + async def get_change_history( self, auth_data: TokenData, *, @@ -429,7 +420,7 @@ def get_change_history( """ self._check_authorization(username, auth_data) self._validate_ip_or_cidr(ip_or_cidr) - return self._token_change_store.list( + return await self._token_change_store.list( cursor=HistoryCursor.from_str(cursor) if cursor else None, limit=limit, since=since, @@ -508,7 +499,7 @@ async def get_internal_token( return token # See if there's already a matching internal token. - key = self._token_db_store.get_internal_token_key( + key = await self._token_db_store.get_internal_token_key( token_data, service, scopes, self._minimum_expiration(token_data) ) if key: @@ -552,11 +543,10 @@ async def get_internal_token( ) await self._token_redis_store.store_data(data) - with self._transaction_manager.transaction(): - self._token_db_store.add( - data, service=service, parent=token_data.token.key - ) - self._token_change_store.add(history_entry) + await self._token_db_store.add( + data, service=service, parent=token_data.token.key + ) + await self._token_change_store.add(history_entry) self._logger.info( "Created new internal token", @@ -606,7 +596,7 @@ async def get_notebook_token( return token # See if there's already a matching notebook token. - key = self._token_db_store.get_notebook_token_key( + key = await self._token_db_store.get_notebook_token_key( token_data, self._minimum_expiration(token_data) ) if key: @@ -647,16 +637,15 @@ async def get_notebook_token( ) await self._token_redis_store.store_data(data) - with self._transaction_manager.transaction(): - self._token_db_store.add(data, parent=token_data.token.key) - self._token_change_store.add(history_entry) + await self._token_db_store.add(data, parent=token_data.token.key) + await self._token_change_store.add(history_entry) # Cache the token and return it. self._logger.info("Created new notebook token", key=token.key) self._token_cache.store_notebook_token(token, token_data) return token - def get_token_info( + async def get_token_info( self, key: str, auth_data: TokenData, username: Optional[str] ) -> Optional[TokenInfo]: """Get information about a token. @@ -672,13 +661,13 @@ def get_token_info( If set, constrain the result to tokens from that user and return `None` if the token exists but is for a different user. """ - info = self.get_token_info_unchecked(key, username) + info = await self.get_token_info_unchecked(key, username) if not info: return None self._check_authorization(info.username, auth_data) return info - def get_token_info_unchecked( + async def get_token_info_unchecked( self, key: str, username: Optional[str] = None ) -> Optional[TokenInfo]: """Get information about a token without checking authorization. @@ -691,7 +680,7 @@ def get_token_info_unchecked( If set, constrain the result to tokens from that user and return `None` if the token exists but is for a different user. """ - info = self._token_db_store.get_info(key) + info = await self._token_db_store.get_info(key) if not info: return None if username and info.username != username: @@ -711,7 +700,7 @@ async def get_user_info(self, token: Token) -> Optional[TokenUserInfo]: groups=data.groups, ) - def list_tokens( + async def list_tokens( self, auth_data: TokenData, username: Optional[str] = None ) -> List[TokenInfo]: """List tokens. @@ -736,7 +725,7 @@ def list_tokens( authentication information. """ self._check_authorization(username, auth_data) - return self._token_db_store.list(username=username) + return await self._token_db_store.list(username=username) async def modify_token( self, @@ -790,7 +779,7 @@ async def modify_token( ``auth_data`` or the user attempted to modify a token type other than user. """ - info = self.get_token_info_unchecked(key, username) + info = await self.get_token_info_unchecked(key, username) if not info: return None self._check_authorization(info.username, auth_data) @@ -823,32 +812,31 @@ async def modify_token( ip_address=ip_address, ) - with self._transaction_manager.transaction(): - info = self._token_db_store.modify( - key, - token_name=token_name, - scopes=sorted(scopes) if scopes else scopes, - expires=expires, - no_expire=no_expire, - ) - self._token_change_store.add(history_entry) - - # Update the expiration in Redis if needed. - if info and (no_expire or expires): - data = await self._token_redis_store.get_data_by_key(key) - if data: - data.expires = None if no_expire else expires - await self._token_redis_store.store_data(data) - else: - info = None - - # Update subtokens if needed. - if update_subtoken_expires and info: - assert expires - for child in self._token_db_store.get_children(key): - await self._modify_expires( - child, auth_data, expires, ip_address - ) + info = await self._token_db_store.modify( + key, + token_name=token_name, + scopes=sorted(scopes) if scopes else scopes, + expires=expires, + no_expire=no_expire, + ) + await self._token_change_store.add(history_entry) + + # Update the expiration in Redis if needed. + if info and (no_expire or expires): + data = await self._token_redis_store.get_data_by_key(key) + if data: + data.expires = None if no_expire else expires + await self._token_redis_store.store_data(data) + else: + info = None + + # Update subtokens if needed. + if update_subtoken_expires and info: + assert expires + for child in await self._token_db_store.get_children(key): + await self._modify_expires( + child, auth_data, expires, ip_address + ) if info: timestamp = int(info.expires.timestamp()) if info.expires else None @@ -916,7 +904,7 @@ async def _delete_one_token( """Helper function to delete a single token. This does not do cascading delete and assumes authorization has - already been checked. Must be called inside a transaction. + already been checked. Parameters ---------- @@ -933,7 +921,7 @@ async def _delete_one_token( success : `bool` Whether the token was found and deleted. """ - info = self.get_token_info_unchecked(key) + info = await self.get_token_info_unchecked(key) if not info: return False @@ -952,9 +940,9 @@ async def _delete_one_token( ) await self._token_redis_store.delete(key) - success = self._token_db_store.delete(key) + success = await self._token_db_store.delete(key) if success: - self._token_change_store.add(history_entry) + await self._token_change_store.add(history_entry) self._logger.info("Deleted token", key=key, username=info.username) return success @@ -1006,7 +994,7 @@ async def _modify_expires( ip_address : `str` The IP address from which the request came. """ - info = self.get_token_info_unchecked(key) + info = await self.get_token_info_unchecked(key) if not info: return if info.expires and info.expires <= expires: @@ -1027,8 +1015,8 @@ async def _modify_expires( ip_address=ip_address, ) - self._token_db_store.modify(key, expires=expires) - self._token_change_store.add(history_entry) + await self._token_db_store.modify(key, expires=expires) + await self._token_change_store.add(history_entry) data = await self._token_redis_store.get_data_by_key(key) if data: data.expires = expires diff --git a/src/gafaelfawr/storage/admin.py b/src/gafaelfawr/storage/admin.py index 4d0012b77..72d6044a1 100644 --- a/src/gafaelfawr/storage/admin.py +++ b/src/gafaelfawr/storage/admin.py @@ -2,7 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast + +from sqlalchemy import delete +from sqlalchemy.engine import CursorResult +from sqlalchemy.future import select from gafaelfawr.models.admin import Admin from gafaelfawr.schema import Admin as SQLAdmin @@ -10,7 +14,7 @@ if TYPE_CHECKING: from typing import List - from sqlalchemy.orm import Session + from sqlalchemy.ext.asyncio import AsyncSession __all__ = ["AdminStore"] @@ -20,19 +24,19 @@ class AdminStore: Parameters ---------- - session : `sqlalchemy.orm.Session` - The underlying database session. + session : `sqlalchemy.ext.asyncio.AsyncSession` + The database session proxy. """ - def __init__(self, session: Session) -> None: + def __init__(self, session: AsyncSession) -> None: self._session = session - def add(self, admin: Admin) -> None: + async def add(self, admin: Admin) -> None: """Add a new token administrator.""" new = SQLAdmin(username=admin.username) self._session.add(new) - def delete(self, admin: Admin) -> bool: + async def delete(self, admin: Admin) -> bool: """Delete an administrator. Parameters @@ -46,18 +50,12 @@ def delete(self, admin: Admin) -> bool: `True` if the administrator was found and deleted, `False` otherwise. """ - result = ( - self._session.query(SQLAdmin) - .filter_by(username=admin.username) - .delete(synchronize_session=False) - ) - return result > 0 - - def list(self) -> List[Admin]: + stmt = delete(SQLAdmin).where(SQLAdmin.username == admin.username) + result = cast(CursorResult, await self._session.execute(stmt)) + return result.rowcount > 0 + + async def list(self) -> List[Admin]: """Return a list of current administrators.""" - return [ - Admin.from_orm(a) - for a in self._session.query(SQLAdmin) - .order_by(SQLAdmin.username) - .all() - ] + stmt = select(SQLAdmin).order_by(SQLAdmin.username) + result = await self._session.scalars(stmt) + return [Admin.from_orm(a) for a in result.all()] diff --git a/src/gafaelfawr/storage/history.py b/src/gafaelfawr/storage/history.py index 4ece899f3..2e220dfc4 100644 --- a/src/gafaelfawr/storage/history.py +++ b/src/gafaelfawr/storage/history.py @@ -5,7 +5,8 @@ import re from typing import TYPE_CHECKING -from sqlalchemy import and_, or_ +from sqlalchemy import and_, func, or_ +from sqlalchemy.future import select from sqlalchemy.sql import text from gafaelfawr.models.history import ( @@ -14,13 +15,14 @@ TokenChangeHistoryEntry, ) from gafaelfawr.schema import AdminHistory, TokenChangeHistory -from gafaelfawr.util import normalize_datetime +from gafaelfawr.util import datetime_to_db, normalize_datetime if TYPE_CHECKING: from datetime import datetime from typing import Optional - from sqlalchemy.orm import Query, Session + from sqlalchemy.ext.asyncio import AsyncSession + from sqlalchemy.sql import Select from gafaelfawr.models.history import AdminHistoryEntry from gafaelfawr.models.token import TokenType @@ -33,16 +35,17 @@ class AdminHistoryStore: Parameters ---------- - session : `sqlalchemy.orm.Session` - The underlying database session. + session : `sqlalchemy.ext.AsyncSession` + The database session proxy. """ - def __init__(self, session: Session) -> None: + def __init__(self, session: AsyncSession) -> None: self._session = session - def add(self, entry: AdminHistoryEntry) -> None: + async def add(self, entry: AdminHistoryEntry) -> None: """Record a change to the token administrators.""" new = AdminHistory(**entry.dict()) + new.event_time = datetime_to_db(entry.event_time) self._session.add(new) @@ -51,14 +54,17 @@ class TokenChangeHistoryStore: Parameters ---------- - session : `sqlalchemy.orm.Session` - The underlying database session. + session : `sqlalchemy.ext.asyncio.AsyncSession` + The database session proxy. + is_postgres : `bool` + Whether the backend database is PostgreSQL. """ - def __init__(self, session: Session) -> None: + def __init__(self, session: AsyncSession, is_postgres: bool) -> None: self._session = session + self._is_postgres = is_postgres - def add(self, entry: TokenChangeHistoryEntry) -> None: + async def add(self, entry: TokenChangeHistoryEntry) -> None: """Record a change to a token.""" entry_dict = entry.dict() @@ -69,9 +75,12 @@ def add(self, entry: TokenChangeHistoryEntry) -> None: entry_dict["old_scopes"] = ",".join(sorted(entry.old_scopes)) new = TokenChangeHistory(**entry_dict) + new.expires = datetime_to_db(entry.expires) + new.old_expires = datetime_to_db(entry.old_expires) + new.event_time = datetime_to_db(entry.event_time) self._session.add(new) - def list( + async def list( self, *, cursor: Optional[HistoryCursor] = None, @@ -119,70 +128,74 @@ def list( entries : List[`gafaelfawr.models.history.TokenChangeHistoryEntry`] List of change history entries, which may be empty. """ - query = self._session.query(TokenChangeHistory) + stmt = select(TokenChangeHistory) if since: - query = query.filter(TokenChangeHistory.event_time >= since) + since = datetime_to_db(since) + stmt = stmt.where(TokenChangeHistory.event_time >= since) if until: - query = query.filter(TokenChangeHistory.event_time <= until) + until = datetime_to_db(until) + stmt = stmt.where(TokenChangeHistory.event_time <= until) if username: - query = query.filter_by(username=username) + stmt = stmt.where(TokenChangeHistory.username == username) if actor: - query = query.filter_by(actor=actor) + stmt = stmt.where(TokenChangeHistory.actor == actor) if key: - query = query.filter( + stmt = stmt.where( or_( TokenChangeHistory.token == key, TokenChangeHistory.parent == key, ) ) if token: - query = query.filter_by(token=token) + stmt = stmt.where(TokenChangeHistory.token == token) if token_type: - query = query.filter_by(token_type=token_type) + stmt = stmt.where(TokenChangeHistory.token_type == token_type) if ip_or_cidr: - query = self._apply_ip_or_cidr_filter(query, ip_or_cidr) + stmt = self._apply_ip_or_cidr_filter(stmt, ip_or_cidr) # Shunt the complicated case of a paginated query to a separate # function to keep the logic more transparent. if cursor or limit: - return self._paginated_query(query, cursor, limit) + return await self._paginated_query(stmt, cursor, limit) # Perform the query and return the results. - query = query.order_by( + stmt = stmt.order_by( TokenChangeHistory.event_time.desc(), TokenChangeHistory.id.desc() ) - entries = query.all() - return PaginatedHistory[TokenChangeHistoryEntry]( + result = await self._session.scalars(stmt) + entries = result.all() + history = PaginatedHistory[TokenChangeHistoryEntry]( entries=[TokenChangeHistoryEntry.from_orm(e) for e in entries], count=len(entries), prev_cursor=None, next_cursor=None, ) + return history - def _paginated_query( + async def _paginated_query( self, - query: Query, + stmt: Select, cursor: Optional[HistoryCursor], limit: Optional[int], ) -> PaginatedHistory[TokenChangeHistoryEntry]: """Run a paginated query (one with a limit or a cursor).""" - limited_query = query + limited_stmt = stmt # Apply the cursor, if there is one. if cursor: - limited_query = self._apply_cursor(limited_query, cursor) + limited_stmt = self._apply_cursor(limited_stmt, cursor) # When retrieving a previous set of results using a previous # cursor, we have to reverse the sort algorithm so that the cursor # boundary can be applied correctly. We'll then later reverse the # result set to return it in proper forward-sorted order. if cursor and cursor.previous: - limited_query = limited_query.order_by( + limited_stmt = limited_stmt.order_by( TokenChangeHistory.event_time, TokenChangeHistory.id ) else: - limited_query = limited_query.order_by( + limited_stmt = limited_stmt.order_by( TokenChangeHistory.event_time.desc(), TokenChangeHistory.id.desc(), ) @@ -191,12 +204,14 @@ def _paginated_query( # to create a cursor (because there are more elements) and what the # cursor value should be (for forward cursors). if limit: - limited_query = limited_query.limit(limit + 1) + limited_stmt = limited_stmt.limit(limit + 1) # Execute the query twice, once to get the next bach of results and # once to get the count of all entries without pagination. - entries = limited_query.all() - count = query.count() + result = await self._session.scalars(limited_stmt) + entries = result.all() + count_stmt = select(func.count()).select_from(stmt.subquery()) + count = await self._session.scalar(count_stmt) # Calculate the cursors, remove the extra element we asked for, and # reverse the results again if we did a reverse sort because we were @@ -226,24 +241,25 @@ def _paginated_query( ) @staticmethod - def _apply_cursor(query: Query, cursor: HistoryCursor) -> Query: + def _apply_cursor(stmt: Select, cursor: HistoryCursor) -> Select: """Apply a cursor to a query.""" + time = datetime_to_db(cursor.time) if cursor.previous: - return query.filter( + return stmt.where( or_( - TokenChangeHistory.event_time > cursor.time, + TokenChangeHistory.event_time > time, and_( - TokenChangeHistory.event_time == cursor.time, + TokenChangeHistory.event_time == time, TokenChangeHistory.id > cursor.id, ), ) ) else: - return query.filter( + return stmt.where( or_( - TokenChangeHistory.event_time < cursor.time, + TokenChangeHistory.event_time < time, and_( - TokenChangeHistory.event_time == cursor.time, + TokenChangeHistory.event_time == time, TokenChangeHistory.id <= cursor.id, ), ) @@ -263,7 +279,9 @@ def _build_prev_cursor(entry: TokenChangeHistory) -> HistoryCursor: assert prev_time return HistoryCursor(time=prev_time, id=entry.id, previous=True) - def _apply_ip_or_cidr_filter(self, query: Query, ip_or_cidr: str) -> Query: + def _apply_ip_or_cidr_filter( + self, stmt: Select, ip_or_cidr: str + ) -> Select: """Apply an appropriate filter for an IP or CIDR block. If the underlying database is not PostgreSQL, which supports native @@ -272,15 +290,14 @@ def _apply_ip_or_cidr_filter(self, query: Query, ip_or_cidr: str) -> Query: but the intended supported database is PostgreSQL anyway. """ if "/" in ip_or_cidr: - if self._session.get_bind().name == "postgresql": - return query.filter(text(":c >> ip_address")).params( - c=ip_or_cidr - ) + cidr = ip_or_cidr + if self._is_postgres: + return stmt.where(text(":c >> ip_address")).params(c=cidr) else: if ":" in str(ip_or_cidr): - net = re.sub("::/[0-9]+$", ":%", ip_or_cidr) + net = re.sub("::/[0-9]+$", ":%", cidr) else: - net = re.sub(r"(\.0)+/[0-9]+$", ".%", ip_or_cidr) - return query.filter(TokenChangeHistory.ip_address.like(net)) + net = re.sub(r"(\.0)+/[0-9]+$", ".%", cidr) + return stmt.where(TokenChangeHistory.ip_address.like(net)) else: - return query.filter_by(ip_address=str(ip_or_cidr)) + return stmt.where(TokenChangeHistory.ip_address == str(ip_or_cidr)) diff --git a/src/gafaelfawr/storage/token.py b/src/gafaelfawr/storage/token.py index 2608d549a..c123a2c09 100644 --- a/src/gafaelfawr/storage/token.py +++ b/src/gafaelfawr/storage/token.py @@ -3,17 +3,22 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast + +from sqlalchemy import delete +from sqlalchemy.engine import CursorResult +from sqlalchemy.future import select from gafaelfawr.exceptions import DeserializeException, DuplicateTokenNameError from gafaelfawr.models.token import TokenInfo, TokenType from gafaelfawr.schema.subtoken import Subtoken from gafaelfawr.schema.token import Token as SQLToken +from gafaelfawr.util import datetime_to_db if TYPE_CHECKING: from typing import List, Optional - from sqlalchemy.orm import Session + from sqlalchemy.ext.asyncio import AsyncSession from structlog.stdlib import BoundLogger from gafaelfawr.models.token import Token, TokenData @@ -33,14 +38,14 @@ class TokenDatabaseStore: Parameters ---------- - session : `sqlalchemy.orm.Session` - The underlying database session. + session : `sqlalchemy.ext.asyncio.AsyncSession` + The database session proxy. """ - def __init__(self, session: Session) -> None: + def __init__(self, session: AsyncSession) -> None: self._session = session - def add( + async def add( self, data: TokenData, *, @@ -67,14 +72,7 @@ def add( The user already has a token by that name. """ if token_name: - name_conflict = ( - self._session.query(SQLToken.token) - .filter_by(username=data.username, token_name=token_name) - .scalar() - ) - if name_conflict: - msg = f"Token name {token_name} already used" - raise DuplicateTokenNameError(msg) + await self._check_name_conflict(data.username, token_name) new = SQLToken( token=data.token.key, username=data.username, @@ -82,16 +80,16 @@ def add( token_name=token_name, scopes=",".join(sorted(data.scopes)), service=service, - created=data.created, - expires=data.expires, + created=datetime_to_db(data.created), + expires=datetime_to_db(data.expires), ) self._session.add(new) - self._session.flush() + await self._session.flush() if parent: subtoken = Subtoken(parent=parent, child=data.token.key) self._session.add(subtoken) - def delete(self, key: str) -> bool: + async def delete(self, key: str) -> bool: """Delete a token. Parameters @@ -104,9 +102,11 @@ def delete(self, key: str) -> bool: success : `bool` Whether the token was found to be deleted. """ - return self._session.query(SQLToken).filter_by(token=key).delete() >= 1 + stmt = delete(SQLToken).where(SQLToken.token == key) + result = cast(CursorResult, await self._session.execute(stmt)) + return result.rowcount >= 1 - def get_children(self, key: str) -> List[str]: + async def get_children(self, key: str) -> List[str]: """Return all children (recursively) of a token. Parameters @@ -124,17 +124,14 @@ def get_children(self, key: str) -> List[str]: all_children = [] parents = [key] while parents: - records = ( - self._session.query(Subtoken.child) - .filter(Subtoken.parent.in_(parents)) - .all() - ) - children = [r.child for r in records] + stmt = select(Subtoken.child).where(Subtoken.parent.in_(parents)) + result = await self._session.scalars(stmt) + children = result.all() all_children.extend(children) parents = children return all_children - def get_info(self, key: str) -> Optional[TokenInfo]: + async def get_info(self, key: str) -> Optional[TokenInfo]: """Return information about a token. Parameters @@ -156,20 +153,20 @@ def get_info(self, key: str) -> Optional[TokenInfo]: only one database query without fancy ORM mappings at the cost of some irritating mangling of the return value. """ - result = ( - self._session.query(SQLToken, Subtoken.parent) - .filter_by(token=key) + stmt = ( + select(SQLToken, Subtoken.parent) + .where(SQLToken.token == key) .join(Subtoken, Subtoken.child == SQLToken.token, isouter=True) - .one_or_none() ) - if result: - info = TokenInfo.from_orm(result[0]) - info.parent = result[1] - return info - else: + result = (await self._session.execute(stmt)).one_or_none() + if not result: return None + token, parent = result + info = TokenInfo.from_orm(token) + info.parent = parent + return info - def get_internal_token_key( + async def get_internal_token_key( self, token_data: TokenData, service: str, @@ -195,21 +192,21 @@ def get_internal_token_key( The key of an existing internal child token with the desired properties, or `None` if none exist. """ - key = ( - self._session.query(Subtoken.child) - .filter_by(parent=token_data.token.key) + stmt = ( + select(Subtoken.child) + .where(Subtoken.parent == token_data.token.key) .join(SQLToken, Subtoken.child == SQLToken.token) - .filter( + .where( SQLToken.token_type == TokenType.internal, SQLToken.service == service, SQLToken.scopes == ",".join(sorted(scopes)), - SQLToken.expires >= min_expires, + SQLToken.expires >= datetime_to_db(min_expires), ) - .first() + .limit(1) ) - return key[0] if key else None + return await self._session.scalar(stmt) - def get_notebook_token_key( + async def get_notebook_token_key( self, token_data: TokenData, min_expires: datetime ) -> Optional[str]: """Retrieve an existing notebook child token. @@ -227,19 +224,19 @@ def get_notebook_token_key( The key of an existing notebook child token, or `None` if none exist. """ - key = ( - self._session.query(Subtoken.child) - .filter_by(parent=token_data.token.key) + stmt = ( + select(Subtoken.child) + .where(Subtoken.parent == token_data.token.key) .join(SQLToken, Subtoken.child == SQLToken.token) - .filter( + .where( SQLToken.token_type == TokenType.notebook, - SQLToken.expires >= min_expires, + SQLToken.expires >= datetime_to_db(min_expires), ) - .first() + .limit(1) ) - return key[0] if key else None + return await self._session.scalar(stmt) - def list(self, *, username: Optional[str] = None) -> List[TokenInfo]: + async def list(self, *, username: Optional[str] = None) -> List[TokenInfo]: """List tokens. Parameters @@ -252,17 +249,18 @@ def list(self, *, username: Optional[str] = None) -> List[TokenInfo]: tokens : List[`gafaelfawr.models.token.TokenInfo`] Information about the tokens. """ - tokens = self._session.query(SQLToken) + stmt = select(SQLToken) if username: - tokens = tokens.filter_by(username=username) - tokens = tokens.order_by( + stmt = stmt.where(SQLToken.username == username) + stmt = stmt.order_by( SQLToken.last_used.desc(), SQLToken.created.desc(), SQLToken.token, ) - return [TokenInfo.from_orm(t) for t in tokens] + result = await self._session.scalars(stmt) + return [TokenInfo.from_orm(t) for t in result.all()] - def modify( + async def modify( self, key: str, *, @@ -297,27 +295,33 @@ def modify( gafaelfawr.exceptions.DuplicateTokenNameError The user already has a token by that name. """ - token = self._session.query(SQLToken).filter_by(token=key).scalar() + stmt = select(SQLToken).where(SQLToken.token == key) + token = await self._session.scalar(stmt) if not token: return None - if token_name: - name_conflict = ( - self._session.query(SQLToken.token) - .filter_by(username=token.username, token_name=token_name) - .scalar() - ) - if name_conflict: - msg = f"Token name {token_name} already used" - raise DuplicateTokenNameError(msg) + if token_name and token.token_name != token_name: + await self._check_name_conflict(token.username, token_name) token.token_name = token_name if scopes: token.scopes = ",".join(sorted(scopes)) if no_expire: token.expires = None elif expires: - token.expires = expires + token.expires = datetime_to_db(expires) return TokenInfo.from_orm(token) + async def _check_name_conflict( + self, username: str, token_name: str + ) -> None: + """Raise exception if the given token name is already used.""" + stmt = select(SQLToken.token).filter_by( + username=username, token_name=token_name + ) + name_conflict = await self._session.scalar(stmt) + if name_conflict: + msg = f"Token name {token_name} already used" + raise DuplicateTokenNameError(msg) + class TokenRedisStore: """Stores and retrieves token data in Redis. diff --git a/src/gafaelfawr/storage/transaction.py b/src/gafaelfawr/storage/transaction.py deleted file mode 100644 index 1ddee3c83..000000000 --- a/src/gafaelfawr/storage/transaction.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Manage database transactions.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from types import TracebackType - from typing import Literal, Optional - - from sqlalchemy.orm import Session - -__all__ = ["Transaction", "TransactionManager"] - - -class Transaction: - """Returned by a TransactionManager as a context manager. - - This will automatically commit the transaction at the end of the context - block and automatically roll back if there was an exception. - - Parameters - ---------- - session : `sqlalchemy.orm.Session` - The database session. - """ - - def __init__(self, session: Session) -> None: - self._session = session - - def __enter__(self) -> None: - pass - - def __exit__( - self, - exc_type: Optional[type], - exc_val: Optional[Exception], - exc_tb: Optional[TracebackType], - ) -> Literal[False]: - if exc_type: - self._session.rollback() - else: - self._session.commit() - return False - - -class TransactionManager: - """Manage SQL database transactions. - - Parameters - ---------- - session : `sqlalchemy.orm.Session` - The database session. - """ - - def __init__(self, session: Session) -> None: - self._session = session - - def transaction(self) -> Transaction: - """Start a new transaction.""" - return Transaction(self._session) diff --git a/src/gafaelfawr/util.py b/src/gafaelfawr/util.py index c17d4494f..71c2f9bc8 100644 --- a/src/gafaelfawr/util.py +++ b/src/gafaelfawr/util.py @@ -5,7 +5,8 @@ import base64 import os from datetime import datetime, timezone -from typing import TYPE_CHECKING +from ipaddress import IPv4Address, IPv6Address +from typing import TYPE_CHECKING, overload if TYPE_CHECKING: from typing import List, Optional, Union @@ -14,6 +15,7 @@ "add_padding", "base64_to_number", "current_datetime", + "datetime_to_db", "normalize_datetime", "number_to_base64", "random_128_bits", @@ -68,6 +70,21 @@ def current_datetime() -> datetime: return datetime.now(tz=timezone.utc).replace(microsecond=0) +@overload +def datetime_to_db(time: datetime) -> datetime: + ... + + +@overload +def datetime_to_db(time: None) -> None: + ... + + +def datetime_to_db(time: Optional[datetime]) -> Optional[datetime]: + """Strip time zone for storing a datetime in the database.""" + return time.replace(tzinfo=None) if time else None + + def normalize_datetime( v: Optional[Union[int, datetime]] ) -> Optional[datetime]: @@ -98,6 +115,32 @@ def normalize_datetime( return v.replace(tzinfo=timezone.utc) +def normalize_ip_address( + v: Optional[Union[str, IPv4Address, IPv6Address]] +) -> Optional[str]: + """Pydantic validator for IP address fields. + + Convert the PostgreSQL INET type to `str` to support reading entries from + a PostgreSQL database. + + Parameters + ---------- + v : `str` or `ipaddress.IPv4Address` or `ipaddress.IPv6Address` or `None` + The field representing an IP address. + + Returns + ------- + v : `str` or `None` + The converted IP address. + """ + if v is None: + return v + elif isinstance(v, (IPv4Address, IPv6Address)): + return str(v) + else: + return v + + def normalize_scopes( v: Optional[Union[str, List[str]]] ) -> Optional[List[str]]: diff --git a/tests/cli_test.py b/tests/cli_test.py index 28fd0af68..85404132e 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -8,6 +8,7 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING from click.testing import CliRunner @@ -66,7 +67,7 @@ def test_help() -> None: def test_update_service_tokens( tmp_path: Path, mock_kubernetes: MockKubernetesApi ) -> None: - initialize(tmp_path) + asyncio.run(initialize(tmp_path)) mock_kubernetes.create_namespaced_custom_object( "gafaelfawr.lsst.io", "v1alpha1", @@ -99,7 +100,7 @@ def test_update_service_tokens_error( mock_kubernetes: MockKubernetesApi, caplog: LogCaptureFixture, ) -> None: - initialize(tmp_path) + asyncio.run(initialize(tmp_path)) def error_callback(method: str, *args: Any) -> None: if method == "list_cluster_custom_object": diff --git a/tests/handlers/api_admins_test.py b/tests/handlers/api_admins_test.py index 4bd219c0f..8308fa1bd 100644 --- a/tests/handlers/api_admins_test.py +++ b/tests/handlers/api_admins_test.py @@ -35,7 +35,10 @@ async def test_admins(setup: SetupTest) -> None: assert r.json() == [{"username": "admin"}] admin_service = setup.factory.create_admin_service() - admin_service.add_admin("example", actor="admin", ip_address="127.0.0.1") + await admin_service.add_admin( + "example", actor="admin", ip_address="127.0.0.1" + ) + await setup.session.commit() r = await setup.client.get( "/auth/api/v1/admins", diff --git a/tests/handlers/api_history_test.py b/tests/handlers/api_history_test.py index dc566a490..ac5c2f485 100644 --- a/tests/handlers/api_history_test.py +++ b/tests/handlers/api_history_test.py @@ -9,6 +9,7 @@ from urllib.parse import urlencode import pytest +from sqlalchemy.future import select from gafaelfawr.models.history import TokenChangeHistoryEntry from gafaelfawr.models.link import LinkData @@ -19,7 +20,7 @@ TokenUserInfo, ) from gafaelfawr.schema import TokenChangeHistory -from gafaelfawr.util import current_datetime +from gafaelfawr.util import current_datetime, datetime_to_db from tests.support.constants import TEST_HOSTNAME if TYPE_CHECKING: @@ -142,19 +143,17 @@ async def build_history( # Spread out the timestamps so that we can test date range queries. Every # other entry has the same timestamp as the previous entry to test that # queries handle entries with the same timestamp. - entries = ( - setup.session.query(TokenChangeHistory) - .order_by(TokenChangeHistory.id) - .all() - ) + stmt = select(TokenChangeHistory).order_by(TokenChangeHistory.id) + result = await setup.session.execute(stmt) + entries = [e[0] for e in result.all()] event_time = current_datetime() - timedelta(seconds=len(entries) * 5) - with setup.transaction(): - for i, entry in enumerate(entries): - entry.event_time = event_time - if i % 2 != 0: - event_time += timedelta(seconds=5) + for i, entry in enumerate(entries): + entry.event_time = datetime_to_db(event_time) + if i % 2 != 0: + event_time += timedelta(seconds=5) + await setup.session.commit() - history = token_service.get_change_history(service_token_data) + history = await token_service.get_change_history(service_token_data) assert history.count == 20 assert len(history.entries) == 20 return history.entries diff --git a/tests/handlers/api_tokens_test.py b/tests/handlers/api_tokens_test.py index b94192ecf..8424451e3 100644 --- a/tests/handlers/api_tokens_test.py +++ b/tests/handlers/api_tokens_test.py @@ -40,6 +40,7 @@ async def test_create_delete_modify( ip_address="127.0.0.1", ) csrf = await setup.login(session_token) + await setup.session.commit() expires = current_datetime() + timedelta(days=100) r = await setup.client.post( @@ -223,6 +224,7 @@ async def test_token_info(setup: SetupTest) -> None: session_token = await token_service.create_session_token( user_info, scopes=["exec:admin", "user:token"], ip_address="127.0.0.1" ) + await setup.session.commit() r = await setup.client.get( "/auth/api/v1/token-info", @@ -275,6 +277,7 @@ async def test_token_info(setup: SetupTest) -> None: expires=expires, ip_address="127.0.0.1", ) + await setup.session.commit() r = await setup.client.get( "/auth/api/v1/token-info", @@ -471,6 +474,7 @@ async def test_no_scope(setup: SetupTest) -> None: scopes=[], ip_address="127.0.0.1", ) + await setup.session.commit() r = await setup.client.get( f"/auth/api/v1/users/{token_data.username}/tokens", @@ -540,6 +544,7 @@ async def test_wrong_user(setup: SetupTest) -> None: scopes=[], ip_address="127.0.0.1", ) + await setup.session.commit() # Get a token list. r = await setup.client.get("/auth/api/v1/users/other-person/tokens") diff --git a/tests/handlers/login_github_test.py b/tests/handlers/login_github_test.py index 4b78341b1..a6ae4bf71 100644 --- a/tests/handlers/login_github_test.py +++ b/tests/handlers/login_github_test.py @@ -313,7 +313,10 @@ async def test_github_uppercase(setup: SetupTest) -> None: async def test_github_admin(setup: SetupTest) -> None: """Test that a token administrator gets the admin:token scope.""" admin_service = setup.factory.create_admin_service() - admin_service.add_admin("someuser", actor="admin", ip_address="127.0.0.1") + await admin_service.add_admin( + "someuser", actor="admin", ip_address="127.0.0.1" + ) + await setup.session.commit() user_info = GitHubUserInfo( name="A User", username="someuser", diff --git a/tests/services/admin_test.py b/tests/services/admin_test.py index b3eb95731..87f6d5c26 100644 --- a/tests/services/admin_test.py +++ b/tests/services/admin_test.py @@ -13,46 +13,60 @@ from tests.support.setup import SetupTest -def test_add(setup: SetupTest) -> None: +@pytest.mark.asyncio +async def test_add(setup: SetupTest) -> None: admin_service = setup.factory.create_admin_service() - assert admin_service.get_admins() == [Admin(username="admin")] + assert await admin_service.get_admins() == [Admin(username="admin")] - admin_service.add_admin("example", actor="admin", ip_address="192.168.0.1") + await admin_service.add_admin( + "example", actor="admin", ip_address="192.168.0.1" + ) - assert admin_service.get_admins() == [ + assert await admin_service.get_admins() == [ Admin(username="admin"), Admin(username="example"), ] - assert admin_service.is_admin("example") - assert not admin_service.is_admin("foo") + assert await admin_service.is_admin("example") + assert not await admin_service.is_admin("foo") with pytest.raises(PermissionDeniedError): - admin_service.add_admin("foo", actor="bar", ip_address="127.0.0.1") + await admin_service.add_admin( + "foo", actor="bar", ip_address="127.0.0.1" + ) - admin_service.add_admin("foo", actor="", ip_address="127.0.0.1") - assert admin_service.is_admin("foo") - assert not admin_service.is_admin("") + await admin_service.add_admin( + "foo", actor="", ip_address="127.0.0.1" + ) + assert await admin_service.is_admin("foo") + assert not await admin_service.is_admin("") -def test_delete(setup: SetupTest) -> None: +@pytest.mark.asyncio +async def test_delete(setup: SetupTest) -> None: admin_service = setup.factory.create_admin_service() - assert admin_service.get_admins() == [Admin(username="admin")] + assert await admin_service.get_admins() == [Admin(username="admin")] with pytest.raises(PermissionDeniedError): - admin_service.delete_admin( + await admin_service.delete_admin( "admin", actor="admin", ip_address="127.0.0.1" ) - admin_service.add_admin("example", actor="admin", ip_address="127.0.0.1") - admin_service.delete_admin("admin", actor="admin", ip_address="127.0.0.1") - assert admin_service.is_admin("example") - assert not admin_service.is_admin("admin") - assert admin_service.get_admins() == [Admin(username="example")] + await admin_service.add_admin( + "example", actor="admin", ip_address="127.0.0.1" + ) + await admin_service.delete_admin( + "admin", actor="admin", ip_address="127.0.0.1" + ) + assert await admin_service.is_admin("example") + assert not await admin_service.is_admin("admin") + assert await admin_service.get_admins() == [Admin(username="example")] - admin_service.add_admin("other", actor="example", ip_address="127.0.0.1") - admin_service.delete_admin( + await admin_service.add_admin( + "other", actor="example", ip_address="127.0.0.1" + ) + await admin_service.delete_admin( "other", actor="", ip_address="127.0.0.1" ) - assert admin_service.get_admins() == [Admin(username="example")] + assert await admin_service.get_admins() == [Admin(username="example")] diff --git a/tests/services/token_test.py b/tests/services/token_test.py index b3ea89707..b4f092274 100644 --- a/tests/services/token_test.py +++ b/tests/services/token_test.py @@ -67,7 +67,8 @@ async def test_session_token(setup: SetupTest) -> None: expires = data.created + timedelta(minutes=setup.config.issuer.exp_minutes) assert data.expires == expires - assert token_service.get_token_info_unchecked(token.key) == TokenInfo( + info = await token_service.get_token_info_unchecked(token.key) + assert info and info == TokenInfo( token=token.key, username=user_info.username, token_name=None, @@ -80,7 +81,7 @@ async def test_session_token(setup: SetupTest) -> None: ) assert await token_service.get_user_info(token) == user_info - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=token.key, username=data.username ) assert history.entries == [ @@ -103,7 +104,7 @@ async def test_session_token(setup: SetupTest) -> None: ) data = await token_service.get_data(token) assert data and data.scopes == ["exec:admin", "read:all"] - info = token_service.get_token_info_unchecked(token.key) + info = await token_service.get_token_info_unchecked(token.key) assert info and info.scopes == ["exec:admin", "read:all"] @@ -133,7 +134,7 @@ async def test_user_token(setup: SetupTest) -> None: ip_address="192.168.0.1", ) assert await token_service.get_user_info(user_token) == user_info - info = token_service.get_token_info_unchecked(user_token.key) + info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username=user_info.username, @@ -157,7 +158,7 @@ async def test_user_token(setup: SetupTest) -> None: uid=user_info.uid, ) - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=user_token.key, username=data.username ) assert history.entries == [ @@ -195,7 +196,7 @@ async def test_notebook_token(setup: SetupTest) -> None: token = await token_service.get_notebook_token(data, ip_address="1.0.0.1") assert await token_service.get_user_info(token) == user_info - info = token_service.get_token_info_unchecked(token.key) + info = await token_service.get_token_info_unchecked(token.key) assert info and info == TokenInfo( token=token.key, username=user_info.username, @@ -233,7 +234,7 @@ async def test_notebook_token(setup: SetupTest) -> None: ) assert token == new_token - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=token.key, username=data.username ) assert history.entries == [ @@ -271,10 +272,10 @@ async def test_notebook_token(setup: SetupTest) -> None: groups=data.groups, ) await token_service._token_redis_store.store_data(notebook_token_data) - with token_service._transaction_manager.transaction(): - token_service._token_db_store.add( - notebook_token_data, parent=data.token.key - ) + await token_service._token_db_store.add( + notebook_token_data, parent=data.token.key + ) + await setup.session.flush() token_service._token_cache.clear() dup_notebook_token = await token_service.get_notebook_token( data, ip_address="127.0.0.1" @@ -297,7 +298,7 @@ async def test_notebook_token(setup: SetupTest) -> None: data, ip_address="127.0.0.1" ) assert new_token != token - info = token_service.get_token_info_unchecked(new_token.key) + info = await token_service.get_token_info_unchecked(new_token.key) assert info expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) assert info.expires == expires @@ -327,7 +328,7 @@ async def test_internal_token(setup: SetupTest) -> None: ip_address="2001:db8::45", ) assert await token_service.get_user_info(internal_token) == user_info - info = token_service.get_token_info_unchecked(internal_token.key) + info = await token_service.get_token_info_unchecked(internal_token.key) assert info and info == TokenInfo( token=internal_token.key, username=user_info.username, @@ -370,7 +371,7 @@ async def test_internal_token(setup: SetupTest) -> None: ) assert internal_token == new_internal_token - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=internal_token.key, username=data.username ) assert history.entries == [ @@ -411,10 +412,10 @@ async def test_internal_token(setup: SetupTest) -> None: groups=data.groups, ) await token_service._token_redis_store.store_data(internal_token_data) - with token_service._transaction_manager.transaction(): - token_service._token_db_store.add( - internal_token_data, service="some-service", parent=data.token.key - ) + await token_service._token_db_store.add( + internal_token_data, service="some-service", parent=data.token.key + ) + await setup.session.flush() token_service._token_cache.clear() dup_internal_token = await token_service.get_internal_token( data, @@ -457,7 +458,7 @@ async def test_internal_token(setup: SetupTest) -> None: data, service="some-service", scopes=[], ip_address="127.0.0.1" ) assert new_internal_token != internal_token - info = token_service.get_token_info_unchecked(new_internal_token.key) + info = await token_service.get_token_info_unchecked(new_internal_token.key) assert info and info.scopes == [] expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) assert info.expires == expires @@ -629,7 +630,7 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: ) assert_is_now(user_data.created) - history = token_service.get_change_history( + history = await token_service.get_change_history( admin_data, token=token.key, username=request.username ) assert history.entries == [ @@ -649,7 +650,7 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: # Non-admins can't see other people's tokens. with pytest.raises(PermissionDeniedError): - token_service.get_change_history( + await token_service.get_change_history( data, token=token.key, username=request.username ) @@ -666,7 +667,7 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: ) assert_is_now(service_data.created) - history = token_service.get_change_history( + history = await token_service.get_change_history( admin_data, token=token.key, username=request.username ) assert history.entries == [ @@ -711,20 +712,24 @@ async def test_list(setup: SetupTest) -> None: admin_data = await token_service.get_data(other_session_token) assert admin_data - session_info = token_service.get_token_info_unchecked(session_token.key) + session_info = await token_service.get_token_info_unchecked( + session_token.key + ) assert session_info - user_token_info = token_service.get_token_info_unchecked(user_token.key) + user_token_info = await token_service.get_token_info_unchecked( + user_token.key + ) assert user_token_info - other_session_info = token_service.get_token_info_unchecked( + other_session_info = await token_service.get_token_info_unchecked( other_session_token.key ) assert other_session_info - assert token_service.list_tokens(data, "example") == sorted( + assert await token_service.list_tokens(data, "example") == sorted( sorted((session_info, user_token_info), key=lambda t: t.token), key=lambda t: t.created, reverse=True, ) - assert token_service.list_tokens(admin_data) == sorted( + assert await token_service.list_tokens(admin_data) == sorted( sorted( (session_info, other_session_info, user_token_info), key=lambda t: t.token, @@ -735,7 +740,7 @@ async def test_list(setup: SetupTest) -> None: # Regular users can't retrieve all tokens. with pytest.raises(PermissionDeniedError): - token_service.list_tokens(data) + await token_service.list_tokens(data) @pytest.mark.asyncio @@ -768,7 +773,7 @@ async def test_modify(setup: SetupTest) -> None: expires=expires, ip_address="192.168.0.4", ) - info = token_service.get_token_info_unchecked(user_token.key) + info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username="example", @@ -788,7 +793,7 @@ async def test_modify(setup: SetupTest) -> None: ip_address="127.0.4.5", ) - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=user_token.key, username=data.username ) assert history.entries == [ @@ -864,14 +869,14 @@ async def test_delete(setup: SetupTest) -> None: ) assert await token_service.get_data(token) is None - assert token_service.get_token_info_unchecked(token.key) is None + assert await token_service.get_token_info_unchecked(token.key) is None assert await token_service.get_user_info(token) is None assert not await token_service.delete_token( token.key, data, data.username, ip_address="127.0.0.1" ) - history = token_service.get_change_history( + history = await token_service.get_change_history( data, token=token.key, username=data.username ) assert history.entries == [ diff --git a/tests/settings_test.py b/tests/settings_test.py index ebf7c1e63..07c745914 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -99,5 +99,7 @@ async def test_database_password(tmp_path: Path) -> None: config = await config_dependency() del os.environ["GAFAELFAWR_DATABASE_PASSWORD"] - expected = "postgresql://gafaelfawr:some-password@localhost/gafaelfawr" + expected = ( + "postgresql+asyncpg://gafaelfawr:some-password@localhost/gafaelfawr" + ) assert config.database_url == expected diff --git a/tests/support/selenium.py b/tests/support/selenium.py index a90c74d31..232ef6466 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -11,6 +11,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from typing import TYPE_CHECKING +from urllib.parse import urlparse from seleniumwire import webdriver @@ -31,16 +32,16 @@ from urllib.parse import urlparse import structlog -from fastapi_sqlalchemy import db +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker -from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory from gafaelfawr.main import app from gafaelfawr.models.token import TokenUserInfo -from tests.support.tokens import add_expired_session_token from gafaelfawr.util import current_datetime +from tests.support.tokens import add_expired_session_token config_dependency.set_settings_path("{settings_path}") @@ -48,6 +49,7 @@ @app.on_event("startup") async def startup_event() -> None: config = await config_dependency() + is_postgres = config.database_url.startswith("postgresql") logger = structlog.get_logger(config.safir.logger_name) user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) scopes = list(config.known_scopes.keys()) @@ -59,32 +61,34 @@ async def startup_event() -> None: redis = await mockaioredis.create_redis_pool("") redis_dependency.set_redis(redis) - # Initialize the database. Non-SQLite databases need to be reset between - # tests. - should_reset = not urlparse(config.database_url).scheme == "sqlite" - initialize_database(config, reset=should_reset) - - with db(): - # Add an expired token so that we can test display of expired tokens. - await add_expired_session_token( - user_info, - scopes=scopes, - ip_address="127.0.0.1", - session=db.session, - ) - - # Add the valid session token. - factory = ComponentFactory( - config=config, - redis=await redis_dependency(config), - session=db.session, - http_client=MagicMock(), - logger=logger, - ) - token_service = factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=scopes, ip_address="127.0.0.1" - ) + engine = create_async_engine(config.database_url, future=True) + session_factory = sessionmaker( + engine, expire_on_commit=False, class_=AsyncSession + ) + async with session_factory() as session: + async with session.begin(): + # Add an expired token so that we can test display of expired + # tokens. + await add_expired_session_token( + user_info, + scopes=scopes, + ip_address="127.0.0.1", + session=session, + is_postgres=is_postgres, + ) + + # Add the valid session token. + factory = ComponentFactory( + config=config, + redis=await redis_dependency(config), + session=session, + http_client=MagicMock(), + logger=logger, + ) + token_service = factory.create_token_service() + token = await token_service.create_session_token( + user_info, scopes=scopes, ip_address="127.0.0.1" + ) with open("{token_path}", "w") as f: f.write(str(token)) @@ -190,7 +194,11 @@ async def run_app( """ config_dependency.set_settings_path(str(settings_path)) config = await config_dependency() - initialize_database(config) + + # Initialize the database. Non-SQLite databases need to be reset between + # tests. + should_reset = not urlparse(config.database_url).scheme == "sqlite" + await initialize_database(config, reset=should_reset) token_path = tmp_path / "token" app_source = APP_TEMPLATE.format( diff --git a/tests/support/setup.py b/tests/support/setup.py index 2c3d7a8f6..21457278d 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -12,8 +12,8 @@ from asgi_lifespan import LifespanManager from httpx import AsyncClient from safir.dependencies.http_client import http_client_dependency -from sqlalchemy import create_engine -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker from gafaelfawr.constants import COOKIE_NAME from gafaelfawr.database import initialize_database @@ -23,7 +23,6 @@ from gafaelfawr.main import app from gafaelfawr.models.state import State from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo -from gafaelfawr.storage.transaction import TransactionManager from tests.support.constants import TEST_HOSTNAME from tests.support.github import mock_github from tests.support.oidc import ( @@ -43,10 +42,9 @@ from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken, OIDCVerifiedToken from gafaelfawr.providers.github import GitHubUserInfo - from gafaelfawr.storage.transaction import Transaction -def initialize(tmp_path: Path) -> Config: +async def initialize(tmp_path: Path) -> Config: """Do basic initialization and return a configuration. This shared logic can be used either with `SetupTest`, which assumes an @@ -75,7 +73,7 @@ def initialize(tmp_path: Path) -> Config: # Initialize the database. Non-SQLite databases need to be reset between # tests. should_reset = not urlparse(config.database_url).scheme == "sqlite" - initialize_database(config, reset=should_reset) + await initialize_database(config, reset=should_reset) return config @@ -116,7 +114,7 @@ async def create( respx_mock : `respx.Router` The mock for simulating `httpx.AsyncClient` calls. """ - config = initialize(tmp_path) + config = await initialize(tmp_path) if not os.environ.get("REDIS_6379_TCP_PORT"): import mockaioredis @@ -127,11 +125,10 @@ async def create( # Create the database session that will be used by SetupTest and by # the factory it contains. The application will use a separate # session handled by its middleware. - connect_args = {} - if urlparse(config.database_url).scheme == "sqlite": - connect_args = {"check_same_thread": False} - engine = create_engine(config.database_url, connect_args=connect_args) - session = Session(bind=engine) + engine = create_async_engine(config.database_url, future=True) + session_factory = sessionmaker( + engine, expire_on_commit=False, class_=AsyncSession + ) # Build the SetupTest object inside all of the contexts required by # its components and handle clean shutdown. We have to build two @@ -145,15 +142,16 @@ async def create( base_url = f"https://{TEST_HOSTNAME}" async with AsyncClient(app=app, base_url=base_url) as client: async with AsyncClient() as http_client: - yield cls( - tmp_path=tmp_path, - respx_mock=respx_mock, - config=config, - redis=redis, - session=session, - client=client, - http_client=http_client, - ) + async with session_factory() as session: + yield cls( + tmp_path=tmp_path, + respx_mock=respx_mock, + config=config, + redis=redis, + session=session, + client=client, + http_client=http_client, + ) finally: await http_client_dependency.aclose() if os.environ.get("REDIS_6379_TCP_PORT"): @@ -162,7 +160,7 @@ async def create( redis = await redis_dependency() redis.close() await redis.wait_closed() - session.close() + await engine.dispose() def __init__( self, @@ -171,7 +169,7 @@ def __init__( respx_mock: respx.Router, config: Config, redis: Redis, - session: Session, + session: AsyncSession, client: AsyncClient, http_client: AsyncClient, ) -> None: @@ -279,6 +277,7 @@ async def create_session_token( ) data = await token_service.get_data(token) assert data + await self.session.commit() return data def create_upstream_oidc_token( @@ -403,14 +402,3 @@ def set_oidc_token_response( The token. """ mock_oidc_provider_token(self.respx_mock, self.config, code, token) - - def transaction(self) -> Transaction: - """Run code within an open database transaction. - - Returns - ------- - gafaelfawr.storage.transaction.Transaction - A context manager that will automatically commit changes to - the underlying database. - """ - return TransactionManager(self.session).transaction() diff --git a/tests/support/tokens.py b/tests/support/tokens.py index 62edc463f..9777954eb 100644 --- a/tests/support/tokens.py +++ b/tests/support/tokens.py @@ -13,13 +13,12 @@ from gafaelfawr.models.token import Token, TokenData, TokenType, TokenUserInfo from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore -from gafaelfawr.storage.transaction import TransactionManager from gafaelfawr.util import current_datetime if TYPE_CHECKING: from typing import Any, Dict, List, Optional - from sqlalchemy.orm import Session + from sqlalchemy.ext.asyncio import AsyncSession from gafaelfawr.config import Config @@ -31,7 +30,8 @@ async def add_expired_session_token( *, scopes: List[str], ip_address: str, - session: Session, + session: AsyncSession, + is_postgres: bool, ) -> None: """Add an expired session token to the database. @@ -51,12 +51,13 @@ async def add_expired_session_token( The scopes of the token. ip_address : `str` The IP address from which the request came. - session : `sqlalchemy.orm.Session` + session : `sqlalchemy.ext.asyncio.AsyncSession` The database session. + is_postgres : `bool` + Whether the underlying database is PostgreSQL. """ token_db_store = TokenDatabaseStore(session) - token_change_store = TokenChangeHistoryStore(session) - transaction_manager = TransactionManager(session) + token_change_store = TokenChangeHistoryStore(session, is_postgres) token = Token() created = current_datetime() @@ -81,9 +82,8 @@ async def add_expired_session_token( event_time=created, ) - with transaction_manager.transaction(): - token_db_store.add(data) - token_change_store.add(history_entry) + await token_db_store.add(data) + await token_change_store.add(history_entry) def create_test_token( From bf3b877e85ee1dea898f766d3f7f08223238b029 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Wed, 17 Nov 2021 18:05:25 -0800 Subject: [PATCH 02/24] Consolidate some validators Take advantage of the ability to apply a validator to multiple fields to remove some duplicate code. --- src/gafaelfawr/models/history.py | 19 +++++-------------- src/gafaelfawr/models/token.py | 12 +++--------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/gafaelfawr/models/history.py b/src/gafaelfawr/models/history.py index d41904f81..f59cdd86a 100644 --- a/src/gafaelfawr/models/history.py +++ b/src/gafaelfawr/models/history.py @@ -314,24 +314,15 @@ class Config: json_encoders = {datetime: lambda v: int(v.timestamp())} orm_mode = True - _normalize_scopes = validator("scopes", allow_reuse=True, pre=True)( - normalize_scopes - ) - _normalize_expires = validator("expires", allow_reuse=True, pre=True)( - normalize_datetime - ) - _normalize_old_expires = validator( - "old_expires", allow_reuse=True, pre=True - )(normalize_datetime) - _normalize_old_scopes = validator( - "old_scopes", allow_reuse=True, pre=True + _normalize_scopes = validator( + "scopes", "old_scopes", allow_reuse=True, pre=True )(normalize_scopes) + _normalize_expires = validator( + "expires", "old_expires", "event_time", allow_reuse=True, pre=True + )(normalize_datetime) _normalize_ip_address = validator( "ip_address", allow_reuse=True, pre=True )(normalize_ip_address) - _normalize_event_time = validator( - "event_time", allow_reuse=True, pre=True - )(normalize_datetime) def reduced_dict(self) -> Dict[str, Any]: """Custom ``dict`` method to suppress some fields. diff --git a/src/gafaelfawr/models/token.py b/src/gafaelfawr/models/token.py index c37a6c876..2f524538a 100644 --- a/src/gafaelfawr/models/token.py +++ b/src/gafaelfawr/models/token.py @@ -222,15 +222,9 @@ class Config: orm_mode = True json_encoders = {datetime: lambda v: int(v.timestamp())} - _normalize_created = validator("created", allow_reuse=True, pre=True)( - normalize_datetime - ) - _normalize_last_used = validator("last_used", allow_reuse=True, pre=True)( - normalize_datetime - ) - _normalize_expires = validator("expires", allow_reuse=True, pre=True)( - normalize_datetime - ) + _normalize_created = validator( + "created", "last_used", "expires", allow_reuse=True, pre=True + )(normalize_datetime) class TokenUserInfo(BaseModel): From 5cdaf6fbf60baceb53181a3dd17c3a9351b4f8c2 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 18 Nov 2021 13:53:48 -0800 Subject: [PATCH 03/24] Move Selenium code out of string template There is a mechanism in uvicorn to start an app using a factory function, so all of the Selenium code can be moved out of a string template and put directly in tests.support.selenium, where it can be linted and type-checked. Use an environment variable instead of templating to communicate the path to which to write the auth token to the Selenium testing app. --- tests/support/selenium.py | 174 +++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 79 deletions(-) diff --git a/tests/support/selenium.py b/tests/support/selenium.py index 232ef6466..58928fce0 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -11,88 +11,28 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from typing import TYPE_CHECKING -from urllib.parse import urlparse - -from seleniumwire import webdriver - -from gafaelfawr.database import initialize_database -from gafaelfawr.dependencies.config import config_dependency -from gafaelfawr.models.token import Token - -if TYPE_CHECKING: - from pathlib import Path - from typing import AsyncIterator - - from gafelfawr.config import Config - -APP_TEMPLATE = """ -import os -from datetime import timedelta from unittest.mock import MagicMock from urllib.parse import urlparse import structlog +from fastapi import FastAPI +from seleniumwire import webdriver from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker +from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory from gafaelfawr.main import app -from gafaelfawr.models.token import TokenUserInfo -from gafaelfawr.util import current_datetime +from gafaelfawr.models.token import Token, TokenUserInfo from tests.support.tokens import add_expired_session_token -config_dependency.set_settings_path("{settings_path}") - - -@app.on_event("startup") -async def startup_event() -> None: - config = await config_dependency() - is_postgres = config.database_url.startswith("postgresql") - logger = structlog.get_logger(config.safir.logger_name) - user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) - scopes = list(config.known_scopes.keys()) - - # Mock out Redis if there is none running. - if not os.environ.get("REDIS_6379_TCP_PORT"): - import mockaioredis - - redis = await mockaioredis.create_redis_pool("") - redis_dependency.set_redis(redis) - - engine = create_async_engine(config.database_url, future=True) - session_factory = sessionmaker( - engine, expire_on_commit=False, class_=AsyncSession - ) - async with session_factory() as session: - async with session.begin(): - # Add an expired token so that we can test display of expired - # tokens. - await add_expired_session_token( - user_info, - scopes=scopes, - ip_address="127.0.0.1", - session=session, - is_postgres=is_postgres, - ) - - # Add the valid session token. - factory = ComponentFactory( - config=config, - redis=await redis_dependency(config), - session=session, - http_client=MagicMock(), - logger=logger, - ) - token_service = factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=scopes, ip_address="127.0.0.1" - ) +if TYPE_CHECKING: + from pathlib import Path + from typing import AsyncIterator - with open("{token_path}", "w") as f: - f.write(str(token)) -""" + from gafelfawr.config import Config @dataclass @@ -179,6 +119,77 @@ def _wait_for_server(port: int, timeout: float = 5.0) -> None: time.sleep(0.1) +async def _selenium_startup(token_path: str) -> None: + """Startup hook for the app run in Selenium testing mode.""" + config = await config_dependency() + is_postgres = config.database_url.startswith("postgresql") + logger = structlog.get_logger(config.safir.logger_name) + user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) + scopes = list(config.known_scopes.keys()) + + # Mock out Redis if there is none running. + if not os.environ.get("REDIS_6379_TCP_PORT"): + import mockaioredis + + redis = await mockaioredis.create_redis_pool("") + redis_dependency.set_redis(redis) + + engine = create_async_engine(config.database_url, future=True) + session_factory = sessionmaker( + engine, expire_on_commit=False, class_=AsyncSession + ) + async with session_factory() as session: + async with session.begin(): + # Add an expired token so that we can test display of expired + # tokens. + await add_expired_session_token( + user_info, + scopes=scopes, + ip_address="127.0.0.1", + session=session, + is_postgres=is_postgres, + ) + + # Add the valid session token. + factory = ComponentFactory( + config=config, + redis=await redis_dependency(config), + session=session, + http_client=MagicMock(), + logger=logger, + ) + token_service = factory.create_token_service() + token = await token_service.create_session_token( + user_info, scopes=scopes, ip_address="127.0.0.1" + ) + + with open(token_path, "w") as f: + f.write(str(token)) + + +def create_app() -> FastAPI: + """Create the FastAPI app that Selenium should run. + + This is the same as the main Gafaelfawr app but with an additional startup + handler that initializes some tokens in Redis. This setup must be done + inside the spawned app in case the Redis in question is a memory-only mock + Redis. + + Notes + ----- + This function modifies the main Gafaelfawr app in place, so it must only + be called by uvicorn in the separate process spawned by run_app. If it is + run in the main pytest process, it will break other tests. + """ + token_path = os.environ["GAFAELFAWR_TEST_TOKEN_PATH"] + + @app.on_event("startup") + async def selenium_startup_event() -> None: + await _selenium_startup(token_path) + + return app + + @asynccontextmanager async def run_app( tmp_path: Path, settings_path: Path @@ -194,32 +205,37 @@ async def run_app( """ config_dependency.set_settings_path(str(settings_path)) config = await config_dependency() + token_path = tmp_path / "token" # Initialize the database. Non-SQLite databases need to be reset between # tests. should_reset = not urlparse(config.database_url).scheme == "sqlite" await initialize_database(config, reset=should_reset) - token_path = tmp_path / "token" - app_source = APP_TEMPLATE.format( - settings_path=str(settings_path), - token_path=str(token_path), - ) - app_path = tmp_path / "testing.py" - with app_path.open("w") as f: - f.write(app_source) - + # Create the socket that the app will listen on. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("127.0.0.1", 0)) port = s.getsockname()[1] - cmd = ["uvicorn", "--fd", "0", "testing:app"] + # Spawn the app in a separate process using uvicorn. + cmd = [ + "uvicorn", + "--fd", + "0", + "--factory", + "tests.support.selenium:create_app", + ] logging.info("Starting server with command %s", " ".join(cmd)) p = subprocess.Popen( cmd, cwd=str(tmp_path), stdin=s.fileno(), - env={**os.environ, "PYTHONPATH": os.getcwd()}, + env={ + **os.environ, + "GAFAELFAWR_SETTINGS_PATH": str(settings_path), + "GAFAELFAWR_TEST_TOKEN_PATH": str(token_path), + "PYTHONPATH": os.getcwd(), + }, ) s.close() From 39877f77ed010c9ba8295d2bef5657bda3c7148a Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 18 Nov 2021 14:59:53 -0800 Subject: [PATCH 04/24] Fix database session management for Kubernetes In the Kubernetes service, manage transactions directly. Stop creating a transaction by default in the standalone ComponentFactory since that's not useful for long-running Kubernetes resource management. --- src/gafaelfawr/factory.py | 22 +++++++++++----------- src/gafaelfawr/services/kubernetes.py | 27 ++++++++++++++++++++++++--- tests/services/kubernetes_test.py | 2 ++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index 350914c95..13500280e 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -85,19 +85,18 @@ async def standalone(cls) -> AsyncIterator[ComponentFactory]: logger.debug("Connecting to PostgreSQL") engine = create_async_engine(config.database_url, future=True) try: - factory = sessionmaker( + session_factory = sessionmaker( engine, expire_on_commit=False, class_=AsyncSession ) - async with factory() as session: - async with session.begin(): - async with AsyncClient() as client: - yield cls( - config=config, - redis=redis, - session=session, - http_client=client, - logger=logger, - ) + async with session_factory() as session: + async with AsyncClient() as client: + yield cls( + config=config, + redis=redis, + session=session, + http_client=client, + logger=logger, + ) finally: await redis_dependency.close() await engine.dispose() @@ -136,6 +135,7 @@ def create_kubernetes_service(self) -> KubernetesService: return KubernetesService( token_service=token_service, storage=storage, + session=self._session, logger=self._logger, ) diff --git a/src/gafaelfawr/services/kubernetes.py b/src/gafaelfawr/services/kubernetes.py index f43b20bed..5e5b3550f 100644 --- a/src/gafaelfawr/services/kubernetes.py +++ b/src/gafaelfawr/services/kubernetes.py @@ -28,6 +28,7 @@ from typing import Dict, Optional from kubernetes.client import V1Secret + from sqlalchemy.ext.asyncio import AsyncSession from structlog.stdlib import BoundLogger from gafaelfawr.services.token import TokenService @@ -77,16 +78,35 @@ class KubernetesService: found during that initial pass whose secrets are correctly up to date. We will then skip the corresponding events when we receive them when starting up the watcher, although will redo the metadata checks. + + This service unfortunately has to be aware of the database session since + it has to manage transactions around token issuance. The token service is + transaction-unaware because it normally runs in the context of a request + handler, where we implement one transaction per request. + + Parameters + ---------- + token_service : `gafaelfawr.services.token.TokenService` + Token management service. + storage : `gafaelfawr.storage.kubernetes.KubernetesStorage` + Storage layer for the Kubernetes cluster. + session : `sqlalchemy.ext.asyncio.AsyncSession` + Database session, used for transaction management. + logger : `structlog.stdlib.BoundLogger` + Logger to report issues. """ def __init__( self, + *, token_service: TokenService, storage: KubernetesStorage, + session: AsyncSession, logger: BoundLogger, ) -> None: self._token_service = token_service self._storage = storage + self._session = session self._logger = logger self._last_generation: Dict[str, int] = {} @@ -205,9 +225,10 @@ async def _create_service_token( token_type=TokenType.service, scopes=parent.scopes, ) - return await self._token_service.create_token_from_admin_request( - request, TokenData.internal_token(), ip_address=None - ) + async with self._session.begin(): + return await self._token_service.create_token_from_admin_request( + request, TokenData.internal_token(), ip_address=None + ) async def _secret_needs_update( self, parent: GafaelfawrServiceToken, secret: Optional[V1Secret] diff --git a/tests/services/kubernetes_test.py b/tests/services/kubernetes_test.py index ecf8dfda0..553586ec1 100644 --- a/tests/services/kubernetes_test.py +++ b/tests/services/kubernetes_test.py @@ -360,6 +360,7 @@ async def test_modify( type="Opaque", ) mock_kubernetes.replace_namespaced_secret("gafaelfawr", "nublado2", secret) + await setup.session.commit() # Update the secrets. This should create new tokens for both. await kubernetes_service.update_service_tokens() @@ -381,6 +382,7 @@ async def test_modify( # Update the secrets. This should create a new token for the first secret # but not for the second. + await setup.session.commit() await kubernetes_service.update_service_tokens() await assert_kubernetes_secrets_are_correct( setup, mock_kubernetes, is_fresh=False From 2f458e2dcfbcca532295c20aa3b391de73481d87 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 18 Nov 2021 17:06:00 -0800 Subject: [PATCH 05/24] Remove stray pytest-httpx configuration --- tests/conftest.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 063303154..30c57e2bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,6 @@ from gafaelfawr.models.state import State from gafaelfawr.models.token import TokenType from tests.pages.tokens import TokensPage -from tests.support.constants import TEST_HOSTNAME from tests.support.kubernetes import MockKubernetesApi from tests.support.selenium import run_app, selenium_driver from tests.support.settings import build_settings @@ -23,7 +22,7 @@ if TYPE_CHECKING: from pathlib import Path - from typing import AsyncIterator, Iterator, List + from typing import AsyncIterator, Iterator from seleniumwire import webdriver @@ -68,12 +67,6 @@ def mock_kubernetes() -> Iterator[MockKubernetesApi]: patcher.stop() -@pytest.fixture -def non_mocked_hosts() -> List[str]: - """Disable pytest-httpx mocking for the test application.""" - return [TEST_HOSTNAME, "localhost"] - - @pytest.fixture async def selenium_config( tmp_path: Path, driver: webdriver.Chrome From f531d9c95af3c2372d36c069d5920fb42f3cac3b Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 18 Nov 2021 18:01:37 -0800 Subject: [PATCH 06/24] Move app and client fixtures Start unwinding SetupTest by moving the app and client fixtures out of it and into their own fixtures. Update invocations accordingly. --- tests/conftest.py | 41 ++++- tests/handlers/analyze_test.py | 26 ++-- tests/handlers/api_admins_test.py | 56 +++---- tests/handlers/api_history_test.py | 98 ++++++------ tests/handlers/api_login_test.py | 20 +-- tests/handlers/api_tokens_test.py | 223 ++++++++++++++-------------- tests/handlers/auth_logging_test.py | 29 ++-- tests/handlers/auth_test.py | 104 +++++++------ tests/handlers/index_test.py | 6 +- tests/handlers/influxdb_test.py | 19 ++- tests/handlers/login_github_test.py | 97 +++++++----- tests/handlers/login_oidc_test.py | 105 +++++++------ tests/handlers/logout_test.py | 41 ++--- tests/handlers/oidc_test.py | 85 ++++++----- tests/support/setup.py | 48 +++--- 15 files changed, 542 insertions(+), 456 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 30c57e2bf..c2bea2ea3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,17 +4,22 @@ from typing import TYPE_CHECKING from unittest.mock import patch -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse import kubernetes import pytest import respx +from asgi_lifespan import LifespanManager +from httpx import AsyncClient +from gafaelfawr import main from gafaelfawr.constants import COOKIE_NAME +from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.models.state import State from gafaelfawr.models.token import TokenType from tests.pages.tokens import TokensPage +from tests.support.constants import TEST_HOSTNAME from tests.support.kubernetes import MockKubernetesApi from tests.support.selenium import run_app, selenium_driver from tests.support.settings import build_settings @@ -24,11 +29,45 @@ from pathlib import Path from typing import AsyncIterator, Iterator + from fastapi import FastAPI from seleniumwire import webdriver from tests.support.selenium import SeleniumConfig +@pytest.fixture +async def app(tmp_path: Path) -> AsyncIterator[FastAPI]: + """Return a configured test application. + + Wraps the application in a lifespan manager so that startup and shutdown + events are sent during test execution. Initializes the database before + creating the app to ensure that data is dropped from a persistent database + between test cases. + + Notes + ----- + This always uses a settings file configured for GitHub authentication for + the database initialization and initial app configuration, since it + shouldn't matter. Use ``setup.configure()`` after the test has started to + change this if needed for a given test. + """ + settings_path = build_settings(tmp_path, "github") + config_dependency.set_settings_path(str(settings_path)) + config = await config_dependency() + should_reset = not urlparse(config.database_url).scheme == "sqlite" + await initialize_database(config, reset=should_reset) + async with LifespanManager(main.app): + yield main.app + + +@pytest.fixture +async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: + """Return an ``httpx.AsyncClient`` configured to talk to the test app.""" + base_url = f"https://{TEST_HOSTNAME}" + async with AsyncClient(app=app, base_url=base_url) as client: + yield client + + @pytest.fixture(scope="session") def driver() -> Iterator[webdriver.Chrome]: """Create a driver for Selenium testing. diff --git a/tests/handlers/analyze_test.py b/tests/handlers/analyze_test.py index af7fe877c..1ff146fa3 100644 --- a/tests/handlers/analyze_test.py +++ b/tests/handlers/analyze_test.py @@ -12,12 +12,14 @@ from tests.support.headers import query_from_url if TYPE_CHECKING: + from httpx import AsyncClient + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_analyze_no_auth(setup: SetupTest) -> None: - r = await setup.client.get("/auth/analyze") +async def test_analyze_no_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth/analyze") assert r.status_code == 307 url = urlparse(r.headers["Location"]) assert not url.scheme @@ -29,15 +31,15 @@ async def test_analyze_no_auth(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_analyze_session(setup: SetupTest) -> None: +async def test_analyze_session(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( group_names=["foo", "bar"], scopes=["read:all"] ) assert token_data.expires assert token_data.groups - await setup.login(token_data.token) + await setup.login(client, token_data.token) - r = await setup.client.get("/auth/analyze") + r = await client.get("/auth/analyze") assert r.status_code == 200 # Check that the result is formatted for humans. @@ -62,18 +64,18 @@ async def test_analyze_session(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_token(setup: SetupTest) -> None: - r = await setup.client.post("/auth/analyze", data={"token": "some-token"}) +async def test_invalid_token(client: AsyncClient, setup: SetupTest) -> None: + r = await client.post("/auth/analyze", data={"token": "some-token"}) assert r.status_code == 200 assert r.json() == {"token": {"errors": [ANY], "valid": False}} @pytest.mark.asyncio -async def test_analyze_token(setup: SetupTest) -> None: +async def test_analyze_token(client: AsyncClient, setup: SetupTest) -> None: token = Token() # Handle with no session. - r = await setup.client.post("/auth/analyze", data={"token": str(token)}) + r = await client.post("/auth/analyze", data={"token": str(token)}) assert r.status_code == 200 assert r.json() == { "handle": token.dict(), @@ -87,7 +89,7 @@ async def test_analyze_token(setup: SetupTest) -> None: assert token_data.expires assert token_data.groups token = token_data.token - r = await setup.client.post("/auth/analyze", data={"token": str(token)}) + r = await client.post("/auth/analyze", data={"token": str(token)}) # Check that the results from /analyze include the token components and # the token information. @@ -126,9 +128,7 @@ async def test_analyze_token(setup: SetupTest) -> None: assert user_token_data # Check that the correct fields are omitted and nothing odd happens. - r = await setup.client.post( - "/auth/analyze", data={"token": str(user_token)} - ) + r = await client.post("/auth/analyze", data={"token": str(user_token)}) assert r.status_code == 200 assert r.json() == { "handle": user_token.dict(), diff --git a/tests/handlers/api_admins_test.py b/tests/handlers/api_admins_test.py index 8308fa1bd..b1e924dfc 100644 --- a/tests/handlers/api_admins_test.py +++ b/tests/handlers/api_admins_test.py @@ -7,16 +7,18 @@ import pytest if TYPE_CHECKING: + from httpx import AsyncClient + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_admins(setup: SetupTest) -> None: - r = await setup.client.get("/auth/api/v1/admins") +async def test_admins(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth/api/v1/admins") assert r.status_code == 401 token_data = await setup.create_session_token() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -27,7 +29,7 @@ async def test_admins(setup: SetupTest) -> None: } token_data = await setup.create_session_token(scopes=["admin:token"]) - r = await setup.client.get( + r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -40,7 +42,7 @@ async def test_admins(setup: SetupTest) -> None: ) await setup.session.commit() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -49,17 +51,17 @@ async def test_admins(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_add_delete(setup: SetupTest) -> None: - r = await setup.client.post( +async def test_add_delete(client: AsyncClient, setup: SetupTest) -> None: + r = await client.post( "/auth/api/v1/admins", json={"username": "some-user"} ) assert r.status_code == 401 - r = await setup.client.delete("/auth/api/v1/admins/admin") + r = await client.delete("/auth/api/v1/admins/admin") assert r.status_code == 401 token_data = await setup.create_session_token(username="admin") - csrf = await setup.login(token_data.token) - r = await setup.client.post( + csrf = await setup.login(client, token_data.token) + r = await client.post( "/auth/api/v1/admins", headers={"X-CSRF-Token": csrf}, json={"username": "new-admin"}, @@ -69,7 +71,7 @@ async def test_add_delete(setup: SetupTest) -> None: "msg": "Token does not have required scope admin:token", "type": "permission_denied", } - r = await setup.client.delete( + r = await client.delete( "/auth/api/v1/admins/admin", headers={"X-CSRF-Token": csrf} ) assert r.status_code == 403 @@ -81,35 +83,35 @@ async def test_add_delete(setup: SetupTest) -> None: token_data = await setup.create_session_token( username="admin", scopes=["admin:token"] ) - csrf = await setup.login(token_data.token) - r = await setup.client.post( + csrf = await setup.login(client, token_data.token) + r = await client.post( "/auth/api/v1/admins", json={"username": "new-admin"} ) assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "invalid_csrf" - r = await setup.client.post( + r = await client.post( "/auth/api/v1/admins", headers={"X-CSRF-Token": csrf}, json={"username": "new-admin"}, ) assert r.status_code == 204 - r = await setup.client.get("/auth/api/v1/admins") + r = await client.get("/auth/api/v1/admins") assert r.status_code == 200 assert r.json() == [{"username": "admin"}, {"username": "new-admin"}] - r = await setup.client.delete("/auth/api/v1/admins/admin") + r = await client.delete("/auth/api/v1/admins/admin") assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "invalid_csrf" - r = await setup.client.delete( + r = await client.delete( "/auth/api/v1/admins/admin", headers={"X-CSRF-Token": csrf} ) assert r.status_code == 204 - r = await setup.client.get("/auth/api/v1/admins") + r = await client.get("/auth/api/v1/admins") assert r.json() == [{"username": "new-admin"}] # We can still retrieve the list because we have a token with scope # admin:token, but since we (admin) were removed as an admin, we should no # longer be able to add new admins. - r = await setup.client.post( + r = await client.post( "/auth/api/v1/admins", headers={"X-CSRF-Token": csrf}, json={"username": "another-admin"}, @@ -118,31 +120,29 @@ async def test_add_delete(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bootstrap(setup: SetupTest) -> None: +async def test_bootstrap(client: AsyncClient, setup: SetupTest) -> None: token = str(setup.config.bootstrap_token) - r = await setup.client.post( + r = await client.post( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token}"}, json={"username": "example"}, ) assert r.status_code == 204 - r = await setup.client.get( - "/auth/api/v1/admins", - headers={"Authorization": f"bearer {token}"}, + r = await client.get( + "/auth/api/v1/admins", headers={"Authorization": f"bearer {token}"} ) assert r.status_code == 200 assert r.json() == [{"username": "admin"}, {"username": "example"}] - r = await setup.client.delete( + r = await client.delete( "/auth/api/v1/admins/admin", headers={"Authorization": f"bearer {token}"}, ) assert r.status_code == 204 - r = await setup.client.get( - "/auth/api/v1/admins", - headers={"Authorization": f"bearer {token}"}, + r = await client.get( + "/auth/api/v1/admins", headers={"Authorization": f"bearer {token}"} ) assert r.status_code == 200 assert r.json() == [{"username": "example"}] diff --git a/tests/handlers/api_history_test.py b/tests/handlers/api_history_test.py index ac5c2f485..729eaa36c 100644 --- a/tests/handlers/api_history_test.py +++ b/tests/handlers/api_history_test.py @@ -26,6 +26,8 @@ if TYPE_CHECKING: from typing import Any, Callable, Dict, List, Optional, Union + from httpx import AsyncClient + from tests.support.setup import SetupTest @@ -166,7 +168,7 @@ def entry_to_dict(entry: TokenChangeHistoryEntry) -> Dict[str, Any]: async def check_history_request( - setup: SetupTest, + client: AsyncClient, query: Dict[str, Union[str, int]], history: List[TokenChangeHistoryEntry], selector: Callable[[TokenChangeHistoryEntry], bool], @@ -179,7 +181,7 @@ async def check_history_request( url = f"/auth/api/v1/users/{username}/token-change-history?{encoded}" else: url = f"/auth/api/v1/history/token-changes?{encoded}" - r = await setup.client.get(url) + r = await client.get(url) filtered = [entry_to_dict(e) for e in history if selector(e)] if username and len(filtered) == 0: assert r.status_code == 404 @@ -191,7 +193,7 @@ async def check_history_request( async def check_pagination( - setup: SetupTest, + client: AsyncClient, history: List[TokenChangeHistoryEntry], *, username: Optional[str] = None, @@ -213,7 +215,7 @@ async def check_pagination( prev_data = None all_data = [] for end in range(5, len(history) + 4, 5): - r = await setup.client.get(url) + r = await client.get(url) assert r.status_code == 200 data = r.json() assert data == [entry_to_dict(e) for e in history[end - 5 : end]] @@ -227,7 +229,7 @@ async def check_pagination( assert not link_data.prev_url else: assert link_data.prev_url - r = await setup.client.get(link_data.prev_url) + r = await client.get(link_data.prev_url) assert r.status_code == 200 assert r.json() == prev_data assert r.headers["X-Total-Count"] == str(len(history)) @@ -253,7 +255,7 @@ async def check_pagination( # URLs. backwards_data = data while link_data.prev_url: - r = await setup.client.get(link_data.prev_url) + r = await client.get(link_data.prev_url) assert r.status_code == 200 assert r.headers["X-Total-Count"] == str(len(history)) backwards_data = r.json() + backwards_data @@ -262,48 +264,50 @@ async def check_pagination( @pytest.mark.asyncio -async def test_admin_change_history(setup: SetupTest) -> None: +async def test_admin_change_history( + client: AsyncClient, setup: SetupTest +) -> None: token_data = await setup.create_session_token(scopes=["admin:token"]) - await setup.login(token_data.token) + await setup.login(client, token_data.token) history = await build_history(setup) - r = await setup.client.get("/auth/api/v1/history/token-changes") + r = await client.get("/auth/api/v1/history/token-changes") assert r.status_code == 200 assert r.json() == [entry_to_dict(e) for e in history] assert "Link" not in r.headers assert "X-Total-Count" not in r.headers # Check making paginated requests. - await check_pagination(setup, history) + await check_pagination(client, history) # Try a few different types of filtering. await check_history_request( - setup, + client, {"username": history[1].username}, history, lambda e: e.username == history[1].username, ) await check_history_request( - setup, + client, {"actor": ""}, history, lambda e: e.actor == "", ) token = history[1].token await check_history_request( - setup, + client, {"key": token}, history, lambda e: e.token == token or e.parent == token, ) await check_history_request( - setup, + client, {"token_type": "internal"}, history, lambda e: e.token_type == TokenType.internal, ) await check_history_request( - setup, + client, {"token_type": "internal", "key": token}, history, lambda e: ( @@ -312,14 +316,14 @@ async def test_admin_change_history(setup: SetupTest) -> None: ), ) await check_history_request( - setup, + client, {"ip_address": "192.0.2.20"}, history, lambda e: e.ip_address == "192.0.2.20", ) cidr_block = ip_network("192.0.2.0/24") await check_history_request( - setup, + client, {"ip_address": "192.0.2.0/24"}, history, lambda e: ip_address(e.ip_address) in cidr_block, @@ -330,32 +334,32 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: return ip_address(e.ip_address) == expected await check_history_request( - setup, + client, {"ip_address": "2001:db8:034a:ea78:4278:4562:6578:9876"}, history, ipv6_filter, ) cidr_block = ip_network("2001:db8::/32") await check_history_request( - setup, + client, {"ip_address": "2001:db8::/32"}, history, lambda e: ip_address(e.ip_address) in cidr_block, ) await check_history_request( - setup, + client, {"since": int(history[5].event_time.timestamp())}, history[:6], lambda e: True, ) await check_history_request( - setup, + client, {"until": int(history[8].event_time.timestamp())}, history[8:], lambda e: True, ) await check_history_request( - setup, + client, { "since": int(history[9].event_time.timestamp()), "until": int(history[4].event_time.timestamp()), @@ -366,38 +370,40 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: @pytest.mark.asyncio -async def test_user_change_history(setup: SetupTest) -> None: +async def test_user_change_history( + client: AsyncClient, setup: SetupTest +) -> None: token_data = await setup.create_session_token(username="one") - await setup.login(token_data.token) + await setup.login(client, token_data.token) history = [e for e in await build_history(setup) if e.username == "one"] - r = await setup.client.get("/auth/api/v1/users/one/token-change-history") + r = await client.get("/auth/api/v1/users/one/token-change-history") assert r.status_code == 200 assert r.json() == [entry_to_dict(e) for e in history] assert "Link" not in r.headers assert "X-Total-Count" not in r.headers # Check making paginated requests. - await check_pagination(setup, history, username="one") + await check_pagination(client, history, username="one") # Try a few different types of filtering. token = history[1].token await check_history_request( - setup, + client, {"key": token}, history, lambda e: e.token == token or e.parent == token, username="one", ) await check_history_request( - setup, + client, {"token_type": "internal"}, history, lambda e: e.token_type == TokenType.internal, username="one", ) await check_history_request( - setup, + client, {"token_type": "internal", "key": token}, history, lambda e: ( @@ -407,7 +413,7 @@ async def test_user_change_history(setup: SetupTest) -> None: username="one", ) await check_history_request( - setup, + client, {"ip_address": "192.0.2.3"}, history, lambda e: e.ip_address == "192.0.2.3", @@ -419,7 +425,7 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: return ip_address(e.ip_address) == expected await check_history_request( - setup, + client, {"ip_address": "2001:db8:034a:ea78:4278:4562:6578:9876"}, history, ipv6_filter, @@ -427,28 +433,28 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: ) cidr_block = ip_network("192.0.2.0/24") await check_history_request( - setup, + client, {"ip_address": "192.0.2.0/24"}, history, lambda e: ip_address(e.ip_address) in cidr_block, username="one", ) await check_history_request( - setup, + client, {"since": int(history[3].event_time.timestamp())}, history[:4], lambda e: True, username="one", ) await check_history_request( - setup, + client, {"until": int(history[2].event_time.timestamp())}, history[2:], lambda e: True, username="one", ) await check_history_request( - setup, + client, { "since": int(history[5].event_time.timestamp()), "until": int(history[4].event_time.timestamp()), @@ -460,36 +466,34 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: @pytest.mark.asyncio -async def test_auth_required(setup: SetupTest) -> None: +async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() username = token_data.username key = token_data.token.key - r = await setup.client.get("/auth/api/v1/history/token-changes") + r = await client.get("/auth/api/v1/history/token-changes") assert r.status_code == 401 - r = await setup.client.get( - f"/auth/api/v1/users/{username}/token-change-history" - ) + r = await client.get(f"/auth/api/v1/users/{username}/token-change-history") assert r.status_code == 401 - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{username}/tokens/{key}/change-history" ) assert r.status_code == 401 @pytest.mark.asyncio -async def test_admin_required(setup: SetupTest) -> None: +async def test_admin_required(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() - await setup.login(token_data.token) + await setup.login(client, token_data.token) - r = await setup.client.get("/auth/api/v1/history/token-changes") + r = await client.get("/auth/api/v1/history/token-changes") assert r.status_code == 403 @pytest.mark.asyncio -async def test_no_scope(setup: SetupTest) -> None: +async def test_no_scope(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() username = token_data.username token_service = setup.factory.create_token_service() @@ -501,13 +505,13 @@ async def test_no_scope(setup: SetupTest) -> None: ip_address="127.0.0.1", ) - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{username}/token-change-history", headers={"Authorization": f"bearer {token}"}, ) assert r.status_code == 403 - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{username}/tokens/{token.key}/change-history", headers={"Authorization": f"bearer {token}"}, ) diff --git a/tests/handlers/api_login_test.py b/tests/handlers/api_login_test.py index af52e7639..880d076f6 100644 --- a/tests/handlers/api_login_test.py +++ b/tests/handlers/api_login_test.py @@ -17,7 +17,7 @@ from tests.support.headers import parse_www_authenticate if TYPE_CHECKING: - from httpx import Response + from httpx import AsyncClient, Response from gafaelfawr.config import Config from tests.support.setup import SetupTest @@ -32,14 +32,14 @@ def assert_unauthorized_is_correct(r: Response, config: Config) -> None: @pytest.mark.asyncio -async def test_login(setup: SetupTest) -> None: +async def test_login(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( username="example", scopes=["read:all", "exec:admin"] ) cookie = await State(token=token_data.token).as_cookie() - setup.client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) + client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) - r = await setup.client.get("/auth/api/v1/login") + r = await client.get("/auth/api/v1/login") assert r.status_code == 200 data = r.json() @@ -59,13 +59,13 @@ async def test_login(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_login_no_auth(setup: SetupTest) -> None: - r = await setup.client.get("/auth/api/v1/login") +async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth/api/v1/login") assert_unauthorized_is_correct(r, setup.config) # An Authorization header with a valid token still redirects. token_data = await setup.create_session_token() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/login", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -73,7 +73,7 @@ async def test_login_no_auth(setup: SetupTest) -> None: # A token with no underlying Redis representation is ignored. state = State(token=Token()) - r = await setup.client.get( + r = await client.get( "/auth/api/v1/login", cookies={COOKIE_NAME: await state.as_cookie()}, ) @@ -85,7 +85,7 @@ async def test_login_no_auth(setup: SetupTest) -> None: fernet = Fernet(key) data = {"token": "bad-token"} bad_cookie = fernet.encrypt(json.dumps(data).encode()).decode() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/login", cookies={COOKIE_NAME: bad_cookie}, ) @@ -93,7 +93,7 @@ async def test_login_no_auth(setup: SetupTest) -> None: # And finally check with a mangled state that won't decrypt. bad_cookie = "XXX" + await state.as_cookie() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/login", cookies={COOKIE_NAME: bad_cookie}, ) diff --git a/tests/handlers/api_tokens_test.py b/tests/handlers/api_tokens_test.py index 8424451e3..93669502e 100644 --- a/tests/handlers/api_tokens_test.py +++ b/tests/handlers/api_tokens_test.py @@ -18,13 +18,14 @@ if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio async def test_create_delete_modify( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: user_info = TokenUserInfo( username="example", @@ -39,11 +40,11 @@ async def test_create_delete_modify( scopes=["read:all", "exec:admin", "user:token"], ip_address="127.0.0.1", ) - csrf = await setup.login(session_token) + csrf = await setup.login(client, session_token) await setup.session.commit() expires = current_datetime() + timedelta(days=100) - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/example/tokens", headers={"X-CSRF-Token": csrf}, json={ @@ -58,7 +59,7 @@ async def test_create_delete_modify( token_url = r.headers["Location"] assert token_url == f"/auth/api/v1/users/example/tokens/{user_token.key}" - r = await setup.client.get(token_url) + r = await client.get(token_url) assert r.status_code == 200 info = r.json() assert info == { @@ -76,18 +77,18 @@ async def test_create_delete_modify( # over the Authorization header, but we can't just delete the cookie since # we'll lose the CSRF token. Save the cookie and delete it, and then # later restore it. - cookie = setup.client.cookies.pop(COOKIE_NAME) - r = await setup.client.get( + cookie = client.cookies.pop(COOKIE_NAME) + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {user_token}"}, ) assert r.status_code == 200 assert r.json() == info - setup.client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) + client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) # Listing all tokens for this user should return the user token and a # session token. - r = await setup.client.get("/auth/api/v1/users/example/tokens") + r = await client.get("/auth/api/v1/users/example/tokens") assert r.status_code == 200 data = r.json() @@ -110,7 +111,7 @@ async def test_create_delete_modify( # Change the name, scope, and expiration of the token. caplog.clear() new_expires = current_datetime() + timedelta(days=200) - r = await setup.client.patch( + r = await client.patch( token_url, headers={"X-CSRF-Token": csrf}, json={ @@ -151,22 +152,22 @@ async def test_create_delete_modify( ] # Delete the token. - r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf}) + r = await client.delete(token_url, headers={"X-CSRF-Token": csrf}) assert r.status_code == 204 - r = await setup.client.get(token_url) + r = await client.get(token_url) assert r.status_code == 404 # Deleting again should return 404. - r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf}) + r = await client.delete(token_url, headers={"X-CSRF-Token": csrf}) assert r.status_code == 404 # This user should now have only one token. - r = await setup.client.get("/auth/api/v1/users/example/tokens") + r = await client.get("/auth/api/v1/users/example/tokens") assert r.status_code == 200 assert len(r.json()) == 1 # We should be able to see the change history for the token. - r = await setup.client.get(token_url + "/change-history") + r = await client.get(token_url + "/change-history") assert r.status_code == 200 assert r.json() == [ { @@ -212,7 +213,7 @@ async def test_create_delete_modify( @pytest.mark.asyncio -async def test_token_info(setup: SetupTest) -> None: +async def test_token_info(client: AsyncClient, setup: SetupTest) -> None: user_info = TokenUserInfo( username="example", name="Example Person", @@ -226,7 +227,7 @@ async def test_token_info(setup: SetupTest) -> None: ) await setup.session.commit() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {session_token}"}, ) @@ -246,7 +247,7 @@ async def test_token_info(setup: SetupTest) -> None: expires = created + timedelta(minutes=setup.config.issuer.exp_minutes) assert datetime.fromtimestamp(data["expires"], tz=timezone.utc) == expires - r = await setup.client.get( + r = await client.get( "/auth/api/v1/user-info", headers={"Authorization": f"bearer {session_token}"}, ) @@ -279,7 +280,7 @@ async def test_token_info(setup: SetupTest) -> None: ) await setup.session.commit() - r = await setup.client.get( + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {user_token}"}, ) @@ -295,7 +296,7 @@ async def test_token_info(setup: SetupTest) -> None: "expires": int(expires.timestamp()), } - r = await setup.client.get( + r = await client.get( "/auth/api/v1/user-info", headers={"Authorization": f"bearer {user_token}"}, ) @@ -304,57 +305,55 @@ async def test_token_info(setup: SetupTest) -> None: # Test getting a list of tokens for a user. state = State(token=session_token) - r = await setup.client.get( + r = await client.get( "/auth/api/v1/users/example/tokens", cookies={COOKIE_NAME: await state.as_cookie()}, ) @pytest.mark.asyncio -async def test_auth_required(setup: SetupTest) -> None: +async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() token = token_data.token - csrf = await setup.login(token) + csrf = await setup.login(client, token) # Replace the cookie with one containing the CSRF token but not the # authentication token. - setup.logout() - setup.client.cookies[COOKIE_NAME] = await State(csrf=csrf).as_cookie() + setup.logout(client) + client.cookies[COOKIE_NAME] = await State(csrf=csrf).as_cookie() - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"X-CSRF-Token": csrf}, json={"username": "foo", "token_type": "service"}, ) assert r.status_code == 401 - r = await setup.client.get("/auth/api/v1/users/example/tokens") + r = await client.get("/auth/api/v1/users/example/tokens") assert r.status_code == 401 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/example/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, ) assert r.status_code == 401 - r = await setup.client.get( - f"/auth/api/v1/users/example/tokens/{token.key}" - ) + r = await client.get(f"/auth/api/v1/users/example/tokens/{token.key}") assert r.status_code == 401 - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/example/tokens/{token.key}/change-history" ) assert r.status_code == 401 - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/example/tokens/{token.key}", headers={"X-CSRF-Token": csrf}, ) assert r.status_code == 401 - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/example/tokens/{token.key}", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, @@ -363,9 +362,9 @@ async def test_auth_required(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_csrf_required(setup: SetupTest) -> None: +async def test_csrf_required(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token(scopes=["admin:token"]) - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) token_service = setup.factory.create_token_service() user_token = await token_service.create_user_token( token_data, @@ -375,48 +374,48 @@ async def test_csrf_required(setup: SetupTest) -> None: ip_address="127.0.0.1", ) - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", json={"username": "foo", "token_type": "service"}, ) assert r.status_code == 403 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"X-CSRF-Token": f"XXX{csrf}"}, json={"username": "foo", "token_type": "service"}, ) assert r.status_code == 403 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/example/tokens", json={"token_name": "some token"} ) assert r.status_code == 403 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/example/tokens", headers={"X-CSRF-Token": f"XXX{csrf}"}, json={"token_name": "some token"}, ) assert r.status_code == 403 - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/example/tokens/{user_token.key}" ) assert r.status_code == 403 - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/example/tokens/{user_token.key}", headers={"X-CSRF-Token": f"XXX{csrf}"}, ) assert r.status_code == 403 - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/example/tokens/{user_token.key}", json={"token_name": "some token"}, ) assert r.status_code == 403 - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/example/tokens/{user_token.key}", headers={"X-CSRF-Token": f"XXX{csrf}"}, json={"token_name": "some token"}, @@ -425,37 +424,37 @@ async def test_csrf_required(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_bootstrap(setup: SetupTest) -> None: +async def test_no_bootstrap(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() token = token_data.token bootstrap_token = str(setup.config.bootstrap_token) - r = await setup.client.get( + r = await client.get( "/auth/api/v1/users/example/tokens", headers={"Authorization": f"bearer {bootstrap_token}"}, ) assert r.status_code == 401 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/example/tokens", headers={"Authorization": f"bearer {bootstrap_token}"}, json={"token_name": "some token"}, ) assert r.status_code == 401 - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/example/tokens/{token.key}", headers={"Authorization": f"bearer {bootstrap_token}"}, ) assert r.status_code == 401 - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/example/tokens/{token.key}", headers={"Authorization": f"bearer {bootstrap_token}"}, ) assert r.status_code == 401 - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/example/tokens/{token.key}", headers={"Authorization": f"bearer {bootstrap_token}"}, json={"token_name": "some token"}, @@ -464,7 +463,7 @@ async def test_no_bootstrap(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_scope(setup: SetupTest) -> None: +async def test_no_scope(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() token_service = setup.factory.create_token_service() token = await token_service.create_user_token( @@ -476,32 +475,32 @@ async def test_no_scope(setup: SetupTest) -> None: ) await setup.session.commit() - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"Authorization": f"bearer {token}"}, ) assert r.status_code == 403 - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"Authorization": f"bearer {token}"}, json={"token_name": "some token"}, ) assert r.status_code == 403 - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{token_data.username}/tokens/{token.key}", headers={"Authorization": f"bearer {token}"}, ) assert r.status_code == 403 - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/{token_data.username}/tokens/{token.key}", headers={"Authorization": f"bearer {token}"}, ) assert r.status_code == 403 - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/{token_data.username}/tokens/{token.key}", headers={"Authorization": f"bearer {token}"}, json={"token_name": "some token"}, @@ -510,12 +509,12 @@ async def test_no_scope(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_modify_nonuser(setup: SetupTest) -> None: +async def test_modify_nonuser(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() token = token_data.token - csrf = await setup.login(token) + csrf = await setup.login(client, token) - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/{token_data.username}/tokens/{token.key}", headers={"X-CSRF-Token": csrf}, json={"token_name": "happy token"}, @@ -525,9 +524,9 @@ async def test_modify_nonuser(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_wrong_user(setup: SetupTest) -> None: +async def test_wrong_user(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) token_service = setup.factory.create_token_service() user_info = TokenUserInfo( username="other-person", name="Some Other Person", uid=137123 @@ -547,12 +546,12 @@ async def test_wrong_user(setup: SetupTest) -> None: await setup.session.commit() # Get a token list. - r = await setup.client.get("/auth/api/v1/users/other-person/tokens") + r = await client.get("/auth/api/v1/users/other-person/tokens") assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "permission_denied" # Create a new user token. - r = await setup.client.post( + r = await client.post( "/auth/api/v1/users/other-person/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "happy token"}, @@ -561,14 +560,14 @@ async def test_wrong_user(setup: SetupTest) -> None: assert r.json()["detail"][0]["type"] == "permission_denied" # Get an individual token. - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/other-person/tokens/{other_token.key}" ) assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "permission_denied" # Get the history of an individual token. - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/other-person/tokens/{other_token.key}" "/change-history" ) @@ -576,40 +575,40 @@ async def test_wrong_user(setup: SetupTest) -> None: assert r.json()["detail"][0]["type"] == "permission_denied" # Ensure you can't see someone else's token under your username either. - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}" ) assert r.status_code == 404 # Or their history. - r = await setup.client.get( + r = await client.get( f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}" "/change-history" ) assert r.status_code == 404 # Delete a token. - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/other-person/tokens/{other_token.key}", headers={"X-CSRF-Token": csrf}, ) assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "permission_denied" - r = await setup.client.delete( + r = await client.delete( f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}", headers={"X-CSRF-Token": csrf}, ) assert r.status_code == 404 # Modify a token. - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/other-person/tokens/{other_token.key}", json={"token_name": "happy token"}, headers={"X-CSRF-Token": csrf}, ) assert r.status_code == 403 assert r.json()["detail"][0]["type"] == "permission_denied" - r = await setup.client.patch( + r = await client.patch( f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}", json={"token_name": "happy token"}, headers={"X-CSRF-Token": csrf}, @@ -618,12 +617,12 @@ async def test_wrong_user(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_expires(setup: SetupTest) -> None: +async def test_no_expires(client: AsyncClient, setup: SetupTest) -> None: """Test creating a user token that doesn't expire.""" token_data = await setup.create_session_token() - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, @@ -631,13 +630,13 @@ async def test_no_expires(setup: SetupTest) -> None: assert r.status_code == 201 token_url = r.headers["Location"] - r = await setup.client.get(token_url) + r = await client.get(token_url) assert "expires" not in r.json() # Create a user token with an expiration and then adjust it to not expire. now = datetime.now(tz=timezone.utc).replace(microsecond=0) expires = now + timedelta(days=2) - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={ @@ -652,10 +651,10 @@ async def test_no_expires(setup: SetupTest) -> None: assert user_token_data and user_token_data.expires == expires token_url = r.headers["Location"] - r = await setup.client.get(token_url) + r = await client.get(token_url) assert r.json()["expires"] == int(expires.timestamp()) - r = await setup.client.patch( + r = await client.patch( token_url, headers={"X-CSRF-Token": csrf}, json={"expires": None}, @@ -670,18 +669,20 @@ async def test_no_expires(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_duplicate_token_name(setup: SetupTest) -> None: +async def test_duplicate_token_name( + client: AsyncClient, setup: SetupTest +) -> None: """Test duplicate token names.""" token_data = await setup.create_session_token() - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, ) assert r.status_code == 201 - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, @@ -691,14 +692,14 @@ async def test_duplicate_token_name(setup: SetupTest) -> None: # Create a token with a different name and then try to modify the name to # conflict. - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "another token"}, ) assert r.status_code == 201 token_url = r.headers["Location"] - r = await setup.client.patch( + r = await client.patch( token_url, headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, @@ -708,15 +709,15 @@ async def test_duplicate_token_name(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bad_expires(setup: SetupTest) -> None: +async def test_bad_expires(client: AsyncClient, setup: SetupTest) -> None: """Test creating or modifying a token with bogus expirations.""" token_data = await setup.create_session_token() - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) now = int(time.time()) bad_expires = [-now, -1, 0, now, now + (5 * 60) - 1] for bad_expire in bad_expires: - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token", "expires": bad_expire}, @@ -727,7 +728,7 @@ async def test_bad_expires(setup: SetupTest) -> None: assert data["detail"][0]["type"] == "invalid_expires" # Create a valid token. - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token"}, @@ -737,7 +738,7 @@ async def test_bad_expires(setup: SetupTest) -> None: # Now try modifying the expiration time to the same bogus values. for bad_expire in bad_expires: - r = await setup.client.patch( + r = await client.patch( token_url, headers={"X-CSRF-Token": csrf}, json={"expires": bad_expire}, @@ -749,19 +750,19 @@ async def test_bad_expires(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bad_scopes(setup: SetupTest) -> None: +async def test_bad_scopes(client: AsyncClient, setup: SetupTest) -> None: """Test creating or modifying a token with bogus scopes.""" known_scopes = list(setup.config.known_scopes.keys()) assert len(known_scopes) > 4 token_data = await setup.create_session_token( scopes=known_scopes[1:3] + ["other:scope", "user:token"] ) - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) # Check that we reject both an unknown scope and a scope that's present on # the session but isn't valid in the configuration. for bad_scope in (known_scopes[3], "other:scope"): - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token", "scopes": [bad_scope]}, @@ -772,7 +773,7 @@ async def test_bad_scopes(setup: SetupTest) -> None: assert data["detail"][0]["type"] == "invalid_scopes" # Create a valid token with all of the scopes as the session. - r = await setup.client.post( + r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", headers={"X-CSRF-Token": csrf}, json={"token_name": "some token", "scopes": known_scopes[1:3]}, @@ -782,7 +783,7 @@ async def test_bad_scopes(setup: SetupTest) -> None: # Now try modifying it with the invalid scope. for bad_scope in (known_scopes[3], "other:scope"): - r = await setup.client.patch( + r = await client.patch( token_url, headers={"X-CSRF-Token": csrf}, json={"scopes": [known_scopes[1], bad_scope]}, @@ -794,12 +795,12 @@ async def test_bad_scopes(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_create_admin(setup: SetupTest) -> None: +async def test_create_admin(client: AsyncClient, setup: SetupTest) -> None: """Test creating a token through the admin interface.""" token_data = await setup.create_session_token(scopes=["exec:admin"]) - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"X-CSRF-Token": csrf}, json={"username": "a-service", "token_type": "service"}, @@ -807,11 +808,11 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.status_code == 403 token_data = await setup.create_session_token(scopes=["admin:token"]) - csrf = await setup.login(token_data.token) + csrf = await setup.login(client, token_data.token) now = datetime.now(tz=timezone.utc) expires = int((now + timedelta(days=2)).timestamp()) - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"X-CSRF-Token": csrf}, json={ @@ -831,8 +832,8 @@ async def test_create_admin(setup: SetupTest) -> None: token_url = f"/auth/api/v1/users/a-service/tokens/{service_token.key}" assert r.headers["Location"] == token_url - setup.logout() - r = await setup.client.get( + setup.logout(client) + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {str(service_token)}"}, ) @@ -845,7 +846,7 @@ async def test_create_admin(setup: SetupTest) -> None: "created": ANY, "expires": expires, } - r = await setup.client.get( + r = await client.get( "/auth/api/v1/user-info", headers={"Authorization": f"bearer {str(service_token)}"}, ) @@ -859,19 +860,19 @@ async def test_create_admin(setup: SetupTest) -> None: "groups": [{"name": "some-group", "id": 12381}], } - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={"username": "a-user", "token_type": "session"}, ) assert r.status_code == 422 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={"username": "a-user", "token_type": "user"}, ) assert r.status_code == 422 - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={ @@ -883,7 +884,7 @@ async def test_create_admin(setup: SetupTest) -> None: ) assert r.status_code == 422 assert r.json()["detail"][0]["type"] == "invalid_expires" - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={ @@ -896,7 +897,7 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.status_code == 422 assert r.json()["detail"][0]["type"] == "invalid_scopes" - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={ @@ -912,7 +913,7 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.headers["Location"] == token_url # Successfully create a user token. - r = await setup.client.get( + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {str(user_token)}"}, ) @@ -925,7 +926,7 @@ async def test_create_admin(setup: SetupTest) -> None: "scopes": [], "created": ANY, } - r = await setup.client.get( + r = await client.get( "/auth/api/v1/user-info", headers={"Authorization": f"bearer {str(user_token)}"}, ) @@ -933,7 +934,7 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.json() == {"username": "a-user"} # Check handling of duplicate token name errors. - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={ @@ -946,7 +947,7 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.json()["detail"][0]["type"] == "duplicate_token_name" # Check handling of an invalid username. - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={"Authorization": f"bearer {str(service_token)}"}, json={ @@ -958,7 +959,7 @@ async def test_create_admin(setup: SetupTest) -> None: assert r.status_code == 422 # Check that the bootstrap token also works. - r = await setup.client.post( + r = await client.post( "/auth/api/v1/tokens", headers={ "Authorization": f"bearer {str(setup.config.bootstrap_token)}" diff --git a/tests/handlers/auth_logging_test.py b/tests/handlers/auth_logging_test.py index d7e07f3ec..2517ff24d 100644 --- a/tests/handlers/auth_logging_test.py +++ b/tests/handlers/auth_logging_test.py @@ -11,17 +11,20 @@ if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_success(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_success( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: token_data = await setup.create_session_token(scopes=["exec:admin"]) # Successful request with X-Forwarded-For and a bearer token. caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -51,7 +54,7 @@ async def test_success(setup: SetupTest, caplog: LogCaptureFixture) -> None: basic = f"{token_data.token}:x-oauth-basic".encode() basic_b64 = base64.b64encode(basic).decode() caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -68,7 +71,7 @@ async def test_success(setup: SetupTest, caplog: LogCaptureFixture) -> None: basic = f"x-oauth-basic:{token_data.token}".encode() basic_b64 = base64.b64encode(basic).decode() caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -84,12 +87,12 @@ async def test_success(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio async def test_authorization_failed( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: token_data = await setup.create_session_token(scopes=["exec:admin"]) caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:test", "satisfy": "any"}, headers={ @@ -120,12 +123,12 @@ async def test_authorization_failed( @pytest.mark.asyncio async def test_original_url( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: token_data = await setup.create_session_token() caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -154,7 +157,7 @@ async def test_original_url( # Check with both X-Original-URI and X-Original-URL. The former should # override the latter. caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -170,12 +173,12 @@ async def test_original_url( @pytest.mark.asyncio async def test_chained_x_forwarded( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: token_data = await setup.create_session_token() caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -208,10 +211,10 @@ async def test_chained_x_forwarded( @pytest.mark.asyncio async def test_invalid_token( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": "Bearer blah"}, diff --git a/tests/handlers/auth_test.py b/tests/handlers/auth_test.py index a4156e42d..403d8c1ea 100644 --- a/tests/handlers/auth_test.py +++ b/tests/handlers/auth_test.py @@ -13,12 +13,14 @@ from tests.support.headers import parse_www_authenticate if TYPE_CHECKING: + from httpx import AsyncClient + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_no_auth(setup: SetupTest) -> None: - r = await setup.client.get("/auth", params={"scope": "exec:admin"}) +async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth", params={"scope": "exec:admin"}) assert r.status_code == 401 assert r.headers["Cache-Control"] == "no-cache, must-revalidate" authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) @@ -26,7 +28,7 @@ async def test_no_auth(setup: SetupTest) -> None: assert authenticate.auth_type == AuthType.Bearer assert authenticate.realm == setup.config.realm - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "bearer"} ) assert r.status_code == 401 @@ -36,12 +38,12 @@ async def test_no_auth(setup: SetupTest) -> None: assert authenticate.auth_type == AuthType.Bearer assert authenticate.realm == setup.config.realm - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "bogus"} ) assert r.status_code == 422 - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "basic"} ) assert r.status_code == 401 @@ -53,15 +55,15 @@ async def test_no_auth(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid(setup: SetupTest) -> None: +async def test_invalid(client: AsyncClient, setup: SetupTest) -> None: token = await setup.create_session_token() - r = await setup.client.get( + r = await client.get( "/auth", headers={"Authorization": f"bearer {token.token}"} ) assert r.status_code == 422 assert r.json()["detail"][0]["type"] == "value_error.missing" - r = await setup.client.get( + r = await client.get( "/auth", headers={"Authorization": f"bearer {token.token}"}, params={"scope": "exec:admin", "satisfy": "foo"}, @@ -69,7 +71,7 @@ async def test_invalid(setup: SetupTest) -> None: assert r.status_code == 422 assert r.json()["detail"][0]["type"] == "type_error.enum" - r = await setup.client.get( + r = await client.get( "/auth", headers={"Authorization": f"bearer {token.token}"}, params={"scope": "exec:admin", "auth_type": "foo"}, @@ -79,8 +81,8 @@ async def test_invalid(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_auth(setup: SetupTest) -> None: - r = await setup.client.get( +async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": "Bearer"}, @@ -92,7 +94,7 @@ async def test_invalid_auth(setup: SetupTest) -> None: assert authenticate.realm == setup.config.realm assert authenticate.error == AuthError.invalid_request - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": "token foo"}, @@ -104,7 +106,7 @@ async def test_invalid_auth(setup: SetupTest) -> None: assert authenticate.realm == setup.config.realm assert authenticate.error == AuthError.invalid_request - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": "Bearer token"}, @@ -119,7 +121,7 @@ async def test_invalid_auth(setup: SetupTest) -> None: # Create a nonexistent token. token = Token() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Bearer {token}"}, @@ -134,10 +136,10 @@ async def test_invalid_auth(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_access_denied(setup: SetupTest) -> None: +async def test_access_denied(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Bearer {token_data.token}"}, @@ -154,8 +156,8 @@ async def test_access_denied(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_auth_forbidden(setup: SetupTest) -> None: - r = await setup.client.get( +async def test_auth_forbidden(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get( "/auth/forbidden", params=[("scope", "exec:test"), ("scope", "exec:admin")], ) @@ -169,7 +171,7 @@ async def test_auth_forbidden(setup: SetupTest) -> None: assert authenticate.scope == "exec:admin exec:test" assert "Token missing required scope" in r.text - r = await setup.client.get( + r = await client.get( "/auth/forbidden", params={"scope": "exec:admin", "auth_type": "basic"} ) assert r.status_code == 403 @@ -182,10 +184,10 @@ async def test_auth_forbidden(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_satisfy_all(setup: SetupTest) -> None: +async def test_satisfy_all(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token(scopes=["exec:test"]) - r = await setup.client.get( + r = await client.get( "/auth", params=[("scope", "exec:test"), ("scope", "exec:admin")], headers={"Authorization": f"Bearer {token_data.token}"}, @@ -202,12 +204,12 @@ async def test_satisfy_all(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success(setup: SetupTest) -> None: +async def test_success(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( group_names=["admin"], scopes=["exec:admin", "read:all"] ) - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={ @@ -227,14 +229,14 @@ async def test_success(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success_minimal(setup: SetupTest) -> None: +async def test_success_minimal(client: AsyncClient, setup: SetupTest) -> None: user_info = TokenUserInfo(username="user", uid=1234) token_service = setup.factory.create_token_service() token = await token_service.create_session_token( user_info, scopes=["read:all"], ip_address="127.0.0.1" ) - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "read:all"}, headers={"Authorization": f"Bearer {token}"}, @@ -250,13 +252,13 @@ async def test_success_minimal(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_notebook(setup: SetupTest) -> None: +async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( group_names=["admin"], scopes=["exec:admin", "read:all"] ) assert token_data.expires - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "notebook": "true"}, headers={"Authorization": f"Bearer {token_data.token}"}, @@ -265,7 +267,7 @@ async def test_notebook(setup: SetupTest) -> None: notebook_token = Token.from_str(r.headers["X-Auth-Request-Token"]) assert notebook_token != token_data.token - r = await setup.client.get( + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"Bearer {notebook_token}"}, ) @@ -281,7 +283,7 @@ async def test_notebook(setup: SetupTest) -> None: } # Requesting a token with the same parameters returns the same token. - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "notebook": "true"}, headers={"Authorization": f"Bearer {token_data.token}"}, @@ -291,13 +293,13 @@ async def test_notebook(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_internal(setup: SetupTest) -> None: +async def test_internal(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( group_names=["admin"], scopes=["exec:admin", "read:all", "read:some"] ) assert token_data.expires - r = await setup.client.get( + r = await client.get( "/auth", params={ "scope": "exec:admin", @@ -310,7 +312,7 @@ async def test_internal(setup: SetupTest) -> None: internal_token = Token.from_str(r.headers["X-Auth-Request-Token"]) assert internal_token != token_data.token - r = await setup.client.get( + r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"Bearer {internal_token}"}, ) @@ -327,7 +329,7 @@ async def test_internal(setup: SetupTest) -> None: } # Requesting a token with the same parameters returns the same token. - r = await setup.client.get( + r = await client.get( "/auth", params={ "scope": "exec:admin", @@ -341,11 +343,11 @@ async def test_internal(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_internal_errors(setup: SetupTest) -> None: +async def test_internal_errors(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token(scopes=["read:some"]) # Delegating a token with a scope the original doesn't have will fail. - r = await setup.client.get( + r = await client.get( "/auth", params={ "scope": "read:some", @@ -362,7 +364,7 @@ async def test_internal_errors(setup: SetupTest) -> None: assert authenticate.scope == "read:all read:some" # Cannot request a notebook token and an internal token at the same time. - r = await setup.client.get( + r = await client.get( "/auth", params={ "scope": "read:some", @@ -376,7 +378,7 @@ async def test_internal_errors(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success_any(setup: SetupTest) -> None: +async def test_success_any(client: AsyncClient, setup: SetupTest) -> None: """Test ``satisfy=any`` as an ``/auth`` parameter. Ask for either ``exec:admin`` or ``exec:test`` and pass in credentials @@ -387,7 +389,7 @@ async def test_success_any(setup: SetupTest) -> None: group_names=["test"], scopes=["exec:test"] ) - r = await setup.client.get( + r = await client.get( "/auth", params=[ ("scope", "exec:admin"), @@ -405,14 +407,14 @@ async def test_success_any(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_basic(setup: SetupTest) -> None: +async def test_basic(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token( group_names=["test"], scopes=["exec:admin"] ) basic = f"{token_data.token}:x-oauth-basic".encode() basic_b64 = base64.b64encode(basic).decode() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Basic {basic_b64}"}, @@ -422,7 +424,7 @@ async def test_basic(setup: SetupTest) -> None: basic = f"x-oauth-basic:{token_data.token}".encode() basic_b64 = base64.b64encode(basic).decode() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Basic {basic_b64}"}, @@ -434,7 +436,7 @@ async def test_basic(setup: SetupTest) -> None: # appear anywhere in the auth string. basic = f"{token_data.token}:something-else".encode() basic_b64 = base64.b64encode(basic).decode() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Basic {basic_b64}"}, @@ -444,9 +446,9 @@ async def test_basic(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_basic_failure(setup: SetupTest) -> None: +async def test_basic_failure(client: AsyncClient, setup: SetupTest) -> None: basic_b64 = base64.b64encode(b"bogus-string").decode() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"Authorization": f"Basic {basic_b64}"}, @@ -460,7 +462,7 @@ async def test_basic_failure(setup: SetupTest) -> None: for basic in (b"foo:foo", b"x-oauth-basic:foo", b"foo:x-oauth-basic"): basic_b64 = base64.b64encode(basic).decode() - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "basic"}, headers={"Authorization": f"Basic {basic_b64}"}, @@ -473,9 +475,11 @@ async def test_basic_failure(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_ajax_unauthorized(setup: SetupTest) -> None: +async def test_ajax_unauthorized( + client: AsyncClient, setup: SetupTest +) -> None: """Test that AJAX requests without auth get 403, not 401.""" - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "exec:admin"}, headers={"X-Requested-With": "XMLHttpRequest"}, @@ -488,14 +492,16 @@ async def test_ajax_unauthorized(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success_unicode_name(setup: SetupTest) -> None: +async def test_success_unicode_name( + client: AsyncClient, setup: SetupTest +) -> None: user_info = TokenUserInfo(username="user", uid=1234, name="名字") token_service = setup.factory.create_token_service() token = await token_service.create_session_token( user_info, scopes=["read:all"], ip_address="127.0.0.1" ) - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "read:all"}, headers={"Authorization": f"Bearer {token}"}, diff --git a/tests/handlers/index_test.py b/tests/handlers/index_test.py index 076e819df..2356dc297 100644 --- a/tests/handlers/index_test.py +++ b/tests/handlers/index_test.py @@ -7,12 +7,14 @@ import pytest if TYPE_CHECKING: + from httpx import AsyncClient + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_get_index(setup: SetupTest) -> None: - r = await setup.client.get("/") +async def test_get_index(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/") assert r.status_code == 200 data = r.json() assert data["name"] == setup.config.safir.name diff --git a/tests/handlers/influxdb_test.py b/tests/handlers/influxdb_test.py index dc11edadf..5855ad4c8 100644 --- a/tests/handlers/influxdb_test.py +++ b/tests/handlers/influxdb_test.py @@ -14,19 +14,22 @@ if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_influxdb(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_influxdb( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: token_data = await setup.create_session_token() assert token_data.expires influxdb_secret = setup.config.issuer.influxdb_secret assert influxdb_secret caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/tokens/influxdb/new", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -62,8 +65,8 @@ async def test_influxdb(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio -async def test_no_auth(setup: SetupTest) -> None: - r = await setup.client.get("/auth/tokens/influxdb/new") +async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth/tokens/influxdb/new") assert r.status_code == 401 authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert not isinstance(authenticate, AuthErrorChallenge) @@ -73,13 +76,13 @@ async def test_no_auth(setup: SetupTest) -> None: @pytest.mark.asyncio async def test_not_configured( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: await setup.configure("oidc") token_data = await setup.create_session_token() caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/tokens/influxdb/new", headers={"Authorization": f"bearer {token_data.token}"}, ) @@ -105,7 +108,7 @@ async def test_not_configured( @pytest.mark.asyncio async def test_influxdb_force_username( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: await setup.configure("influxdb-username") token_data = await setup.create_session_token() @@ -114,7 +117,7 @@ async def test_influxdb_force_username( assert influxdb_secret caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/tokens/influxdb/new", headers={"Authorization": f"bearer {token_data.token}"}, ) diff --git a/tests/handlers/login_github_test.py b/tests/handlers/login_github_test.py index a6ae4bf71..da8b1ab70 100644 --- a/tests/handlers/login_github_test.py +++ b/tests/handlers/login_github_test.py @@ -19,12 +19,13 @@ from typing import Dict, Optional from _pytest.logging import LogCaptureFixture - from httpx import Response + from httpx import AsyncClient, Response from tests.support.setup import SetupTest async def simulate_github_login( + client: AsyncClient, setup: SetupTest, user_info: GitHubUserInfo, headers: Optional[Dict[str, str]] = None, @@ -72,9 +73,7 @@ async def simulate_github_login( ) # Simulate the redirect to GitHub. - r = await setup.client.get( - "/login", params={"rd": return_url}, headers=headers - ) + r = await client.get("/login", params={"rd": return_url}, headers=headers) assert r.status_code == 307 url = urlparse(r.headers["Location"]) assert url.scheme == "https" @@ -88,7 +87,7 @@ async def simulate_github_login( } # Simulate the return from GitHub. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) if r.status_code == 307: @@ -98,7 +97,9 @@ async def simulate_github_login( @pytest.mark.asyncio -async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_login( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -118,7 +119,9 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: # Simulate the GitHub login. caplog.clear() - r = await simulate_github_login(setup, user_info, return_url=return_url) + r = await simulate_github_login( + client, setup, user_info, return_url=return_url + ) assert r.status_code == 307 assert parse_log(caplog) == [ { @@ -152,7 +155,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: # Check that the /auth route works and finds our token, and that the user # information is correct. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-Token-Scopes"] == "read:all user:token" assert r.headers["X-Auth-Request-Scopes-Accepted"] == "read:all" @@ -164,7 +167,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: assert r.headers["X-Auth-Request-Groups"] == expected # Do the same verification with the user-info endpoint. - r = await setup.client.get("/auth/api/v1/user-info") + r = await client.get("/auth/api/v1/user-info") assert r.status_code == 200 assert r.json() == { "username": "githubuser", @@ -180,7 +183,9 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio -async def test_login_redirect_header(setup: SetupTest) -> None: +async def test_login_redirect_header( + client: AsyncClient, setup: SetupTest +) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" user_info = GitHubUserInfo( name="GitHub User", @@ -193,7 +198,7 @@ async def test_login_redirect_header(setup: SetupTest) -> None: setup.set_github_response("some-code", user_info) # Simulate the initial authentication request. - r = await setup.client.get( + r = await client.get( "/login", headers={"X-Auth-Request-Redirect": return_url} ) assert r.status_code == 307 @@ -201,7 +206,7 @@ async def test_login_redirect_header(setup: SetupTest) -> None: query = parse_qs(url.query) # Simulate the return from GitHub. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 @@ -209,13 +214,17 @@ async def test_login_redirect_header(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_login_no_destination(setup: SetupTest) -> None: - r = await setup.client.get("/login") +async def test_login_no_destination( + client: AsyncClient, setup: SetupTest +) -> None: + r = await client.get("/login") assert r.status_code == 422 @pytest.mark.asyncio -async def test_cookie_auth_with_token(setup: SetupTest) -> None: +async def test_cookie_auth_with_token( + client: AsyncClient, setup: SetupTest +) -> None: """Test that cookie auth takes precedence over an Authorization header. JupyterHub sends an Authorization header in its internal requests with @@ -234,13 +243,14 @@ async def test_cookie_auth_with_token(setup: SetupTest) -> None: # Simulate the GitHub login. r = await simulate_github_login( + client, setup, user_info, headers={"Authorization": "token some-jupyterhub-token"}, ) # Now make a request to the /auth endpoint with a bogus token. - r = await setup.client.get( + r = await client.get( "/auth", params={"scope": "read:all"}, headers={"Authorization": "token some-jupyterhub-token"}, @@ -250,7 +260,7 @@ async def test_cookie_auth_with_token(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bad_redirect(setup: SetupTest) -> None: +async def test_bad_redirect(client: AsyncClient, setup: SetupTest) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -259,12 +269,10 @@ async def test_bad_redirect(setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")], ) - r = await setup.client.get( - "/login", params={"rd": "https://foo.example.com/"} - ) + r = await client.get("/login", params={"rd": "https://foo.example.com/"}) assert r.status_code == 422 - r = await setup.client.get( + r = await client.get( "/login", headers={"X-Auth-Request-Redirect": "https://foo.example.com/"}, ) @@ -273,6 +281,7 @@ async def test_bad_redirect(setup: SetupTest) -> None: # But if we're deployed under foo.example.com as determined by the # X-Forwarded-Host header, this will be allowed. r = await simulate_github_login( + client, setup, user_info, headers={ @@ -285,7 +294,7 @@ async def test_bad_redirect(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_github_uppercase(setup: SetupTest) -> None: +async def test_github_uppercase(client: AsyncClient, setup: SetupTest) -> None: """Tests that usernames and organization names are forced to lowercase. We do not test that slugs are forced to lowercase (and do not change the @@ -300,17 +309,17 @@ async def test_github_uppercase(setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) - r = await simulate_github_login(setup, user_info) + r = await simulate_github_login(client, setup, user_info) assert r.status_code == 307 # The user returned by the /auth route should be forced to lowercase. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-User"] == "someuser" @pytest.mark.asyncio -async def test_github_admin(setup: SetupTest) -> None: +async def test_github_admin(client: AsyncClient, setup: SetupTest) -> None: """Test that a token administrator gets the admin:token scope.""" admin_service = setup.factory.create_admin_service() await admin_service.add_admin( @@ -325,16 +334,16 @@ async def test_github_admin(setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) - r = await simulate_github_login(setup, user_info) + r = await simulate_github_login(client, setup, user_info) assert r.status_code == 307 # The user should have admin:token scope. - r = await setup.client.get("/auth", params={"scope": "admin:token"}) + r = await client.get("/auth", params={"scope": "admin:token"}) assert r.status_code == 200 @pytest.mark.asyncio -async def test_invalid_username(setup: SetupTest) -> None: +async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: """Test that invalid usernames are rejected.""" user_info = GitHubUserInfo( name="A User", @@ -344,13 +353,15 @@ async def test_invalid_username(setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) - r = await simulate_github_login(setup, user_info, expect_revoke=True) + r = await simulate_github_login( + client, setup, user_info, expect_revoke=True + ) assert r.status_code == 403 assert "Invalid username: invalid user" in r.text @pytest.mark.asyncio -async def test_invalid_groups(setup: SetupTest) -> None: +async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: user_info = GitHubUserInfo( name="A User", username="someuser", @@ -363,18 +374,18 @@ async def test_invalid_groups(setup: SetupTest) -> None: ], ) - r = await simulate_github_login(setup, user_info) + r = await simulate_github_login(client, setup, user_info) assert r.status_code == 307 # The invalid groups should not appear but the valid group should still be # present. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-Groups"] == "org-a-team" @pytest.mark.asyncio -async def test_paginated_teams(setup: SetupTest) -> None: +async def test_paginated_teams(client: AsyncClient, setup: SetupTest) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -392,11 +403,13 @@ async def test_paginated_teams(setup: SetupTest) -> None: ], ) - r = await simulate_github_login(setup, user_info, paginate_teams=True) + r = await simulate_github_login( + client, setup, user_info, paginate_teams=True + ) assert r.status_code == 307 # Check the group list. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 expected = ",".join( [ @@ -410,7 +423,7 @@ async def test_paginated_teams(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_valid_groups(setup: SetupTest) -> None: +async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: assert setup.config.github user_info = GitHubUserInfo( name="GitHub User", @@ -420,19 +433,21 @@ async def test_no_valid_groups(setup: SetupTest) -> None: teams=[], ) - r = await simulate_github_login(setup, user_info, expect_revoke=True) + r = await simulate_github_login( + client, setup, user_info, expect_revoke=True + ) assert r.status_code == 403 assert r.headers["Cache-Control"] == "no-cache, must-revalidate" assert "githubuser is not a member of any authorized groups" in r.text assert "Some error instructions with HTML." in r.text # The user should not be logged in. - r = await setup.client.get("/auth", params={"scope": "user:token"}) + r = await client.get("/auth", params={"scope": "user:token"}) assert r.status_code == 401 @pytest.mark.asyncio -async def test_unicode_name(setup: SetupTest) -> None: +async def test_unicode_name(client: AsyncClient, setup: SetupTest) -> None: user_info = GitHubUserInfo( name="名字", username="githubuser", @@ -441,11 +456,11 @@ async def test_unicode_name(setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")], ) - r = await simulate_github_login(setup, user_info) + r = await simulate_github_login(client, setup, user_info) assert r.status_code == 307 # Check that the name as returned from the user-info API is correct. - r = await setup.client.get("/auth/api/v1/user-info") + r = await client.get("/auth/api/v1/user-info") assert r.status_code == 200 assert r.json() == { "username": "githubuser", diff --git a/tests/handlers/login_oidc_test.py b/tests/handlers/login_oidc_test.py index edcf10edb..29b14bf47 100644 --- a/tests/handlers/login_oidc_test.py +++ b/tests/handlers/login_oidc_test.py @@ -13,12 +13,15 @@ if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_login( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token( groups=["admin"], name="Some Person", email="person@example.com" @@ -29,7 +32,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: return_url = "https://example.com:4444/foo?a=bar&b=baz" caplog.clear() - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 assert r.headers["Location"].startswith(setup.config.oidc.login_url) url = urlparse(r.headers["Location"]) @@ -60,7 +63,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: # Simulate the return from the provider. caplog.clear() - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 @@ -94,7 +97,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: ] # Check that the /auth route works and finds our token. - r = await setup.client.get("/auth", params={"scope": "exec:admin"}) + r = await client.get("/auth", params={"scope": "exec:admin"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-Token-Scopes"] == expected_scopes assert r.headers["X-Auth-Request-Scopes-Accepted"] == "exec:admin" @@ -106,7 +109,9 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio -async def test_login_redirect_header(setup: SetupTest) -> None: +async def test_login_redirect_header( + client: AsyncClient, setup: SetupTest +) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" await setup.configure("oidc") token = setup.create_upstream_oidc_token(groups=["admin"]) @@ -114,7 +119,7 @@ async def test_login_redirect_header(setup: SetupTest) -> None: setup.set_oidc_configuration_response(setup.config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" - r = await setup.client.get( + r = await client.get( "/login", headers={"X-Auth-Request-Redirect": return_url} ) assert r.status_code == 307 @@ -122,7 +127,7 @@ async def test_login_redirect_header(setup: SetupTest) -> None: query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 @@ -130,7 +135,7 @@ async def test_login_redirect_header(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_oauth2_callback(setup: SetupTest) -> None: +async def test_oauth2_callback(client: AsyncClient, setup: SetupTest) -> None: """Test the compatibility /oauth2/callback route.""" await setup.configure("oidc") token = setup.create_upstream_oidc_token(groups=["admin"]) @@ -139,14 +144,14 @@ async def test_oauth2_callback(setup: SetupTest) -> None: assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) assert query["redirect_uri"][0] == setup.config.oidc.redirect_url # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -155,7 +160,7 @@ async def test_oauth2_callback(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_claim_names(setup: SetupTest) -> None: +async def test_claim_names(client: AsyncClient, setup: SetupTest) -> None: """Uses an alternate settings environment with non-default claims.""" await setup.configure( "oidc", username_claim="username", uid_claim="numeric_uid" @@ -168,14 +173,14 @@ async def test_claim_names(setup: SetupTest) -> None: setup.set_oidc_configuration_response(setup.config.issuer.keypair) return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) assert query["redirect_uri"][0] == setup.config.oidc.redirect_url # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 @@ -184,7 +189,7 @@ async def test_claim_names(setup: SetupTest) -> None: # Check that the /auth route works and sets the headers correctly. uid # will be set to some-user and uidNumber will be set to 1000, so we'll # know if we read the alternate claim names correctly instead. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-User"] == "alt-username" assert r.headers["X-Auth-Request-Uid"] == "7890" @@ -192,14 +197,14 @@ async def test_claim_names(setup: SetupTest) -> None: @pytest.mark.asyncio async def test_callback_error( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: """Test an error return from the OIDC token endpoint.""" await setup.configure("oidc") assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) @@ -216,7 +221,7 @@ async def test_callback_error( # Simulate the return from the OpenID Connect provider. caplog.clear() - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -248,9 +253,9 @@ async def test_callback_error( setup.respx_mock.post(setup.config.oidc.token_url).respond( 400, json={"foo": "bar"} ) - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -262,9 +267,9 @@ async def test_callback_error( setup.respx_mock.post(setup.config.oidc.token_url).respond( json={"foo": "bar"} ) - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -273,9 +278,9 @@ async def test_callback_error( # Return invalid JSON, which should raise an error during JSON decoding. setup.respx_mock.post(setup.config.oidc.token_url).respond(content=b"foo") - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -286,9 +291,9 @@ async def test_callback_error( setup.respx_mock.post(setup.config.oidc.token_url).respond( 400, content=b"foo" ) - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) - r = await setup.client.get( + r = await client.get( "/oauth2/callback", params={"code": "some-code", "state": query["state"][0]}, ) @@ -297,12 +302,12 @@ async def test_callback_error( @pytest.mark.asyncio -async def test_connection_error(setup: SetupTest) -> None: +async def test_connection_error(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) @@ -311,7 +316,7 @@ async def test_connection_error(setup: SetupTest) -> None: # provider and check that an appropriate error is shown to the user. token_url = setup.config.oidc.token_url setup.respx_mock.post(token_url).mock(side_effect=ConnectError) - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 403 @@ -319,7 +324,7 @@ async def test_connection_error(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_verify_error(setup: SetupTest) -> None: +async def test_verify_error(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token(groups=["admin"]) assert setup.config.oidc @@ -332,7 +337,7 @@ async def test_verify_error(setup: SetupTest) -> None: assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) @@ -340,7 +345,7 @@ async def test_verify_error(setup: SetupTest) -> None: # Returning from OpenID Connect login should fail because we haven't # registered the signing key, and therefore attempting to retrieve it will # fail, causing a token verification error. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 403 @@ -348,7 +353,7 @@ async def test_verify_error(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_username(setup: SetupTest) -> None: +async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token( groups=["admin"], sub="invalid@user", uid="invalid@user" @@ -358,13 +363,13 @@ async def test_invalid_username(setup: SetupTest) -> None: assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 403 @@ -372,7 +377,9 @@ async def test_invalid_username(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_group_syntax(setup: SetupTest) -> None: +async def test_invalid_group_syntax( + client: AsyncClient, setup: SetupTest +) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token( isMemberOf=[{"name": "foo", "id": ["bar"]}] @@ -382,13 +389,13 @@ async def test_invalid_group_syntax(setup: SetupTest) -> None: assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 403 @@ -396,7 +403,7 @@ async def test_invalid_group_syntax(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_groups(setup: SetupTest) -> None: +async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token( isMemberOf=[ @@ -414,38 +421,38 @@ async def test_invalid_groups(setup: SetupTest) -> None: assert setup.config.oidc return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 assert r.headers["Location"] == return_url - r = await setup.client.get("/auth", params={"scope": "exec:admin"}) + r = await client.get("/auth", params={"scope": "exec:admin"}) assert r.status_code == 200 assert r.headers["X-Auth-Request-Groups"] == "valid,admin" @pytest.mark.asyncio -async def test_no_valid_groups(setup: SetupTest) -> None: +async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token(groups=[]) setup.set_oidc_token_response("some-code", token) setup.set_oidc_configuration_response(setup.config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 403 @@ -455,32 +462,32 @@ async def test_no_valid_groups(setup: SetupTest) -> None: assert "Some error instructions with HTML." in r.text # The user should not be logged in. - r = await setup.client.get("/auth", params={"scope": "user:token"}) + r = await client.get("/auth", params={"scope": "user:token"}) assert r.status_code == 401 @pytest.mark.asyncio -async def test_unicode_name(setup: SetupTest) -> None: +async def test_unicode_name(client: AsyncClient, setup: SetupTest) -> None: await setup.configure("oidc") token = setup.create_upstream_oidc_token(name="名字", groups=["admin"]) setup.set_oidc_token_response("some-code", token) setup.set_oidc_configuration_response(setup.config.issuer.keypair) return_url = "https://example.com/foo" - r = await setup.client.get("/login", params={"rd": return_url}) + r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Simulate the return from the OpenID Connect provider. - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 assert r.headers["Location"] == return_url # Check that the name as returned from the user-info API is correct. - r = await setup.client.get("/auth/api/v1/user-info") + r = await client.get("/auth/api/v1/user-info") assert r.status_code == 200 assert r.json() == { "username": token.username, diff --git a/tests/handlers/logout_test.py b/tests/handlers/logout_test.py index b5c524acc..ae55e7d70 100644 --- a/tests/handlers/logout_test.py +++ b/tests/handlers/logout_test.py @@ -12,22 +12,25 @@ if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_logout(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_logout( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: token_data = await setup.create_session_token(scopes=["read:all"]) - await setup.login(token_data.token) + await setup.login(client, token_data.token) # Confirm that we're logged in. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 # Go to /logout without specifying a redirect URL. caplog.clear() - r = await setup.client.get("/logout") + r = await client.get("/logout") # Check the redirect and logging. assert r.status_code == 307 @@ -43,36 +46,36 @@ async def test_logout(setup: SetupTest, caplog: LogCaptureFixture) -> None: ] # Confirm that we're no longer logged in. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 401 @pytest.mark.asyncio -async def test_logout_with_url(setup: SetupTest) -> None: +async def test_logout_with_url(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token(scopes=["read:all"]) - await setup.login(token_data.token) + await setup.login(client, token_data.token) # Confirm that we're logged in. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 200 # Go to /logout with a redirect URL and check the redirect. redirect_url = "https://example.com:4444/logged-out" - r = await setup.client.get("/logout", params={"rd": redirect_url}) + r = await client.get("/logout", params={"rd": redirect_url}) assert r.status_code == 307 assert r.headers["Location"] == redirect_url # Confirm that we're no longer logged in. - r = await setup.client.get("/auth", params={"scope": "read:all"}) + r = await client.get("/auth", params={"scope": "read:all"}) assert r.status_code == 401 @pytest.mark.asyncio async def test_logout_not_logged_in( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: caplog.clear() - r = await setup.client.get("/logout") + r = await client.get("/logout") assert r.status_code == 307 assert r.headers["Location"] == setup.config.after_logout_url @@ -88,10 +91,8 @@ async def test_logout_not_logged_in( @pytest.mark.asyncio -async def test_logout_bad_url(setup: SetupTest) -> None: - r = await setup.client.get( - "/logout", params={"rd": "https://foo.example.com/"} - ) +async def test_logout_bad_url(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/logout", params={"rd": "https://foo.example.com/"}) assert r.status_code == 422 assert r.json() == { "detail": [ @@ -106,7 +107,7 @@ async def test_logout_bad_url(setup: SetupTest) -> None: @pytest.mark.asyncio async def test_logout_github( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: assert setup.config.github user_info = GitHubUserInfo( @@ -121,15 +122,15 @@ async def test_logout_github( # Log in and log out. setup.set_github_response("some-code", user_info, expect_revoke=True) - r = await setup.client.get("/login", params={"rd": "https://example.com"}) + r = await client.get("/login", params={"rd": "https://example.com"}) assert r.status_code == 307 query = query_from_url(r.headers["Location"]) - r = await setup.client.get( + r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) assert r.status_code == 307 caplog.clear() - r = await setup.client.get("/logout") + r = await client.get("/logout") # Check the redirect and logging. assert r.status_code == 307 diff --git a/tests/handlers/oidc_test.py b/tests/handlers/oidc_test.py index 8b1951271..81aa7d338 100644 --- a/tests/handlers/oidc_test.py +++ b/tests/handlers/oidc_test.py @@ -23,21 +23,24 @@ from typing import Dict from _pytest.logging import LogCaptureFixture + from httpx import AsyncClient from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_login( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] await setup.configure(oidc_clients=clients) token_data = await setup.create_session_token() - await setup.login(token_data.token) + await setup.login(client, token_data.token) return_url = f"https://{TEST_HOSTNAME}:4444/foo?a=bar&b=baz" # Log in caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/openid/login", params={ "response_type": "code", @@ -79,7 +82,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: # Redeem the code for a token and check the result. caplog.clear() - r = await setup.client.post( + r = await client.post( "/auth/openid/token", data={ "grant_type": "authorization_code", @@ -140,7 +143,7 @@ async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio async def test_unauthenticated( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] await setup.configure(oidc_clients=clients) @@ -154,7 +157,7 @@ async def test_unauthenticated( } caplog.clear() - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 307 url = urlparse(r.headers["Location"]) @@ -179,20 +182,20 @@ async def test_unauthenticated( @pytest.mark.asyncio async def test_login_errors( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] await setup.configure(oidc_clients=clients) token_data = await setup.create_session_token() - await setup.login(token_data.token) + await setup.login(client, token_data.token) # No parameters at all. - r = await setup.client.get("/auth/openid/login") + r = await client.get("/auth/openid/login") assert r.status_code == 422 # Good client ID but missing redirect_uri. login_params = {"client_id": "some-id"} - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 422 # Bad client ID. @@ -201,7 +204,7 @@ async def test_login_errors( "client_id": "bad-client", "redirect_uri": f"https://{TEST_HOSTNAME}/", } - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 400 assert "Unknown client_id bad-client" in r.text @@ -224,14 +227,14 @@ async def test_login_errors( # Bad redirect_uri. login_params["client_id"] = "some-id" login_params["redirect_uri"] = "https://foo.example.com/" - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 422 assert "URL is not at" in r.text # Valid redirect_uri but missing response_type. login_params["redirect_uri"] = f"https://{TEST_HOSTNAME}/app" caplog.clear() - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 307 url = urlparse(r.headers["Location"]) assert url.scheme == "https" @@ -262,7 +265,7 @@ async def test_login_errors( # Invalid response_type. login_params["response_type"] = "bogus" - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 307 assert query_from_url(r.headers["Location"]) == { "error": ["invalid_request"], @@ -271,7 +274,7 @@ async def test_login_errors( # Valid response_type but missing scope. login_params["response_type"] = "code" - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 307 assert query_from_url(r.headers["Location"]) == { "error": ["invalid_request"], @@ -280,7 +283,7 @@ async def test_login_errors( # Invalid scope. login_params["scope"] = "user:email" - r = await setup.client.get("/auth/openid/login", params=login_params) + r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 307 assert query_from_url(r.headers["Location"]) == { "error": ["invalid_request"], @@ -290,7 +293,7 @@ async def test_login_errors( @pytest.mark.asyncio async def test_token_errors( - setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture ) -> None: clients = [ OIDCClient(client_id="some-id", client_secret="some-secret"), @@ -306,7 +309,7 @@ async def test_token_errors( # Missing parameters. request: Dict[str, str] = {} caplog.clear() - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_request", @@ -332,7 +335,7 @@ async def test_token_errors( "redirect_uri": f"https://{TEST_HOSTNAME}/", } caplog.clear() - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "unsupported_grant_type", @@ -352,7 +355,7 @@ async def test_token_errors( # Invalid code. request["grant_type"] = "authorization_code" - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -362,7 +365,7 @@ async def test_token_errors( # No client_secret. request["code"] = str(OIDCAuthorizationCode()) caplog.clear() - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", @@ -382,7 +385,7 @@ async def test_token_errors( # Incorrect client_id. request["client_secret"] = "other-secret" - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", @@ -391,7 +394,7 @@ async def test_token_errors( # Incorrect client_secret. request["client_id"] = "some-id" - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", @@ -403,7 +406,7 @@ async def test_token_errors( bogus_code = OIDCAuthorizationCode() request["code"] = str(bogus_code) caplog.clear() - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -415,7 +418,7 @@ async def test_token_errors( # Corrupt stored data. await setup.redis.set(bogus_code.key, "XXXXXXX") - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -425,7 +428,7 @@ async def test_token_errors( # Correct code, but invalid client_id for that code. bogus_code = await oidc_service.issue_code("other-id", redirect_uri, token) request["code"] = str(bogus_code) - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -434,7 +437,7 @@ async def test_token_errors( # Correct code and client_id but invalid redirect_uri. request["code"] = str(code) - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -447,7 +450,7 @@ async def test_token_errors( token.key, token_data, token_data.username, ip_address="127.0.0.1" ) request["redirect_uri"] = redirect_uri - r = await setup.client.post("/auth/openid/token", data=request) + r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", @@ -456,12 +459,12 @@ async def test_token_errors( @pytest.mark.asyncio -async def test_userinfo(setup: SetupTest) -> None: +async def test_userinfo(client: AsyncClient, setup: SetupTest) -> None: token_data = await setup.create_session_token() issuer = setup.factory.create_token_issuer() oidc_token = issuer.issue_token(token_data, jti="some-jti") - r = await setup.client.get( + r = await client.get( "/auth/userinfo", headers={"Authorization": f"Bearer {oidc_token.encoded}"}, ) @@ -471,8 +474,8 @@ async def test_userinfo(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_auth(setup: SetupTest) -> None: - r = await setup.client.get("/auth/userinfo") +async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/auth/userinfo") assert r.status_code == 401 authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) @@ -482,13 +485,15 @@ async def test_no_auth(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid(setup: SetupTest, caplog: LogCaptureFixture) -> None: +async def test_invalid( + client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture +) -> None: token_data = await setup.create_session_token() issuer = setup.factory.create_token_issuer() oidc_token = issuer.issue_token(token_data, jti="some-jti") caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/userinfo", headers={"Authorization": f"token {oidc_token.encoded}"}, ) @@ -512,7 +517,7 @@ async def test_invalid(setup: SetupTest, caplog: LogCaptureFixture) -> None: } ] - r = await setup.client.get( + r = await client.get( "/auth/userinfo", headers={"Authorization": f"bearer{oidc_token.encoded}"}, ) @@ -526,7 +531,7 @@ async def test_invalid(setup: SetupTest, caplog: LogCaptureFixture) -> None: assert authenticate.error_description == "Malformed Authorization header" caplog.clear() - r = await setup.client.get( + r = await client.get( "/auth/userinfo", headers={"Authorization": f"bearer XXX{oidc_token.encoded}"}, ) @@ -553,8 +558,8 @@ async def test_invalid(setup: SetupTest, caplog: LogCaptureFixture) -> None: @pytest.mark.asyncio -async def test_well_known_jwks(setup: SetupTest) -> None: - r = await setup.client.get("/.well-known/jwks.json") +async def test_well_known_jwks(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/.well-known/jwks.json") assert r.status_code == 200 result = r.json() @@ -579,8 +584,8 @@ async def test_well_known_jwks(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_well_known_oidc(setup: SetupTest) -> None: - r = await setup.client.get("/.well-known/openid-configuration") +async def test_well_known_oidc(client: AsyncClient, setup: SetupTest) -> None: + r = await client.get("/.well-known/openid-configuration") assert r.status_code == 200 base_url = setup.config.issuer.iss diff --git a/tests/support/setup.py b/tests/support/setup.py index 21457278d..a6a266e57 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -9,7 +9,6 @@ import respx import structlog -from asgi_lifespan import LifespanManager from httpx import AsyncClient from safir.dependencies.http_client import http_client_dependency from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine @@ -20,7 +19,6 @@ from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory -from gafaelfawr.main import app from gafaelfawr.models.state import State from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo from tests.support.constants import TEST_HOSTNAME @@ -138,20 +136,16 @@ async def create( # separate or requests for routes that are also served by the app will # bypass the mock and call the app instead, causing tests to fail. try: - async with LifespanManager(app): - base_url = f"https://{TEST_HOSTNAME}" - async with AsyncClient(app=app, base_url=base_url) as client: - async with AsyncClient() as http_client: - async with session_factory() as session: - yield cls( - tmp_path=tmp_path, - respx_mock=respx_mock, - config=config, - redis=redis, - session=session, - client=client, - http_client=http_client, - ) + async with AsyncClient() as http_client: + async with session_factory() as session: + yield cls( + tmp_path=tmp_path, + respx_mock=respx_mock, + config=config, + redis=redis, + session=session, + http_client=http_client, + ) finally: await http_client_dependency.aclose() if os.environ.get("REDIS_6379_TCP_PORT"): @@ -170,7 +164,6 @@ def __init__( config: Config, redis: Redis, session: AsyncSession, - client: AsyncClient, http_client: AsyncClient, ) -> None: self.tmp_path = tmp_path @@ -178,7 +171,6 @@ def __init__( self.config = config self.redis = redis self.session = session - self.client = client self.http_client = http_client self.logger = structlog.get_logger(config.safir.logger_name) assert self.logger @@ -311,7 +303,7 @@ def create_upstream_oidc_token( self.config, kid, groups=groups, **claims ) - async def login(self, token: Token) -> str: + async def login(self, client: AsyncClient, token: Token) -> str: """Create a valid Gafaelfawr session. Add a valid Gafaelfawr session cookie to the `httpx.AsyncClient`, use @@ -319,6 +311,8 @@ async def login(self, token: Token) -> str: Parameters ---------- + client : `httpx.AsyncClient` + The client to add the session cookie to. token : `gafaelfawr.models.token.Token` The token for the client identity to use. @@ -328,14 +322,20 @@ async def login(self, token: Token) -> str: The CSRF token to use in subsequent API requests. """ cookie = await State(token=token).as_cookie() - self.client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) - r = await self.client.get("/auth/api/v1/login") + client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) + r = await client.get("/auth/api/v1/login") assert r.status_code == 200 return r.json()["csrf"] - def logout(self) -> None: - """Delete the Gafaelfawr session token.""" - del self.client.cookies[COOKIE_NAME] + def logout(self, client: AsyncClient) -> None: + """Delete the Gafaelfawr session token. + + Parameters + ---------- + client : `httpx.AsyncClient` + The client from which to remove the session cookie. + """ + del client.cookies[COOKIE_NAME] def set_github_response( self, From d720b93b64e5ec88ca9a7848aff36d3da7a6c401 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 13:50:42 -0800 Subject: [PATCH 07/24] Update JavaScript dependencies Includes updates of Gatsby and eslint-config-airbnb. The latter required a bunch of mechanical changes to the JavaScript source. --- .pre-commit-config.yaml | 14 +- ui/package-lock.json | 13161 ++++++++----------- ui/package.json | 14 +- ui/src/components/createTokenButton.js | 10 +- ui/src/components/editTokenModal.js | 6 +- ui/src/components/layout.js | 23 +- ui/src/components/timestamp.js | 6 +- ui/src/components/token.js | 6 +- ui/src/components/tokenChangeHistory.js | 6 +- ui/src/components/tokenChangeSearch.js | 6 +- ui/src/components/tokenChangeSearchForm.js | 6 +- ui/src/components/tokenChangeTable.js | 6 +- ui/src/components/tokenData.js | 6 +- ui/src/components/tokenForm.js | 6 +- ui/src/components/tokenList.js | 6 +- ui/src/components/tokenModal.js | 6 +- ui/src/components/tokenName.js | 6 +- ui/src/components/tokenTable.js | 14 +- ui/src/pages/changes.js | 6 +- ui/src/pages/id/[token].js | 6 +- ui/src/pages/index.js | 6 +- 21 files changed, 5872 insertions(+), 7454 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ff0f3b9a..584757928 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,16 +43,16 @@ repos: hooks: - id: eslint additional_dependencies: - - '@babel/eslint-parser@7.16.0' + - '@babel/eslint-parser@7.16.3' - '@babel/preset-react@7.16.0' - - eslint@8.2.0 - - eslint-config-airbnb@18.2.1 + - eslint@8.3.0 + - eslint-config-airbnb@19.0.1 - eslint-config-prettier@8.3.0 - eslint-config-wesbos@2.1.0 - eslint-plugin-html@6.2.0 - - eslint-plugin-import@2.25.2 - - eslint-plugin-jsx-a11y@6.4.1 + - eslint-plugin-import@2.25.3 + - eslint-plugin-jsx-a11y@6.5.1 - eslint-plugin-prettier@4.0.0 - - eslint-plugin-react@7.26.1 - - eslint-plugin-react-hooks@4.2.0 + - eslint-plugin-react@7.27.1 + - eslint-plugin-react-hooks@4.3.0 - prettier@2.4.1 diff --git a/ui/package-lock.json b/ui/package-lock.json index 64dd9972f..74853e0e4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,9 +9,9 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "date-fns": "^2.25.0", + "date-fns": "^2.26.0", "formik": "^2.2.9", - "gatsby": "^4.1.6", + "gatsby": "^4.2.0", "gatsby-plugin-use-query-params": "^1.0.1", "prop-types": "^15.7.2", "react": "^17.0.2", @@ -28,15 +28,15 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.16.3", - "eslint": "^8.2.0", - "eslint-config-airbnb": "^18.2.1", + "eslint": "^8.3.0", + "eslint-config-airbnb": "^19.0.1", "eslint-config-prettier": "^8.3.0", "eslint-config-wesbos": "^2.1.0", "eslint-plugin-html": "^6.2.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.3.0", "prettier": "^2.4.1" } @@ -69,9 +69,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.0.tgz", - "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", "engines": { "node": ">=6.9.0" } @@ -105,6 +105,14 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@babel/eslint-parser": { "version": "7.16.3", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.3.tgz", @@ -135,6 +143,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz", @@ -159,13 +175,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.0.tgz", - "integrity": "sha512-S7iaOT1SYlqK0sQaCi21RX4+13hmdmnxIEAnQUB/eh7GeAnRjOUgTYpLkUOiRXzD+yog1JxP0qyAQZ7ZxVxLVg==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz", + "integrity": "sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==", "dependencies": { "@babel/compat-data": "^7.16.0", "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", + "browserslist": "^4.17.5", "semver": "^6.3.0" }, "engines": { @@ -210,9 +226,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.4.tgz", - "integrity": "sha512-OrpPZ97s+aPi6h2n1OXzdhVis1SGSsMU2aMHgLcOKfsp4/v1NWpx3CWT3lBj5eeBq9cDkPkh+YCfdF7O12uNDQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.0.tgz", + "integrity": "sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==", "dependencies": { "@babel/helper-compilation-targets": "^7.13.0", "@babel/helper-module-imports": "^7.12.13", @@ -333,9 +349,9 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.0.tgz", - "integrity": "sha512-MLM1IOMe9aQBqMWxcRw8dcb9jlM86NIw7KA0Wri91Xkfied+dE0QuBFSBjMNvqzmS0OSIDsMNC24dBEkPUi7ew==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz", + "integrity": "sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.0", "@babel/helper-wrap-function": "^7.16.0", @@ -423,12 +439,12 @@ } }, "node_modules/@babel/helpers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.0.tgz", - "integrity": "sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.3.tgz", + "integrity": "sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w==", "dependencies": { "@babel/template": "^7.16.0", - "@babel/traverse": "^7.16.0", + "@babel/traverse": "^7.16.3", "@babel/types": "^7.16.0" }, "engines": { @@ -448,10 +464,74 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.2.tgz", - "integrity": "sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", "bin": { "parser": "bin/babel-parser.js" }, @@ -490,12 +570,12 @@ } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.0.tgz", - "integrity": "sha512-nyYmIo7ZqKsY6P4lnVmBlxp9B3a96CscbLotlsNuktMHahkDwoPYEjXrZHU0Tj844Z9f1IthVxQln57mhkcExw==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz", + "integrity": "sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg==", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.16.0", + "@babel/helper-remap-async-to-generator": "^7.16.4", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -991,6 +1071,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.0.tgz", @@ -1347,15 +1435,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.0.tgz", - "integrity": "sha512-zlPf1/XFn5+vWdve3AAhf+Sxl+MVa5VlwTwWgnLx23u4GlatSRQJ3Eoo9vllf0a9il3woQsT4SK+5Z7c06h8ag==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.4.tgz", + "integrity": "sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A==", "dependencies": { "@babel/helper-module-imports": "^7.16.0", "@babel/helper-plugin-utils": "^7.14.5", - "babel-plugin-polyfill-corejs2": "^0.2.3", - "babel-plugin-polyfill-corejs3": "^0.3.0", - "babel-plugin-polyfill-regenerator": "^0.2.3", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.4.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", "semver": "^6.3.0" }, "engines": { @@ -1482,17 +1570,17 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.0.tgz", - "integrity": "sha512-cdTu/W0IrviamtnZiTfixPfIncr2M1VqRrkjzZWlr1B4TVYimCFK5jkyOdP4qw2MrlKHi+b3ORj6x8GoCew8Dg==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.4.tgz", + "integrity": "sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==", "dependencies": { - "@babel/compat-data": "^7.16.0", - "@babel/helper-compilation-targets": "^7.16.0", + "@babel/compat-data": "^7.16.4", + "@babel/helper-compilation-targets": "^7.16.3", "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.2", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-async-generator-functions": "^7.16.0", + "@babel/plugin-proposal-async-generator-functions": "^7.16.4", "@babel/plugin-proposal-class-properties": "^7.16.0", "@babel/plugin-proposal-class-static-block": "^7.16.0", "@babel/plugin-proposal-dynamic-import": "^7.16.0", @@ -1542,7 +1630,7 @@ "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.0", "@babel/plugin-transform-new-target": "^7.16.0", "@babel/plugin-transform-object-super": "^7.16.0", - "@babel/plugin-transform-parameters": "^7.16.0", + "@babel/plugin-transform-parameters": "^7.16.3", "@babel/plugin-transform-property-literals": "^7.16.0", "@babel/plugin-transform-regenerator": "^7.16.0", "@babel/plugin-transform-reserved-words": "^7.16.0", @@ -1555,10 +1643,10 @@ "@babel/plugin-transform-unicode-regex": "^7.16.0", "@babel/preset-modules": "^0.1.5", "@babel/types": "^7.16.0", - "babel-plugin-polyfill-corejs2": "^0.2.3", - "babel-plugin-polyfill-corejs3": "^0.3.0", - "babel-plugin-polyfill-regenerator": "^0.2.3", - "core-js-compat": "^3.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.4.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.19.1", "semver": "^6.3.0" }, "engines": { @@ -1630,9 +1718,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.0.tgz", - "integrity": "sha512-Oi2qwQ21X7/d9gn3WiwkDTJmq3TQtYNz89lRnoFy8VeZpWlsyXvzSwiRrRZ8cXluvSwqKxqHJ6dBd9Rv+p0ZGQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.3.tgz", + "integrity": "sha512-IAdDC7T0+wEB4y2gbIL0uOXEYpiZEeuFUTVbdGq+UwCcF35T/tS8KrmMomEwEc5wBbyfH3PJVpTSUqrhPDXFcQ==", "dependencies": { "core-js-pure": "^3.19.0", "regenerator-runtime": "^0.13.4" @@ -1642,9 +1730,9 @@ } }, "node_modules/@babel/standalone": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.16.3.tgz", - "integrity": "sha512-hoStDfHl2+EYUv1LNHhZTysa+lMSwIEkkT4HnDNX+F0zqvPdoE2QLF7qtkd45cgCGOwQjrvwe2mOKcX3f6Wr8A==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.16.4.tgz", + "integrity": "sha512-FDRLwjeQfPm5jaHNuB+vwNyGCp24Ah3kEsbLzKmh0eSru+QCr4DmjgbRPoz71AwXLVtXU+l/i7MlVlIj5XO7Gw==", "engines": { "node": ">=6.9.0" } @@ -1663,16 +1751,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.0.tgz", - "integrity": "sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.3.tgz", + "integrity": "sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==", "dependencies": { "@babel/code-frame": "^7.16.0", "@babel/generator": "^7.16.0", "@babel/helper-function-name": "^7.16.0", "@babel/helper-hoist-variables": "^7.16.0", "@babel/helper-split-export-declaration": "^7.16.0", - "@babel/parser": "^7.16.0", + "@babel/parser": "^7.16.3", "@babel/types": "^7.16.0", "debug": "^4.1.0", "globals": "^11.1.0" @@ -1681,6 +1769,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz", @@ -1742,6 +1838,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1757,39 +1854,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@gatsbyjs/reach-router": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@gatsbyjs/reach-router/-/reach-router-1.3.6.tgz", @@ -1958,6 +2022,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@graphql-tools/load/node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "engines": { + "node": ">= 4" + } + }, "node_modules/@graphql-tools/load/node_modules/is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -2158,6 +2230,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", + "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", @@ -2191,20 +2264,6 @@ "node": ">= 8.3" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@jest/types/node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -2217,41 +2276,6 @@ "node": ">=8" } }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@mdx-js/util": { "version": "2.0.0-next.8", "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-2.0.0-next.8.tgz", @@ -2349,18 +2373,10 @@ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/@popperjs/core": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", - "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz", + "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2415,17 +2431,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@sindresorhus/transliterate": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", @@ -2539,9 +2544,9 @@ "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==" }, "node_modules/@types/eslint": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz", - "integrity": "sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2642,9 +2647,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.176", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", - "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==" + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" }, "node_modules/@types/mdast": { "version": "3.0.10", @@ -2673,9 +2678,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==" }, "node_modules/@types/node-fetch": { "version": "2.5.12", @@ -2705,9 +2710,9 @@ } }, "node_modules/@types/react": { - "version": "17.0.34", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz", - "integrity": "sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==", + "version": "17.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", + "integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2803,27 +2808,12 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "dependencies": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" + "node": ">= 4" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { @@ -2856,6 +2846,29 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "dependencies": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", @@ -3152,9 +3165,9 @@ } }, "node_modules/acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "bin": { "acorn": "bin/acorn" }, @@ -3275,14 +3288,17 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { @@ -3334,7 +3350,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "4.2.2", @@ -3607,12 +3624,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.3.tgz", - "integrity": "sha512-NDZ0auNRzmAfE1oDDPW2JhzIMXUk+FFe2ICejmt5T4ocKgiQx3e0VCRx9NCAidcMtL2RUZaWtXnmjTCkx0tcbA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", + "integrity": "sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==", "dependencies": { "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.2.4", + "@babel/helper-define-polyfill-provider": "^0.3.0", "semver": "^6.1.1" }, "peerDependencies": { @@ -3620,11 +3637,11 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.3.0.tgz", - "integrity": "sha512-JLwi9vloVdXLjzACL80j24bG6/T1gYxwowG44dg6HN/7aTPdyPbJJidf6ajoA3RPHHtW0j9KMrSOLpIZpAnPpg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz", + "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.2.4", + "@babel/helper-define-polyfill-provider": "^0.3.0", "core-js-compat": "^3.18.0" }, "peerDependencies": { @@ -3632,23 +3649,23 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.3.tgz", - "integrity": "sha512-JVE78oRZPKFIeUqFGrSORNzQnrDwZR16oiWeGM8ZyjBn2XAT5OjP+wXx5ESuo33nUsFUEJYjtklnsKbxW5L+7g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz", + "integrity": "sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.2.4" + "@babel/helper-define-polyfill-provider": "^0.3.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-remove-graphql-queries": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.1.3.tgz", - "integrity": "sha512-84LUlWYfw+aQjUNsd81DwjdOPpQI9/EnxL7smdkFH1WjwfIhAaJYW+tNN4eZLurhexI5mYdHIJQ3kD6hpzwkNw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.2.0.tgz", + "integrity": "sha512-9WLJRInT+cr76wGT75FHf5qIVoFuq83fvgzWEidSYS7M6BXM/A8JK7vxuhom2aDwYmxWn+tXNQGYKp3BEIvV1Q==", "dependencies": { "@babel/runtime": "^7.15.4", - "gatsby-core-utils": "^3.1.3" + "gatsby-core-utils": "^3.2.0" }, "engines": { "node": ">=14.15.0" @@ -3659,12 +3676,12 @@ } }, "node_modules/babel-plugin-styled-components": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.3.tgz", - "integrity": "sha512-meGStRGv+VuKA/q0/jXxrPNWEm4LPfYIqxooDTdmh8kFsP/Ph7jJG5rUPwUPX3QHUvggwdbgdGpo88P/rRYsVw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", + "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.15.4", - "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", "babel-plugin-syntax-jsx": "^6.18.0", "lodash": "^4.17.11" }, @@ -3683,9 +3700,9 @@ "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" }, "node_modules/babel-preset-gatsby": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-gatsby/-/babel-preset-gatsby-2.1.3.tgz", - "integrity": "sha512-O9yqDlzPrcY/Q1AsoMMbWcbt1cjc7rLP8Ga04/jxukPGI1GqK6FPFv8LVuPPI2lNKK2RLfhIoaaVgvcB5P2ZFw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-gatsby/-/babel-preset-gatsby-2.2.0.tgz", + "integrity": "sha512-SFlilnVwNFVEwbgz0vsLe3ckCN6PW0XUvSNJlnzkuClKSx9BcPItNXRQtpYKkVHA4W1XZX5Qm9NaIOflP2i9tw==", "dependencies": { "@babel/plugin-proposal-class-properties": "^7.14.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", @@ -3700,8 +3717,8 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-macros": "^2.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", - "gatsby-core-utils": "^3.1.3", - "gatsby-legacy-polyfills": "^2.1.0" + "gatsby-core-utils": "^3.2.0", + "gatsby-legacy-polyfills": "^2.2.0" }, "engines": { "node": ">=14.15.0" @@ -3958,81 +3975,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/boxen/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/boxen/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4054,12 +3996,12 @@ } }, "node_modules/browserslist": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.6.tgz", - "integrity": "sha512-uPgz3vyRTlEiCv4ee9KlsKgo2V6qPk7Jsn0KAn2OBqbqKo3iNcPEC1Ti6J4dwnz+aIRfEEEuOzC9IBk8tXUomw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", + "integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", "dependencies": { - "caniuse-lite": "^1.0.30001274", - "electron-to-chromium": "^1.3.886", + "caniuse-lite": "^1.0.30001280", + "electron-to-chromium": "^1.3.896", "escalade": "^3.1.1", "node-releases": "^2.0.1", "picocolors": "^1.0.0" @@ -4253,9 +4195,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001278", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001278.tgz", - "integrity": "sha512-mpF9KeH8u5cMoEmIic/cr7PNS+F5LWBk0t2ekGT60lFf0Wq+n9LspAj0g3P+o7DQhD3sUdlMln4YFAWhFYn9jg==", + "version": "1.0.30001283", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz", + "integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/browserslist" @@ -4271,16 +4213,18 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/character-entities": { @@ -4637,36 +4581,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -4723,17 +4637,20 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/colord": { "version": "2.9.1", @@ -4770,9 +4687,9 @@ } }, "node_modules/common-tags": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.1.tgz", - "integrity": "sha512-uOZd85rJqrdEIE/JjhW5YAeatX8iqjjvVzIyfx7JL7G5r9Tep6YpYT9gEJWhWpVyDQEyzukWd6p2qULpJ8tmBw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "engines": { "node": ">=4.0.0" } @@ -4939,9 +4856,9 @@ } }, "node_modules/contentful-management/node_modules/type-fest": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.3.tgz", - "integrity": "sha512-7VNmE7FlsrdcWjKbtuRuynZz96Gmf35p5DvoR2tbceNP0vd58ISx87PvUUInlhtRC49vSX6qlxEKc7AoiHRirg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.6.0.tgz", + "integrity": "sha512-XN1FDGGtaSDA6CFsCW5iolTQqFsnJ+ZF6JqSz0SqXoh4F8GY0xqUv5RYnTilpmL+sOH8OH4FX8tf9YyAPM2LDA==", "engines": { "node": ">=12.20" }, @@ -5016,9 +4933,9 @@ } }, "node_modules/core-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.1.tgz", - "integrity": "sha512-Tnc7E9iKd/b/ff7GFbhwPVzJzPztGrChB8X8GLqoYGdEOG8IpLnK1xPyo3ZoO3HsK6TodJS58VGPOxA+hLHQMg==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.2.tgz", + "integrity": "sha512-ciYCResnLIATSsXuXnIOH4CbdfgV+H1Ltg16hJFN7/v6OxqnFr/IFGeLacaZ+fHLAm0TBbXwNK9/DNBzBUrO/g==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5026,11 +4943,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.19.1.tgz", - "integrity": "sha512-Q/VJ7jAF/y68+aUsQJ/afPOewdsGkDtcMb40J8MbuWKlK3Y+wtHq8bTHKPj2WKWLIqmS5JhHs4CzHtz6pT2W6g==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.19.2.tgz", + "integrity": "sha512-ObBY1W5vx/LFFMaL1P5Udo4Npib6fu+cMokeziWkA8Tns4FcDemKF5j9JvaI5JhdkW8EQJQGJN1EcrzmEwuAqQ==", "dependencies": { - "browserslist": "^4.17.6", + "browserslist": "^4.18.1", "semver": "7.0.0" }, "funding": { @@ -5047,9 +4964,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.19.1.tgz", - "integrity": "sha512-Q0Knr8Es84vtv62ei6/6jXH/7izKmOrtrxH9WJTHLCMAVeU+8TF8z8Nr08CsH4Ot0oJKzBzJJL9SJBYIv7WlfQ==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.19.2.tgz", + "integrity": "sha512-5LkcgQEy8pFeVnd/zomkUBSwnmIxuF1C8E9KrMAbOc8f34IBT9RGvTYeNDdp1PnvMJrrVhvk1hg/yVV5h/znlg==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5097,9 +5014,9 @@ } }, "node_modules/create-gatsby": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/create-gatsby/-/create-gatsby-2.1.1.tgz", - "integrity": "sha512-TyTrJD5Wa2WTa5PVY3ng1aIo7vqyR3rmQJuGRNPNeRZIfrYBrMGILWIh8TV+pofTKiZsRhPPMmHlpDD62tqPcw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/create-gatsby/-/create-gatsby-2.2.0.tgz", + "integrity": "sha512-nQ3t2+qpSnepqxFeBrkL6os5TR2TN4Nc1cCX/3YCWzbMQ7etc54Yjsw/PRFBUFtbt9RJk/7CURtJKFHDNsHtZw==", "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -5157,14 +5074,6 @@ "node": ">=4" } }, - "node_modules/css-color-names": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", - "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", - "engines": { - "node": "*" - } - }, "node_modules/css-declaration-sorter": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz", @@ -5401,11 +5310,11 @@ "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" }, "node_modules/cssnano": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.10.tgz", - "integrity": "sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz", + "integrity": "sha512-U38V4x2iJ3ijPdeWqUrEr4eKBB5PbEKsNP5T8xcik2Au3LeMtiMHX0i2Hu9k51FcKofNZumbrcdC6+a521IUHg==", "dependencies": { - "cssnano-preset-default": "^5.1.6", + "cssnano-preset-default": "^5.1.8", "is-resolvable": "^1.1.0", "lilconfig": "^2.0.3", "yaml": "^1.10.2" @@ -5422,9 +5331,9 @@ } }, "node_modules/cssnano-preset-default": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz", - "integrity": "sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.8.tgz", + "integrity": "sha512-zWMlP0+AMPBVE852SqTrP0DnhTcTA2C1wAF92TKZ3Va+aUVqLIhkqKlnJIXXdqXD7RN+S1ujuWmNpvrJBiM/vg==", "dependencies": { "css-declaration-sorter": "^6.0.3", "cssnano-utils": "^2.0.1", @@ -5435,11 +5344,11 @@ "postcss-discard-duplicates": "^5.0.1", "postcss-discard-empty": "^5.0.1", "postcss-discard-overridden": "^5.0.1", - "postcss-merge-longhand": "^5.0.3", - "postcss-merge-rules": "^5.0.2", + "postcss-merge-longhand": "^5.0.4", + "postcss-merge-rules": "^5.0.3", "postcss-minify-font-values": "^5.0.1", "postcss-minify-gradients": "^5.0.3", - "postcss-minify-params": "^5.0.1", + "postcss-minify-params": "^5.0.2", "postcss-minify-selectors": "^5.1.0", "postcss-normalize-charset": "^5.0.1", "postcss-normalize-display-values": "^5.0.1", @@ -5448,13 +5357,13 @@ "postcss-normalize-string": "^5.0.1", "postcss-normalize-timing-functions": "^5.0.1", "postcss-normalize-unicode": "^5.0.1", - "postcss-normalize-url": "^5.0.2", + "postcss-normalize-url": "^5.0.3", "postcss-normalize-whitespace": "^5.0.1", "postcss-ordered-values": "^5.0.2", - "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-initial": "^5.0.2", "postcss-reduce-transforms": "^5.0.1", "postcss-svgo": "^5.0.3", - "postcss-unique-selectors": "^5.0.1" + "postcss-unique-selectors": "^5.0.2" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -5486,9 +5395,9 @@ } }, "node_modules/csstype": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" }, "node_modules/d": { "version": "1.0.1", @@ -5510,9 +5419,9 @@ "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==" }, "node_modules/date-fns": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz", - "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==", + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.26.0.tgz", + "integrity": "sha512-VQI812dRi3cusdY/fhoBKvc6l2W8BPWU1FNVnFH9Nttjx4AFBRzfSVb/Eyc7jBT6e9sg1XtAGsYpBQ6c/jygbg==", "engines": { "node": ">=0.11" }, @@ -5522,9 +5431,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { "ms": "2.1.2" }, @@ -5545,6 +5454,18 @@ "node": ">=0.10.0" } }, + "node_modules/decode-named-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.0.tgz", + "integrity": "sha512-KTiXDlRp9MMm/nlgI8rDGKoNNKiTJBl0RPjnBM680m2HlgJEA4JTASspK44lsvE4GQJildMRFp2HdEBiG+nqng==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -5675,6 +5596,14 @@ "node": ">=8" } }, + "node_modules/del/node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "engines": { + "node": ">= 4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6002,9 +5931,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.3.891", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.891.tgz", - "integrity": "sha512-3cpwR82QkIS01CN/dup/4Yr3BiOiRLlZlcAFn/5FbNCunMO9ojqDgEP9JEo1QNLflu3pEnPWve50gHOEKc7r6w==" + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.4.tgz", + "integrity": "sha512-teHtgwcmVcL46jlFvAaqjyiTLWuMrUQO1JqV303JKB4ysXG6m8fXSFhbjal9st0r9mNskI22AraJZorb1VcLVg==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -6316,17 +6245,21 @@ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "dev": true, "dependencies": { "@eslint/eslintrc": "^1.0.4", "@humanwhocodes/config-array": "^0.6.0", @@ -6337,10 +6270,10 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^6.0.0", + "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6378,372 +6311,491 @@ } }, "node_modules/eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.1.tgz", + "integrity": "sha512-ooV8Vdt3gmV6hCSJutRw5beArd56Cd++vvy7j0Y7DBhUKqcTyI7cWNcJpCJlcftGPRLtzdU0hNMtquOUSSm8nA==", "dev": true, "dependencies": { - "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", - "object.entries": "^1.1.2" + "object.entries": "^1.1.5" }, "engines": { - "node": ">= 6" + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.21.5", - "eslint-plugin-react-hooks": "^4 || ^3 || ^2.3.0 || ^1.7.0" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0" } }, - "node_modules/eslint-config-airbnb-typescript": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-14.0.2.tgz", - "integrity": "sha512-oaVR63DqpRUiOOeSVxIzhD3FXbqJRH+7Lt9GCMsS9SKgrRW3XpZINN2FO4JEsnaHEGkktumd0AHE9K7KQNuXSQ==", + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, - "peer": true, "dependencies": { - "eslint-config-airbnb-base": "^14.2.1" + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.29.3", - "@typescript-eslint/parser": "^4.29.3" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "node_modules/eslint-config-prettier": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", + "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", "dev": true, - "peer": true, - "dependencies": { - "@babel/highlight": "^7.10.4" + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "peer": true, + "node_modules/eslint-config-react-app": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", + "integrity": "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" + "confusing-browser-globals": "^1.0.10" }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0", + "@typescript-eslint/parser": "^4.0.0", + "babel-eslint": "^10.0.0", + "eslint": "^7.5.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^24.0.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8", + "eslint-plugin-testing-library": "^3.9.0" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + }, + "eslint-plugin-testing-library": { + "optional": true + } } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "node_modules/eslint-config-wesbos": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-wesbos/-/eslint-config-wesbos-2.1.0.tgz", + "integrity": "sha512-qwuEFecMmfRoaV/7niB7StdaLwUQEWv9Oz67G5rQsiEm84ACC3mgMBe2NXeV5l2LHUuCiyHWKMWFpmWvPicnhQ==", "dev": true, - "peer": true, + "peerDependencies": { + "@babel/core": "^7.15.8", + "@babel/eslint-parser": "^7.15.4", + "@babel/preset-react": "^7.14.5", + "@types/node": "^16.7.13", + "@typescript-eslint/eslint-plugin": "^4.31.0", + "@typescript-eslint/parser": "^4.31.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-airbnb-typescript": "^14.0.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-html": "^6.1.2", + "eslint-plugin-import": "^2.24.2", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.25.1", + "eslint-plugin-react-hooks": "^4.2.0", + "prettier": "^2.3.2", + "typescript": "^4.4.2" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" + "debug": "^3.2.7", + "resolve": "^1.20.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "peer": true, + "node_modules/eslint-module-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", + "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", "dependencies": { - "color-convert": "^2.0.1" + "debug": "^3.2.7", + "find-up": "^2.1.0", + "pkg-dir": "^2.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dependencies": { - "sprintf-js": "~1.0.2" + "ms": "^2.1.1" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-flowtype": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", + "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "lodash": "^4.17.15", + "string-natural-compare": "^3.0.1" }, "engines": { - "node": ">=10" + "node": "^10.12.0 || >=12.0.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "eslint": "^7.1.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-graphql": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-4.0.0.tgz", + "integrity": "sha512-d5tQm24YkVvCEk29ZR5ScsgXqAGCjKlMS8lx3mS7FS/EKsWbkvXQImpvic03EpMIvNTBW5e+2xnHzXB/VHNZJw==", "dependencies": { - "color-name": "~1.1.4" + "@babel/runtime": "^7.10.0", + "graphql-config": "^3.0.2", + "lodash.flatten": "^4.4.0", + "lodash.without": "^4.4.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=10.0" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/eslint-plugin-html": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-6.2.0.tgz", + "integrity": "sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g==", "dev": true, - "peer": true + "dependencies": { + "htmlparser2": "^7.1.2" + } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-import": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", + "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.1", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.11.0" + }, "engines": { - "node": ">=10" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=0.10.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", + "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" + "@babel/runtime": "^7.16.3", + "aria-query": "^4.2.2", + "array-includes": "^3.1.4", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.3.5", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.7", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.2.1", + "language-tags": "^1.0.5", + "minimatch": "^3.0.4" }, "engines": { - "node": ">= 6" + "node": ">=4.0" }, "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint-plugin-prettier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", + "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", "dev": true, - "peer": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "prettier-linter-helpers": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flatmap": "^1.2.5", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.0", + "object.values": "^1.1.5", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.6" + }, "engines": { "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "esutils": "^2.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, "engines": { - "node": ">=4" + "node": ">=8.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "peer": true, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dependencies": { - "is-glob": "^4.0.1" + "eslint-visitor-keys": "^2.0.0" }, "engines": { - "node": ">= 6" + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "dev": true, - "peer": true, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.6.0.tgz", + "integrity": "sha512-V+LPY/T3kur5QO3u+1s34VDTcRxjXWPUGM4hlmTb5DwVD0OQz631yGTxJZf4SpAqAjdbBVe978S8BJeHpAdOhQ==", "dependencies": { - "type-fest": "^0.20.2" + "@types/eslint": "^7.28.2", + "arrify": "^2.0.1", + "jest-worker": "^27.3.1", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1" }, "engines": { - "node": ">=8" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "peer": true, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.0.tgz", + "integrity": "sha512-4WuKcUxtzxBoKOUFbt1MtTY9fJwPVD4aN/4Cgxee7OLetPZn5as2bjfZz98XSf2Zq1JFfhqPZpS+43BmWXKgCA==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, "engines": { - "node": ">=8" + "node": ">= 10.13.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "peer": true, + "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, "engines": { - "node": ">= 4" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/eslint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, - "peer": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/lru-cache": { + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6751,12 +6803,11 @@ "node": ">=10" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/semver": { + "node_modules/eslint/node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, - "peer": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -6767,186 +6818,190 @@ "node": ">=10" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/eslint/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/espree": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, - "peer": true, "dependencies": { - "has-flag": "^4.0.0" + "acorn": "^8.6.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", "dev": true, - "peer": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint-config-airbnb-typescript/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/eslint-config-airbnb/node_modules/eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1" + "node": ">=0.10" } }, - "node_modules/eslint-config-prettier": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "engines": { + "node": ">=4.0" } }, - "node_modules/eslint-config-wesbos": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-wesbos/-/eslint-config-wesbos-2.1.0.tgz", - "integrity": "sha512-qwuEFecMmfRoaV/7niB7StdaLwUQEWv9Oz67G5rQsiEm84ACC3mgMBe2NXeV5l2LHUuCiyHWKMWFpmWvPicnhQ==", - "dev": true, - "peerDependencies": { - "@babel/core": "^7.15.8", - "@babel/eslint-parser": "^7.15.4", - "@babel/preset-react": "^7.14.5", - "@types/node": "^16.7.13", - "@typescript-eslint/eslint-plugin": "^4.31.0", - "@typescript-eslint/parser": "^4.31.0", - "eslint": "^7.32.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-airbnb-typescript": "^14.0.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-html": "^6.1.2", - "eslint-plugin-import": "^2.24.2", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.25.1", - "eslint-plugin-react-hooks": "^4.2.0", - "prettier": "^2.3.2", - "typescript": "^4.4.2" + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" + "node_modules/estree-util-is-identifier-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.0.0.tgz", + "integrity": "sha512-aXXZFVMnBBDRP81vS4YtAYJ0hUkgEsXea7lNKWCOeaAquGb1Jm2rcONPB5fpzwgbNxulTvrWuKnp9UElUGAKeQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/estree-util-visit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.1.0.tgz", + "integrity": "sha512-3lXJ4Us9j8TUif9cWcQy81t9p5OLasnDuuhrFiqb+XstmKC1d1LmrQWYsY49/9URcfHE64mPypDBaNK9NwWDPQ==", "dependencies": { - "ms": "^2.1.1" + "@types/estree-jsx": "^0.0.1", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-module-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", - "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0", - "pkg-dir": "^2.0.0" - }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" } }, - "node_modules/eslint-plugin-graphql": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-4.0.0.tgz", - "integrity": "sha512-d5tQm24YkVvCEk29ZR5ScsgXqAGCjKlMS8lx3mS7FS/EKsWbkvXQImpvic03EpMIvNTBW5e+2xnHzXB/VHNZJw==", + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "dependencies": { - "@babel/runtime": "^7.10.0", - "graphql-config": "^3.0.2", - "lodash.flatten": "^4.4.0", - "lodash.without": "^4.4.0" - }, + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/event-source-polyfill": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz", + "integrity": "sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "engines": { - "node": ">=10.0" - }, - "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + "node": ">=6" } }, - "node_modules/eslint-plugin-html": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-6.2.0.tgz", - "integrity": "sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g==", - "dev": true, - "dependencies": { - "htmlparser2": "^7.1.2" + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" } }, - "node_modules/eslint-plugin-import": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", - "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.1", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.11.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", @@ -6954,1809 +7009,1700 @@ "ms": "2.0.0" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { - "esutils": "^2.0.2" + "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", - "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { - "@babel/runtime": "^7.16.3", - "aria-query": "^4.2.2", - "array-includes": "^3.1.4", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.3.5", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.7", - "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.2.1", - "language-tags": "^1.0.5", - "minimatch": "^3.0.4" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dependencies": { + "kind-of": "^3.0.2" }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.0.tgz", - "integrity": "sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==", + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } + "node_modules/expand-brackets/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dependencies": { - "esutils": "^2.0.2" + "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-buffer": "^1.1.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=0.10.0" } }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" + "node": ">=0.10.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "engines": { - "node": ">=10" - } + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "dependencies": { - "color-convert": "^2.0.1" + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 0.10.0" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/express-graphql": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz", + "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "accepts": "^1.3.7", + "content-type": "^1.0.4", + "http-errors": "1.8.0", + "raw-body": "^2.4.1" }, "engines": { - "node": ">=10" + "node": ">= 10.x" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "graphql": "^14.7.0 || ^15.3.0" } }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/express-graphql/node_modules/bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-graphql/node_modules/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", "dependencies": { - "color-name": "~1.1.4" + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.6" } }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" + "node_modules/express-graphql/node_modules/raw-body": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", + "dependencies": { + "bytes": "3.1.1", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.8" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", - "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", + "node_modules/express-graphql/node_modules/raw-body/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 0.6" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "node_modules/express-graphql/node_modules/raw-body/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=0.6" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/express-graphql/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "engines": { - "node": ">= 4" + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/eslint/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/ext": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "type": "^2.5.0" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "node_modules/ext/node_modules/type": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dependencies": { - "has-flag": "^4.0.0" + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" }, "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/eslint/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/espree": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", - "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dependencies": { - "acorn": "^8.5.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.0.0" + "os-tmpdir": "~1.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=0.6.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dependencies": { - "estraverse": "^5.1.0" + "is-descriptor": "^1.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { - "estraverse": "^5.2.0" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.0.0.tgz", - "integrity": "sha512-aXXZFVMnBBDRP81vS4YtAYJ0hUkgEsXea7lNKWCOeaAquGb1Jm2rcONPB5fpzwgbNxulTvrWuKnp9UElUGAKeQ==", + "node_modules/extract-files": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", + "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", + "engines": { + "node": "^10.17.0 || ^12.0.0 || >= 13.7.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/jaydenseric" } }, - "node_modules/estree-util-visit": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.1.0.tgz", - "integrity": "sha512-3lXJ4Us9j8TUif9cWcQy81t9p5OLasnDuuhrFiqb+XstmKC1d1LmrQWYsY49/9URcfHE64mPypDBaNK9NwWDPQ==", + "node_modules/fast-copy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-2.1.1.tgz", + "integrity": "sha512-Qod3DdRgFZ8GUIM6ygeoZYpQ0QLW9cf/FS9KhhjlYggcSZXWAemAw8BOCO5LuYCrR3Uj3qXDVTUzOUwG8C7beQ==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dependencies": { - "@types/estree-jsx": "^0.0.1", - "@types/unist": "^2.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">= 6" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/event-source-polyfill": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz", - "integrity": "sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==" + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/fd": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/fd/-/fd-0.0.3.tgz", + "integrity": "sha512-iAHrIslQb3U68OcMSP0kkNWabp7sSN6d2TBSb2JO3gcLJVDd4owr/hKM4SFJovFOUeeXeItjYgouEDTMWiVAnA==" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "escape-string-regexp": "^1.0.5" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">=0.8.0" } }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dependencies": { - "is-descriptor": "^0.1.0" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=0.10.0" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", "dependencies": { - "is-extendable": "^0.1.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", "dependencies": { - "kind-of": "^3.0.2" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.9.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dependencies": { - "is-buffer": "^1.1.5" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/expand-brackets/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "node_modules/file-type": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz", + "integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==", "dependencies": { - "kind-of": "^3.0.2" + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dependencies": { - "is-buffer": "^1.1.5" - }, + "node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4.0" } }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", "engines": { "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/expand-brackets/node_modules/ms": { + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "node_modules/express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, - "node_modules/express-graphql": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz", - "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==", + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dependencies": { - "accepts": "^1.3.7", - "content-type": "^1.0.4", - "http-errors": "1.8.0", - "raw-body": "^2.4.1" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 10.x" - }, - "peerDependencies": { - "graphql": "^14.7.0 || ^15.3.0" + "node": ">=8" } }, - "node_modules/express-graphql/node_modules/http-errors": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", - "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/express-graphql/node_modules/raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.3", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express-graphql/node_modules/raw-body/node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "p-limit": "^2.2.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/express-graphql/node_modules/raw-body/node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/express-graphql/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/express/node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dependencies": { - "type": "^2.5.0" + "node": ">=8" } }, - "node_modules/ext/node_modules/type": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "find-up": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "locate-path": "^2.0.0" }, "engines": { "node": ">=4" } }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dependencies": { - "os-tmpdir": "~1.0.2" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=0.6.0" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" + }, + "node_modules/focus-trap": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.7.1.tgz", + "integrity": "sha512-a6czHbT9twVpy2RpkWQA9vIgwQgB9Nx1PIxNNUxQT4nugG/3QibwxO+tWTh9i+zSY2SFiX4pnYhTaFaQF/6ZAg==", "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "tabbable": "^5.2.1" } }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "node_modules/focus-trap-react": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-8.8.2.tgz", + "integrity": "sha512-YgacIMxeAOytHOEbzBWL7+itBkn4ARMwQhtt6hYVHqHzPUPhmfEyKJ/nqsyMerzOK1DzlDv8Q8phRAY8vpa0rA==", "dependencies": { - "is-descriptor": "^1.0.0" + "focus-trap": "^6.7.1" }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "prop-types": "^15.7.2", + "react": ">=16.0.0", + "react-dom": ">=16.0.0" } }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dependencies": { - "is-extendable": "^0.1.0" - }, + "node_modules/follow-redirects": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", + "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { - "node": ">=0.10.0" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "engines": { "node": ">=0.10.0" } }, - "node_modules/extract-files": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", - "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", - "engines": { - "node": "^10.17.0 || ^12.0.0 || >= 13.7.0" - }, - "funding": { - "url": "https://github.com/sponsors/jaydenseric" - } - }, - "node_modules/fast-copy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-2.1.1.tgz", - "integrity": "sha512-Qod3DdRgFZ8GUIM6ygeoZYpQ0QLW9cf/FS9KhhjlYggcSZXWAemAw8BOCO5LuYCrR3Uj3qXDVTUzOUwG8C7beQ==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", + "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "@babel/code-frame": "^7.5.5", + "chalk": "^2.4.1", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.11.5", + "yarn": ">=1.0.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { - "is-glob": "^4.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" - }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dependencies": { - "reusify": "^1.0.4" + "node": ">=4" } }, - "node_modules/fd": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/fd/-/fd-0.0.3.tgz", - "integrity": "sha512-iAHrIslQb3U68OcMSP0kkNWabp7sSN6d2TBSb2JO3gcLJVDd4owr/hKM4SFJovFOUeeXeItjYgouEDTMWiVAnA==" - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dependencies": { - "escape-string-regexp": "^1.0.5" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { - "flat-cache": "^3.0.4" + "is-extendable": "^0.1.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "node": ">=4" } }, - "node_modules/file-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, + "color-name": "1.1.3" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "engines": { - "node": ">=8.9.0" + "node": ">=0.8.0" } }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=0.10.0" } }, - "node_modules/file-type": { - "version": "16.5.3", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz", - "integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/filesize": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "engines": { - "node": ">= 0.4.0" + "node": ">=4" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "engines": { "node": ">=0.10.0" } }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { - "ms": "2.0.0" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/find-cache-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/find-cache-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dependencies": { - "p-locate": "^4.1.0" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/find-cache-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dependencies": { - "p-try": "^2.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, - "node_modules/find-cache-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/formik": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz", + "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], "dependencies": { - "p-limit": "^2.2.0" + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/find-cache-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/fraction.js": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz", + "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==", "engines": { - "node": ">=8" + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" } }, - "node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dependencies": { - "find-up": "^4.0.0" + "map-cache": "^0.2.2" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dependencies": { - "locate-path": "^2.0.0" - }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "node_modules/fs-exists-cached": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz", + "integrity": "sha1-zyVVTKBQ3EmuZla0HeQiWJidy84=" + }, + "node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12" } }, - "node_modules/flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==" + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" }, - "node_modules/focus-trap": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.7.1.tgz", - "integrity": "sha512-a6czHbT9twVpy2RpkWQA9vIgwQgB9Nx1PIxNNUxQT4nugG/3QibwxO+tWTh9i+zSY2SFiX4pnYhTaFaQF/6ZAg==", - "dependencies": { - "tabbable": "^5.2.1" - } + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "node_modules/follow-redirects": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" ], "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "engines": { - "node": ">=0.10.0" - } + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", - "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "node_modules/gatsby": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby/-/gatsby-4.2.0.tgz", + "integrity": "sha512-VOLlviKLmfdlts/idCwtRvzUjkOo4WzDMdlh5thq/ai/5sBdVObOcrKnlcZin7n/MGl78t3k4tLS3UjybLhhIw==", + "hasInstallScript": true, "dependencies": { - "@babel/code-frame": "^7.5.5", - "chalk": "^2.4.1", - "micromatch": "^3.1.10", - "minimatch": "^3.0.4", - "semver": "^5.6.0", - "tapable": "^1.0.0", - "worker-rpc": "^0.1.0" + "@babel/code-frame": "^7.14.0", + "@babel/core": "^7.15.5", + "@babel/eslint-parser": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/parser": "^7.15.5", + "@babel/runtime": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4", + "@gatsbyjs/reach-router": "^1.3.6", + "@gatsbyjs/webpack-hot-middleware": "^2.25.2", + "@nodelib/fs.walk": "^1.2.8", + "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", + "@types/http-proxy": "^1.17.7", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.0", + "address": "1.1.2", + "anser": "^2.1.0", + "autoprefixer": "^10.4.0", + "axios": "^0.21.1", + "babel-loader": "^8.2.3", + "babel-plugin-add-module-exports": "^1.0.4", + "babel-plugin-dynamic-import-node": "^2.3.3", + "babel-plugin-lodash": "^3.3.4", + "babel-plugin-remove-graphql-queries": "^4.2.0", + "babel-preset-gatsby": "^2.2.0", + "better-opn": "^2.1.1", + "bluebird": "^3.7.2", + "body-parser": "^1.19.0", + "browserslist": "^4.17.5", + "cache-manager": "^2.11.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "common-tags": "^1.8.0", + "compression": "^1.7.4", + "cookie": "^0.4.1", + "core-js": "^3.17.2", + "cors": "^2.8.5", + "css-loader": "^5.2.7", + "css-minimizer-webpack-plugin": "^2.0.0", + "css.escape": "^1.5.1", + "date-fns": "^2.25.0", + "debug": "^3.2.7", + "deepmerge": "^4.2.2", + "del": "^5.1.0", + "detect-port": "^1.3.0", + "devcert": "^1.2.0", + "dotenv": "^8.6.0", + "eslint": "^7.32.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.10.0", + "eslint-plugin-graphql": "^4.0.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.26.1", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-webpack-plugin": "^2.5.4", + "event-source-polyfill": "^1.0.25", + "execa": "^5.1.1", + "express": "^4.17.1", + "express-graphql": "^0.12.0", + "fastest-levenshtein": "^1.0.12", + "fastq": "^1.13.0", + "file-loader": "^6.2.0", + "find-cache-dir": "^3.3.2", + "fs-exists-cached": "1.0.0", + "fs-extra": "^10.0.0", + "gatsby-cli": "^4.2.0", + "gatsby-core-utils": "^3.2.0", + "gatsby-graphiql-explorer": "^2.2.0", + "gatsby-legacy-polyfills": "^2.2.0", + "gatsby-link": "^4.2.0", + "gatsby-plugin-page-creator": "^4.2.0", + "gatsby-plugin-typescript": "^4.2.0", + "gatsby-plugin-utils": "^2.2.0", + "gatsby-react-router-scroll": "^5.2.0", + "gatsby-telemetry": "^3.2.0", + "gatsby-worker": "^1.2.0", + "glob": "^7.2.0", + "got": "^11.8.2", + "graphql": "^15.7.2", + "graphql-compose": "~7.25.1", + "graphql-playground-middleware-express": "^1.7.22", + "hasha": "^5.2.2", + "http-proxy": "^1.18.1", + "invariant": "^2.2.4", + "is-relative": "^1.0.0", + "is-relative-url": "^3.0.0", + "joi": "^17.4.2", + "json-loader": "^0.5.7", + "latest-version": "5.1.0", + "lmdb-store": "^1.6.11", + "lodash": "^4.17.21", + "md5-file": "^5.0.0", + "meant": "^1.0.3", + "memoizee": "^0.4.15", + "micromatch": "^4.0.4", + "mime": "^2.5.2", + "mini-css-extract-plugin": "1.6.2", + "mitt": "^1.2.0", + "moment": "^2.29.1", + "multer": "^1.4.3", + "node-fetch": "^2.6.6", + "normalize-path": "^3.0.0", + "null-loader": "^4.0.1", + "opentracing": "^0.14.5", + "p-defer": "^3.0.0", + "parseurl": "^1.3.3", + "physical-cpu-count": "^2.0.0", + "platform": "^1.3.6", + "postcss": "^8.3.11", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^5.3.0", + "prompts": "^2.4.2", + "prop-types": "^15.7.2", + "query-string": "^6.14.1", + "raw-loader": "^4.0.2", + "react-dev-utils": "^11.0.4", + "react-refresh": "^0.9.0", + "redux": "4.1.2", + "redux-thunk": "^2.4.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.5", + "shallow-compare": "^1.2.2", + "signal-exit": "^3.0.5", + "slugify": "^1.6.1", + "socket.io": "3.1.2", + "socket.io-client": "3.1.3", + "source-map": "^0.7.3", + "source-map-support": "^0.5.20", + "st": "^2.0.0", + "stack-trace": "^0.0.10", + "string-similarity": "^1.2.2", + "strip-ansi": "^5.2.0", + "style-loader": "^2.0.0", + "terser-webpack-plugin": "^5.2.4", + "tmp": "^0.2.1", + "true-case-path": "^2.2.1", + "type-of": "^2.0.1", + "url-loader": "^4.1.1", + "uuid": "^8.3.2", + "v8-compile-cache": "^2.3.0", + "webpack": "^5.61.0", + "webpack-dev-middleware": "^4.3.0", + "webpack-merge": "^5.8.0", + "webpack-stats-plugin": "^1.0.3", + "webpack-virtual-modules": "^0.3.2", + "xstate": "^4.26.0", + "yaml-loader": "^0.6.0" + }, + "bin": { + "gatsby": "cli.js" }, "engines": { - "node": ">=6.11.5", - "yarn": ">=1.0.0" + "node": ">=14.15.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/gatsby-cli": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-cli/-/gatsby-cli-4.2.0.tgz", + "integrity": "sha512-WC8sIdMpzTSsLcbvrvrfYazq1exTM+lZBoibLTxCBqKVcQ3dNMCSbzIbGieLtKaPs4pOKKvkivOSWEfPqMOdug==", + "hasInstallScript": true, "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "@babel/code-frame": "^7.14.0", + "@babel/runtime": "^7.15.4", + "@types/common-tags": "^1.8.1", + "better-opn": "^2.1.1", + "boxen": "^5.1.2", + "chalk": "^4.1.2", + "clipboardy": "^2.3.0", + "common-tags": "^1.8.0", + "configstore": "^5.0.1", + "convert-hrtime": "^3.0.0", + "create-gatsby": "^2.2.0", + "envinfo": "^7.8.1", + "execa": "^5.1.1", + "fs-exists-cached": "^1.0.0", + "fs-extra": "^10.0.0", + "gatsby-core-utils": "^3.2.0", + "gatsby-recipes": "^1.2.0", + "gatsby-telemetry": "^3.2.0", + "hosted-git-info": "^3.0.8", + "is-valid-path": "^0.1.1", + "joi": "^17.4.2", + "lodash": "^4.17.21", + "meant": "^1.0.3", + "node-fetch": "^2.6.6", + "opentracing": "^0.14.5", + "pretty-error": "^2.1.2", + "progress": "^2.0.3", + "prompts": "^2.4.2", + "redux": "4.1.2", + "resolve-cwd": "^3.0.0", + "semver": "^7.3.5", + "signal-exit": "^3.0.5", + "source-map": "0.7.3", + "stack-trace": "^0.0.10", + "strip-ansi": "^5.2.0", + "update-notifier": "^5.1.0", + "uuid": "3.4.0", + "yargs": "^15.4.1", + "yoga-layout-prebuilt": "^1.10.0", + "yurnalist": "^2.1.0" + }, + "bin": { + "gatsby": "cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.15.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dependencies": { - "is-extendable": "^0.1.0" - }, + "node_modules/gatsby-cli/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/gatsby-cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "yallist": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/gatsby-cli/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dependencies": { - "is-extendable": "^0.1.0" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "node_modules/gatsby-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "node_modules/gatsby-cli/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/gatsby-cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/gatsby-core-utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-3.2.0.tgz", + "integrity": "sha512-gPz02QD1kOGQmu49TZL8Fdo9rX8QBsA7XID0oXyIkZqkK80Tm1Uq1pOOfPE3cWSMEkzc71M79iKISCntk/wNuw==", "dependencies": { - "is-buffer": "^1.1.5" + "@babel/runtime": "^7.15.4", + "ci-info": "2.0.0", + "configstore": "^5.0.1", + "file-type": "^16.5.3", + "fs-extra": "^10.0.0", + "got": "^11.8.2", + "node-object-hash": "^2.3.10", + "proper-lockfile": "^4.1.2", + "tmp": "^0.2.1", + "xdg-basedir": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.15.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/gatsby-graphiql-explorer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.2.0.tgz", + "integrity": "sha512-K8LCrG4d9eFhyyHs4AKT1TKTm1Sw2zyk211TuKcTMwic3Qq0ldDUl75GCEMbMFxxyaqiNNjjhEx6ayySZ3ewJA==", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "@babel/runtime": "^7.15.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.15.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" + "node_modules/gatsby-legacy-polyfills": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.2.0.tgz", + "integrity": "sha512-4WICOoxdsfjfVK369m/fjcTvneUC0noTvdFwWNSItfKnCau4MGNPyXJRP74xgfOIC/ST3KasO6XFdTnGdlWy0A==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "core-js-compat": "3.9.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "node_modules/gatsby-legacy-polyfills/node_modules/core-js-compat": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.9.0.tgz", + "integrity": "sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ==", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "browserslist": "^4.16.3", + "semver": "7.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "node_modules/gatsby-legacy-polyfills/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/gatsby-link": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-link/-/gatsby-link-4.2.0.tgz", + "integrity": "sha512-R3I+rlkVcgDMEOv8MJGi9HdH4CSHyk7qQ9bZ2/HmtGLwEdhNbOlL48PGsCTq9MjGeq+XtWS4/R298TFr0ot1lQ==", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "@babel/runtime": "^7.15.4", + "@types/reach__router": "^1.3.9", + "prop-types": "^15.7.2" }, "engines": { - "node": ">= 6" + "node": ">=14.15.0" + }, + "peerDependencies": { + "@gatsbyjs/reach-router": "^1.3.5", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0" } }, - "node_modules/formik": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz", - "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==", - "funding": [ - { - "type": "individual", - "url": "https://opencollective.com/formik" - } - ], + "node_modules/gatsby-page-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-page-utils/-/gatsby-page-utils-2.2.0.tgz", + "integrity": "sha512-Q+RzAXTtyehPEFNNry+0iQuifOGZ/LrEljfIulJu9UZ+DXHzTI3KL/jugQ/UTHdyK2kIv0kssL7WM49ije7wcA==", "dependencies": { - "deepmerge": "^2.1.1", - "hoist-non-react-statics": "^3.3.0", + "@babel/runtime": "^7.15.4", + "bluebird": "^3.7.2", + "chokidar": "^3.5.2", + "fs-exists-cached": "^1.0.0", + "gatsby-core-utils": "^3.2.0", + "glob": "^7.2.0", "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "react-fast-compare": "^2.0.1", - "tiny-warning": "^1.0.2", - "tslib": "^1.10.0" + "micromatch": "^4.0.4" }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "engines": { - "node": ">= 0.6" + "node": ">=14.15.0" } }, - "node_modules/fraction.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", - "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "node_modules/gatsby-plugin-page-creator": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.2.0.tgz", + "integrity": "sha512-jezKI/ct5gRCPe9VZWxsdA69/IqcPBTbCLZMCPRXEg7PMoCzxmEZEKiA7mFZugRTu4TH8xdMiyvg3uaSdPLBYg==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@sindresorhus/slugify": "^1.1.2", + "chokidar": "^3.5.2", + "fs-exists-cached": "^1.0.0", + "gatsby-core-utils": "^3.2.0", + "gatsby-page-utils": "^2.2.0", + "gatsby-plugin-utils": "^2.2.0", + "gatsby-telemetry": "^3.2.0", + "globby": "^11.0.4", + "lodash": "^4.17.21" + }, "engines": { - "node": "*" + "node": ">=14.15.0" }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" + "peerDependencies": { + "gatsby": "^4.0.0-next" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "node_modules/gatsby-plugin-typescript": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.2.0.tgz", + "integrity": "sha512-nQB0FS8vpo206iy/BTisfeyv8MZcXFarEL94tJPQ88AyC8kRCM7m4yCM4uknDGnTfYApqf1GHbDKn6SeCimtog==", "dependencies": { - "map-cache": "^0.2.2" + "@babel/core": "^7.15.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/preset-typescript": "^7.15.0", + "@babel/runtime": "^7.15.4", + "babel-plugin-remove-graphql-queries": "^4.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.15.0" + }, + "peerDependencies": { + "gatsby": "^4.0.0-next" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "engines": { - "node": ">= 0.6" + "node_modules/gatsby-plugin-use-query-params": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gatsby-plugin-use-query-params/-/gatsby-plugin-use-query-params-1.0.1.tgz", + "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==", + "peerDependencies": { + "gatsby": ">=2.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "use-query-params": ">=0.4.3" } }, - "node_modules/fs-exists-cached": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz", - "integrity": "sha1-zyVVTKBQ3EmuZla0HeQiWJidy84=" - }, - "node_modules/fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "node_modules/gatsby-plugin-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-utils/-/gatsby-plugin-utils-2.2.0.tgz", + "integrity": "sha512-VcrvYk6LxsTs7DqzNCT7iYaqEoMuKr2gXO5HFG1sBMwS6XxLWUe47+CuenOvUcKn71C9a/Ir2pove62+0Xphjw==", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@babel/runtime": "^7.15.4", + "joi": "^17.4.2" }, "engines": { - "node": ">=12" + "node": ">=14.15.0" + }, + "peerDependencies": { + "gatsby": "^4.0.0-next" } }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/gatsby-react-router-scroll": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.2.0.tgz", + "integrity": "sha512-iKzlCgmbOrWVr/5O4bZVLSXg12ZQSQLAvr+2HN/SGKFlF/cObb/znmu/oSssD4A6c3SN8XKv3d0XNUiADio2+Q==", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14.15.0" + }, + "peerDependencies": { + "@gatsbyjs/reach-router": "^1.3.5", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "node_modules/gatsby": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/gatsby/-/gatsby-4.1.6.tgz", - "integrity": "sha512-cXgP3k9Jxw4yv7lKWt/F5RRMPbJREI/180zHYrfgT+KlOm9VQ0TImRzBq4XJBKQVGcYUxD9+QKY75e81onPNwg==", - "hasInstallScript": true, + "node_modules/gatsby-recipes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gatsby-recipes/-/gatsby-recipes-1.2.0.tgz", + "integrity": "sha512-v76Kt1EYBHwf5c9Ic8b0w/eEaDaRh3B/6spAVU6zN+VzEvQX3Oi/VckUyS2/anBOuSnOl3PJVWhFA3/aZpGuxw==", "dependencies": { - "@babel/code-frame": "^7.14.0", "@babel/core": "^7.15.5", - "@babel/eslint-parser": "^7.15.4", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/parser": "^7.15.5", + "@babel/generator": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.0", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", "@babel/runtime": "^7.15.4", - "@babel/traverse": "^7.15.4", + "@babel/standalone": "^7.15.5", + "@babel/template": "^7.15.4", "@babel/types": "^7.15.4", - "@gatsbyjs/reach-router": "^1.3.6", - "@gatsbyjs/webpack-hot-middleware": "^2.25.2", - "@nodelib/fs.walk": "^1.2.8", - "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@types/http-proxy": "^1.17.7", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "@vercel/webpack-asset-relocator-loader": "^1.7.0", - "address": "1.1.2", - "anser": "^2.0.2", - "autoprefixer": "^10.3.7", - "axios": "^0.21.1", - "babel-loader": "^8.2.2", - "babel-plugin-add-module-exports": "^1.0.4", - "babel-plugin-dynamic-import-node": "^2.3.3", - "babel-plugin-lodash": "^3.3.4", - "babel-plugin-remove-graphql-queries": "^4.1.3", - "babel-preset-gatsby": "^2.1.3", - "better-opn": "^2.1.1", - "bluebird": "^3.7.2", - "body-parser": "^1.19.0", - "browserslist": "^4.17.3", - "cache-manager": "^2.11.1", - "chalk": "^4.1.2", + "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/utils": "^7.0.2", + "@hapi/hoek": "8.x.x", + "@hapi/joi": "^15.1.1", + "better-queue": "^3.8.10", "chokidar": "^3.5.2", - "common-tags": "^1.8.0", - "compression": "^1.7.4", - "cookie": "^0.4.1", - "core-js": "^3.17.2", + "contentful-management": "^7.5.1", "cors": "^2.8.5", - "css-loader": "^5.2.7", - "css-minimizer-webpack-plugin": "^2.0.0", - "css.escape": "^1.5.1", - "date-fns": "^2.25.0", - "debug": "^3.2.7", - "deepmerge": "^4.2.2", - "del": "^5.1.0", + "debug": "^4.3.1", "detect-port": "^1.3.0", - "devcert": "^1.2.0", - "dotenv": "^8.6.0", - "eslint": "^7.32.0", - "eslint-config-react-app": "^6.0.0", - "eslint-plugin-flowtype": "^5.10.0", - "eslint-plugin-graphql": "^4.0.0", - "eslint-plugin-import": "^2.24.2", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.26.1", - "eslint-plugin-react-hooks": "^4.2.0", - "eslint-webpack-plugin": "^2.5.4", - "event-source-polyfill": "^1.0.25", + "dotenv": "^8.2.0", "execa": "^5.1.1", "express": "^4.17.1", "express-graphql": "^0.12.0", - "fastest-levenshtein": "^1.0.12", - "fastq": "^1.13.0", - "file-loader": "^6.2.0", - "find-cache-dir": "^3.3.2", - "fs-exists-cached": "1.0.0", "fs-extra": "^10.0.0", - "gatsby-cli": "^4.1.4", - "gatsby-core-utils": "^3.1.3", - "gatsby-graphiql-explorer": "^2.1.0", - "gatsby-legacy-polyfills": "^2.1.0", - "gatsby-link": "^4.1.0", - "gatsby-plugin-page-creator": "^4.1.4", - "gatsby-plugin-typescript": "^4.1.3", - "gatsby-plugin-utils": "^2.1.1", - "gatsby-react-router-scroll": "^5.1.0", - "gatsby-telemetry": "^3.1.3", - "gatsby-worker": "^1.1.0", - "glob": "^7.2.0", - "got": "^11.8.2", - "graphql": "^15.6.1", - "graphql-compose": "~7.25.1", - "graphql-playground-middleware-express": "^1.7.22", - "hasha": "^5.2.2", - "http-proxy": "^1.18.1", - "invariant": "^2.2.4", - "is-relative": "^1.0.0", - "is-relative-url": "^3.0.0", - "joi": "^17.4.2", - "json-loader": "^0.5.7", - "latest-version": "5.1.0", - "lmdb-store": "^1.6.8", + "gatsby-core-utils": "^3.2.0", + "gatsby-telemetry": "^3.2.0", + "glob": "^7.1.6", + "graphql": "^15.4.0", + "graphql-compose": "~7.25.0", + "graphql-subscriptions": "^1.1.0", + "graphql-type-json": "^0.3.2", + "hicat": "^0.8.0", + "is-binary-path": "^2.1.0", + "is-url": "^1.2.4", + "jest-diff": "^25.5.0", + "lock": "^1.0.0", "lodash": "^4.17.21", - "md5-file": "^5.0.0", - "meant": "^1.0.3", - "memoizee": "^0.4.15", - "micromatch": "^4.0.4", - "mime": "^2.5.2", - "mini-css-extract-plugin": "1.6.2", "mitt": "^1.2.0", - "moment": "^2.29.1", - "multer": "^1.4.3", - "node-fetch": "^2.6.5", - "normalize-path": "^3.0.0", - "null-loader": "^4.0.1", - "opentracing": "^0.14.5", - "p-defer": "^3.0.0", - "parseurl": "^1.3.3", - "physical-cpu-count": "^2.0.0", - "platform": "^1.3.6", - "postcss": "^8.3.9", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^5.3.0", - "prompts": "^2.4.2", - "prop-types": "^15.7.2", - "query-string": "^6.14.1", - "raw-loader": "^4.0.2", - "react-dev-utils": "^11.0.4", - "react-refresh": "^0.9.0", - "redux": "4.0.5", - "redux-thunk": "^2.3.0", + "mkdirp": "^0.5.1", + "node-fetch": "^2.5.0", + "pkg-dir": "^4.2.0", + "prettier": "^2.4.1", + "prop-types": "^15.6.1", + "remark-mdx": "^2.0.0-next.4", + "remark-mdxjs": "^2.0.0-next.4", + "remark-parse": "^6.0.3", + "remark-stringify": "^8.1.0", "resolve-from": "^5.0.0", "semver": "^7.3.5", - "shallow-compare": "^1.2.2", - "signal-exit": "^3.0.5", - "slugify": "^1.6.1", - "socket.io": "3.1.2", - "socket.io-client": "3.1.3", - "source-map": "^0.7.3", - "source-map-support": "^0.5.20", - "st": "^2.0.0", - "stack-trace": "^0.0.10", - "string-similarity": "^1.2.2", - "strip-ansi": "^5.2.0", - "style-loader": "^2.0.0", - "terser-webpack-plugin": "^5.2.4", - "tmp": "^0.2.1", - "true-case-path": "^2.2.1", - "type-of": "^2.0.1", - "url-loader": "^4.1.1", - "uuid": "^8.3.2", - "v8-compile-cache": "^2.3.0", - "webpack": "^5.58.1", - "webpack-dev-middleware": "^4.3.0", - "webpack-merge": "^5.8.0", - "webpack-stats-plugin": "^1.0.3", - "webpack-virtual-modules": "^0.3.2", - "xstate": "^4.25.0", - "yaml-loader": "^0.6.0" - }, - "bin": { - "gatsby": "cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0" - } - }, - "node_modules/gatsby-cli": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gatsby-cli/-/gatsby-cli-4.1.4.tgz", - "integrity": "sha512-B8PYO0FPZMuaEJnvLDPPfEO+xHci7pWoS0NnEKKd+12kneObi4doQp0vqqAG+PS3oNtBRGy1nLglf8BTpCex8Q==", - "hasInstallScript": true, - "dependencies": { - "@babel/code-frame": "^7.14.0", - "@babel/runtime": "^7.15.4", - "@types/common-tags": "^1.8.1", - "better-opn": "^2.1.1", - "boxen": "^5.1.2", - "chalk": "^4.1.2", - "clipboardy": "^2.3.0", - "common-tags": "^1.8.0", - "configstore": "^5.0.1", - "convert-hrtime": "^3.0.0", - "create-gatsby": "^2.1.1", - "envinfo": "^7.8.1", - "execa": "^5.1.1", - "fs-exists-cached": "^1.0.0", - "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-recipes": "^1.1.3", - "gatsby-telemetry": "^3.1.3", - "hosted-git-info": "^3.0.8", - "is-valid-path": "^0.1.1", - "joi": "^17.4.2", - "lodash": "^4.17.21", - "meant": "^1.0.3", - "node-fetch": "^2.6.5", - "opentracing": "^0.14.5", - "pretty-error": "^2.1.2", - "progress": "^2.0.3", - "prompts": "^2.4.1", - "redux": "4.0.5", - "resolve-cwd": "^3.0.0", - "semver": "^7.3.5", - "signal-exit": "^3.0.5", - "source-map": "0.7.3", - "stack-trace": "^0.0.10", - "strip-ansi": "^5.2.0", - "update-notifier": "^5.1.0", + "single-trailing-newline": "^1.0.0", + "strip-ansi": "^6.0.0", + "style-to-object": "^0.3.0", + "unified": "^8.4.2", + "unist-util-remove": "^2.0.0", + "unist-util-visit": "^2.0.2", "uuid": "3.4.0", - "yargs": "^15.4.1", - "yoga-layout-prebuilt": "^1.10.0", - "yurnalist": "^2.1.0" - }, - "bin": { - "gatsby": "cli.js" - }, - "engines": { - "node": ">=14.15.0" + "ws": "^7.3.0", + "xstate": "^4.9.1", + "yoga-layout-prebuilt": "^1.9.6" } }, - "node_modules/gatsby-cli/node_modules/ansi-regex": { + "node_modules/gatsby-recipes/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/gatsby-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/gatsby-recipes/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { - "color-convert": "^2.0.1" + "p-locate": "^4.1.0" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/gatsby-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/gatsby-recipes/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "yallist": "^4.0.0" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/gatsby-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" + "node_modules/gatsby-recipes/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "node_modules/gatsby-recipes/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/gatsby-cli/node_modules/has-flag": { + "node_modules/gatsby-recipes/node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "engines": { "node": ">=8" } }, - "node_modules/gatsby-cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/gatsby-recipes/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dependencies": { - "yallist": "^4.0.0" + "find-up": "^4.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/gatsby-cli/node_modules/redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "dependencies": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" + "node": ">=8" } }, - "node_modules/gatsby-cli/node_modules/semver": { + "node_modules/gatsby-recipes/node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", @@ -8770,37 +8716,7 @@ "node": ">=10" } }, - "node_modules/gatsby-cli/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/gatsby-cli/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/gatsby-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/gatsby-cli/node_modules/uuid": { + "node_modules/gatsby-recipes/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", @@ -8809,678 +8725,608 @@ "uuid": "bin/uuid" } }, - "node_modules/gatsby-cli/node_modules/yallist": { + "node_modules/gatsby-recipes/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/gatsby-core-utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-3.1.3.tgz", - "integrity": "sha512-+pg2i3DYVLzmJ/67SVGM+zVxerilGCbcgVxDoN58Y+Htv5TwogUWzPymfoFrJEsWGhlVKlYq7I8jVWSXPzwMHw==", + "node_modules/gatsby-telemetry": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-3.2.0.tgz", + "integrity": "sha512-nLs/PPTPn7xPOiJxRe1Lmd8C0EVaH4rPT3KRT36ftaJBVDT5XhKfhR/tW9zirADD1k6pYW6vYvAQNFfKG5dpDg==", + "hasInstallScript": true, "dependencies": { + "@babel/code-frame": "^7.14.0", "@babel/runtime": "^7.15.4", - "ci-info": "2.0.0", + "@turist/fetch": "^7.1.7", + "@turist/time": "^0.0.2", + "async-retry-ng": "^2.0.1", + "boxen": "^4.2.0", "configstore": "^5.0.1", - "file-type": "^16.5.3", "fs-extra": "^10.0.0", - "got": "^11.8.2", - "node-object-hash": "^2.3.9", - "proper-lockfile": "^4.1.2", - "tmp": "^0.2.1", - "xdg-basedir": "^4.0.0" + "gatsby-core-utils": "^3.2.0", + "git-up": "^4.0.5", + "is-docker": "^2.2.1", + "lodash": "^4.17.21", + "node-fetch": "^2.6.6" }, "engines": { "node": ">=14.15.0" } }, - "node_modules/gatsby-graphiql-explorer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.1.0.tgz", - "integrity": "sha512-Gxr5Vv+Cmcxts/699FPnvp6t+xjtYUZsXyrJ4HoWEA8VU0h1KFVy56CtuZX8oMErTXMOkoueX89ty7b3ktJERw==", + "node_modules/gatsby-telemetry/node_modules/boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", "dependencies": { - "@babel/runtime": "^7.15.4" + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" }, "engines": { - "node": ">=14.15.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby-legacy-polyfills": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.1.0.tgz", - "integrity": "sha512-clDAz0Iv18bLPxfmg14YiL5nt/pCBUqFQcpswSUatfSo6O/PR3L5G8gRJNhgCVdaGp24opcOvh8y+sZWKza5rA==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "core-js-compat": "3.9.0" + "node_modules/gatsby-telemetry/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" } }, - "node_modules/gatsby-legacy-polyfills/node_modules/core-js-compat": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.9.0.tgz", - "integrity": "sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ==", + "node_modules/gatsby-telemetry/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dependencies": { - "browserslist": "^4.16.3", - "semver": "7.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "engines": { + "node": ">=8" } }, - "node_modules/gatsby-legacy-polyfills/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "bin": { - "semver": "bin/semver.js" + "node_modules/gatsby-telemetry/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" } }, - "node_modules/gatsby-link": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/gatsby-link/-/gatsby-link-4.1.0.tgz", - "integrity": "sha512-igOvc/ks6XNwT4vpwViC3n7KthP16XlKWejvIZA8yLKIwkOszcgV5/PYMTri8e2C2xpUAeutTreBWfmRFNcWtw==", + "node_modules/gatsby-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gatsby-worker/-/gatsby-worker-1.2.0.tgz", + "integrity": "sha512-PxWs876LMSwu6gfZXqJiUFrIgnWPa8p4MysD9PhLd2+apaRsZJyMrPE3lblGQ62MehEAD5jpLDS0CpM6BaUCkA==", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/reach__router": "^1.3.9", - "prop-types": "^15.7.2" + "@babel/core": "^7.15.5", + "@babel/runtime": "^7.15.4" }, "engines": { "node": ">=14.15.0" - }, - "peerDependencies": { - "@gatsbyjs/reach-router": "^1.3.5", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0" } }, - "node_modules/gatsby-page-utils": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/gatsby-page-utils/-/gatsby-page-utils-2.1.3.tgz", - "integrity": "sha512-5CrSKsXXF6gk6ZxueyE1z1HHZ4I/+y4nEcYliTl7fpNG+dGIKgmuQIjJoigcvZGEVhn9gk1BTDjBFUuXGrSgNA==", + "node_modules/gatsby/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dependencies": { - "@babel/runtime": "^7.15.4", - "bluebird": "^3.7.2", - "chokidar": "^3.5.2", - "fs-exists-cached": "^1.0.0", - "gatsby-core-utils": "^3.1.3", - "glob": "^7.1.7", - "lodash": "^4.17.21", - "micromatch": "^4.0.4" + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=14.15.0" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/gatsby-plugin-page-creator": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.1.4.tgz", - "integrity": "sha512-MLs/C0Tc+uzRDmiWZyXdE5plDG/DeRnCgsd4iXBkeClzdVyToJSQWNlA30knMAS2jbtZdmYxQIPLvwO3CYd4/Q==", + "node_modules/gatsby/node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { - "@babel/runtime": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@sindresorhus/slugify": "^1.1.2", - "chokidar": "^3.5.2", - "fs-exists-cached": "^1.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-page-utils": "^2.1.3", - "gatsby-plugin-utils": "^2.1.1", - "gatsby-telemetry": "^3.1.3", - "globby": "^11.0.4", - "lodash": "^4.17.21" + "ms": "2.1.2" }, "engines": { - "node": ">=14.15.0" + "node": ">=6.0" }, - "peerDependencies": { - "gatsby": "^4.0.0-next" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/gatsby-plugin-typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.1.3.tgz", - "integrity": "sha512-1yc3q6rszJztj6xd1phd6DnmYAlK7H3RBRT0Oy+tcdphkd307CktDJdrT+ix93O/7UEZnu9nGM5IC8DRCNdYpA==", + "node_modules/gatsby/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", "dependencies": { - "@babel/core": "^7.15.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/preset-typescript": "^7.15.0", - "@babel/runtime": "^7.15.4", - "babel-plugin-remove-graphql-queries": "^4.1.3" + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" }, "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "gatsby": "^4.0.0-next" - } - }, - "node_modules/gatsby-plugin-use-query-params": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gatsby-plugin-use-query-params/-/gatsby-plugin-use-query-params-1.0.1.tgz", - "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==", - "peerDependencies": { - "gatsby": ">=2.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "use-query-params": ">=0.4.3" + "node": ">=10.10.0" } }, - "node_modules/gatsby-plugin-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gatsby-plugin-utils/-/gatsby-plugin-utils-2.1.1.tgz", - "integrity": "sha512-LzLLSg9CWCi4qMqYNYkt8vAoySkNWKfB8QKJKnpU5qzo/3vjZAx9iWHmoNRYLCCHg1bgJn2Kr5cwOMYAEJspyg==", + "node_modules/gatsby/node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { - "@babel/runtime": "^7.15.4", - "joi": "^17.4.2" + "ms": "2.1.2" }, "engines": { - "node": ">=14.15.0" + "node": ">=6.0" }, - "peerDependencies": { - "gatsby": "^4.0.0-next" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/gatsby-react-router-scroll": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.1.0.tgz", - "integrity": "sha512-hIFhYFScalUremdj8OfRow2U7E0+ULt5QgVsKsyPV7NbM1876iAZZhyzxK83jvoULKmhm2TyEgdVavylMCO3uA==", - "dependencies": { - "@babel/runtime": "^7.15.4" + "node_modules/gatsby/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "@gatsbyjs/reach-router": "^1.3.5", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0" + "node": ">=0.4.0" } }, - "node_modules/gatsby-recipes": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gatsby-recipes/-/gatsby-recipes-1.1.3.tgz", - "integrity": "sha512-AdO9Y7vtpa0VSWXe3gZIeIUDJ6/j/MeAkpkuJHbbU8hix9gtbVPoOcmF8VFJUMALNRzhhFsgqRN2mK6jXJlang==", + "node_modules/gatsby/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dependencies": { - "@babel/core": "^7.15.5", - "@babel/generator": "^7.15.4", - "@babel/helper-plugin-utils": "^7.14.0", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "@babel/runtime": "^7.15.4", - "@babel/standalone": "^7.15.5", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4", - "@graphql-tools/schema": "^7.0.0", - "@graphql-tools/utils": "^7.0.2", - "@hapi/hoek": "8.x.x", - "@hapi/joi": "^15.1.1", - "better-queue": "^3.8.10", - "chokidar": "^3.5.2", - "contentful-management": "^7.5.1", - "cors": "^2.8.5", - "debug": "^4.3.1", - "detect-port": "^1.3.0", - "dotenv": "^8.2.0", - "execa": "^5.1.1", - "express": "^4.17.1", - "express-graphql": "^0.12.0", - "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-telemetry": "^3.1.3", - "glob": "^7.1.6", - "graphql": "^15.4.0", - "graphql-compose": "~7.25.0", - "graphql-subscriptions": "^1.1.0", - "graphql-type-json": "^0.3.2", - "hicat": "^0.8.0", - "is-binary-path": "^2.1.0", - "is-url": "^1.2.4", - "jest-diff": "^25.5.0", - "lock": "^1.0.0", - "lodash": "^4.17.21", - "mitt": "^1.2.0", - "mkdirp": "^0.5.1", - "node-fetch": "^2.5.0", - "pkg-dir": "^4.2.0", - "prettier": "^2.3.2", - "prop-types": "^15.6.1", - "remark-mdx": "^2.0.0-next.4", - "remark-mdxjs": "^2.0.0-next.4", - "remark-parse": "^6.0.3", - "remark-stringify": "^8.1.0", - "resolve-from": "^5.0.0", - "semver": "^7.3.5", - "single-trailing-newline": "^1.0.0", - "strip-ansi": "^6.0.0", - "style-to-object": "^0.3.0", - "unified": "^8.4.2", - "unist-util-remove": "^2.0.0", - "unist-util-visit": "^2.0.2", - "uuid": "3.4.0", - "ws": "^7.3.0", - "xstate": "^4.9.1", - "yoga-layout-prebuilt": "^1.9.6" + "sprintf-js": "~1.0.2" } }, - "node_modules/gatsby-recipes/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/gatsby/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" + "ms": "^2.1.1" } }, - "node_modules/gatsby-recipes/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, + "node_modules/gatsby/node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gatsby-recipes/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/gatsby/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dependencies": { - "yallist": "^4.0.0" + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=10" + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/gatsby-recipes/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/gatsby/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dependencies": { - "p-try": "^2.0.0" + "eslint-visitor-keys": "^1.1.0" }, "engines": { "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/gatsby-recipes/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, + "node_modules/gatsby/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/gatsby-recipes/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" + "node_modules/gatsby/node_modules/eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dependencies": { + "@babel/highlight": "^7.10.4" } }, - "node_modules/gatsby-recipes/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/gatsby/node_modules/eslint/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { - "find-up": "^4.0.0" + "ms": "2.1.2" }, "engines": { - "node": ">=8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/gatsby-recipes/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "node_modules/gatsby/node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/gatsby-recipes/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" + "node_modules/gatsby/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/gatsby-recipes/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/gatsby-telemetry": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-3.1.3.tgz", - "integrity": "sha512-vZSj67F4vzoqqK5CyNIRTDTWOqhBYkMey+i6swY3H51tehGTHRyzzOhJorQOJa76cQOcOMXr1/vvxX1T84Y96g==", - "hasInstallScript": true, - "dependencies": { - "@babel/code-frame": "^7.14.0", - "@babel/runtime": "^7.15.4", - "@turist/fetch": "^7.1.7", - "@turist/time": "^0.0.2", - "async-retry-ng": "^2.0.1", - "boxen": "^4.2.0", - "configstore": "^5.0.1", - "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", - "git-up": "^4.0.5", - "is-docker": "^2.2.1", - "lodash": "^4.17.21", - "node-fetch": "^2.6.5" - }, + "node_modules/gatsby/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "engines": { - "node": ">=14.15.0" + "node": ">=4" } }, - "node_modules/gatsby-telemetry/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/gatsby/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": { - "color-convert": "^2.0.1" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 6" } }, - "node_modules/gatsby-telemetry/node_modules/boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "node_modules/gatsby/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "engines": { - "node": ">=8" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/gatsby-telemetry/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/gatsby/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/gatsby-telemetry/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/gatsby/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/gatsby-telemetry/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/gatsby/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dependencies": { - "color-name": "~1.1.4" + "ansi-regex": "^4.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/gatsby-telemetry/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "node_modules/gatsby/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } }, - "node_modules/gatsby-telemetry/node_modules/has-flag": { + "node_modules/gatsby/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/gatsby-telemetry/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dependencies": { - "has-flag": "^4.0.0" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gatsby-telemetry/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/gatsby-worker": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gatsby-worker/-/gatsby-worker-1.1.0.tgz", - "integrity": "sha512-+Ezi+lO+3bwPIgoGlcP7YAVFpn3iI8E44SwbFOvq9Mrfikdy81iWfOHAXB8TVJ6f0H0vATyJ9EoOAF0bZJWouQ==", - "dependencies": { - "@babel/core": "^7.15.5", - "@babel/runtime": "^7.15.4" - }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "engines": { - "node": ">=14.15.0" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby/node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dependencies": { - "ms": "2.1.2" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gatsby/node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "engines": { - "node": ">=10.10.0" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "node_modules/git-up": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", + "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", "dependencies": { - "ms": "2.1.2" + "is-ssh": "^1.3.0", + "parse-url": "^6.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=6.0" + "node": "*" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gatsby/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.4.0" + "node": ">=10.13.0" } }, - "node_modules/gatsby/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", "dependencies": { - "color-convert": "^2.0.1" + "ini": "2.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" } }, - "node_modules/gatsby/node_modules/babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "peer": true, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" + "global-prefix": "^3.0.0" }, "engines": { "node": ">=6" - }, - "peerDependencies": { - "eslint": ">= 4.12.1" - } - }, - "node_modules/gatsby/node_modules/babel-eslint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "peer": true, - "engines": { - "node": ">=4" } }, - "node_modules/gatsby/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=6" } }, - "node_modules/gatsby/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dependencies": { - "color-name": "~1.1.4" + "isexe": "^2.0.0" }, - "engines": { - "node": ">=7.0.0" + "bin": { + "which": "bin/which" } }, - "node_modules/gatsby/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/gatsby/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/gatsby/node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, "engines": { "node": ">=10" }, @@ -9488,237 +9334,188 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby/node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "node_modules/globby/node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/got": { + "version": "11.8.3", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", + "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=10.19.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/gatsby/node_modules/eslint-config-react-app": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", - "integrity": "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==", - "dependencies": { - "confusing-browser-globals": "^1.0.10" - }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "node_modules/graphql": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.2.tgz", + "integrity": "sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==", "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0", - "@typescript-eslint/parser": "^4.0.0", - "babel-eslint": "^10.0.0", - "eslint": "^7.5.0", - "eslint-plugin-flowtype": "^5.2.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-jest": "^24.0.0", - "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-react": "^7.20.3", - "eslint-plugin-react-hooks": "^4.0.8", - "eslint-plugin-testing-library": "^3.9.0" - }, - "peerDependenciesMeta": { - "eslint-plugin-jest": { - "optional": true - }, - "eslint-plugin-testing-library": { - "optional": true - } + "node": ">= 10.x" } }, - "node_modules/gatsby/node_modules/eslint-plugin-flowtype": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", - "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", + "node_modules/graphql-compose": { + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/graphql-compose/-/graphql-compose-7.25.1.tgz", + "integrity": "sha512-TPXTe1BoQkMjp/MH93yA0SQo8PiXxJAv6Eo6K/+kpJELM9l2jZnd5PCduweuXFcKv+nH973wn/VYzYKDMQ9YoQ==", "dependencies": { - "lodash": "^4.17.15", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "graphql-type-json": "0.3.2", + "object-path": "0.11.5" }, "peerDependencies": { - "eslint": "^7.1.0" + "graphql": "^14.2.0 || ^15.0.0" } }, - "node_modules/gatsby/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/graphql-config": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-3.4.1.tgz", + "integrity": "sha512-g9WyK4JZl1Ko++FSyE5Ir2g66njfxGzrDDhBOwnkoWf/t3TnnZG6BBkWP+pkqVJ5pqMJGPKHNrbew8jRxStjhw==", "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "@endemolshinegroup/cosmiconfig-typescript-loader": "3.0.2", + "@graphql-tools/graphql-file-loader": "^6.0.0", + "@graphql-tools/json-file-loader": "^6.0.0", + "@graphql-tools/load": "^6.0.0", + "@graphql-tools/merge": "6.0.0 - 6.2.14", + "@graphql-tools/url-loader": "^6.0.0", + "@graphql-tools/utils": "^7.0.0", + "cosmiconfig": "7.0.0", + "cosmiconfig-toml-loader": "1.0.0", + "minimatch": "3.0.4", + "string-env-interpolation": "1.0.1" }, "engines": { - "node": ">=6" + "node": ">= 10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/gatsby/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "engines": { - "node": ">=4" + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, - "node_modules/gatsby/node_modules/eslint-webpack-plugin": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.5.4.tgz", - "integrity": "sha512-7rYh0m76KyKSDE+B+2PUQrlNS4HJ51t3WKpkJg6vo2jFMbEPTG99cBV0Dm7LXSHucN4WGCG65wQcRiTFrj7iWw==", + "node_modules/graphql-config/node_modules/cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dependencies": { - "@types/eslint": "^7.2.6", - "arrify": "^2.0.1", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "normalize-path": "^3.0.0", - "schema-utils": "^3.0.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0", - "webpack": "^4.0.0 || ^5.0.0" + "node": ">=10" } }, - "node_modules/gatsby/node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "node_modules/graphql-playground-html": { + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/graphql-playground-html/-/graphql-playground-html-1.6.30.tgz", + "integrity": "sha512-tpCujhsJMva4aqE8ULnF7/l3xw4sNRZcSHu+R00VV+W0mfp+Q20Plvcrp+5UXD+2yS6oyCXncA+zoQJQqhGCEw==", "dependencies": { - "@babel/highlight": "^7.10.4" + "xss": "^1.0.6" } }, - "node_modules/gatsby/node_modules/eslint/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "node_modules/graphql-playground-middleware-express": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.23.tgz", + "integrity": "sha512-M/zbTyC1rkgiQjFSgmzAv6umMHOphYLNWZp6Ye5QrD77WfGOOoSqDsVmGUczc2pDkEPEzzGB/bvBO5rdzaTRgw==", "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" + "graphql-playground-html": "^1.6.30" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "express": "^4.16.2" } }, - "node_modules/gatsby/node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/graphql-subscriptions": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz", + "integrity": "sha512-95yD/tKi24q8xYa7Q9rhQN16AYj5wPbrb8tmHGM3WRc9EBmWrG/0kkMl+tQG8wcEuE9ibR4zyOM31p5Sdr2v4g==", "dependencies": { - "ansi-regex": "^5.0.1" + "iterall": "^1.3.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "graphql": "^0.10.5 || ^0.11.3 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, - "node_modules/gatsby/node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "node_modules/graphql-type-json": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", + "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", + "peerDependencies": { + "graphql": ">=0.8.0" } }, - "node_modules/gatsby/node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/graphql-ws": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", + "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", "engines": { - "node": ">=4" + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=15" } }, - "node_modules/gatsby/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", "dependencies": { - "is-glob": "^4.0.1" + "duplexer": "^0.1.1", + "pify": "^4.0.1" }, "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/gatsby/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dependencies": { - "type-fest": "^0.20.2" + "function-bind": "^1.1.1" }, "engines": { - "node": ">=8" - }, + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gatsby/node_modules/has-flag": { + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -9726,2592 +9523,2386 @@ "node": ">=8" } }, - "node_modules/gatsby/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "engines": { - "node": ">= 4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gatsby/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "has-symbols": "^1.0.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gatsby/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dependencies": { - "yallist": "^4.0.0" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dependencies": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "dependencies": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } + "node_modules/has-values/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "node_modules/gatsby/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=10" - } - }, - "node_modules/gatsby/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dependencies": { - "ansi-regex": "^4.1.0" + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/gatsby/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/gatsby/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dependencies": { - "has-flag": "^4.0.0" + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" }, "engines": { "node": ">=8" - } - }, - "node_modules/gatsby/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gatsby/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=8" } }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "node_modules/hicat": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/hicat/-/hicat-0.8.0.tgz", + "integrity": "sha512-om8L9O5XwqeSdwl5NtHgrzK3wcF4fT9T4gb/NktoH8EyoZipas/tvUZLV48xT7fQfMYr9qvb0WEutqdf0LWSqA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "highlight.js": "^10.4.1", + "minimist": "^1.2.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "hicat": "bin/hicat" } }, - "node_modules/get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "engines": { - "node": ">=4" + "node": "*" } }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "engines": { - "node": ">=0.10.0" + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "engines": { - "node": ">=0.10.0" - } + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/git-up": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", - "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", - "dependencies": { - "is-ssh": "^1.3.0", - "parse-url": "^6.0.0" - } + "node_modules/html-entities": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", + "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==" }, - "node_modules/glob": { + "node_modules/htmlparser2": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dependencies": { - "is-glob": "^4.0.3" + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.6" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "node_modules/global-dirs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", - "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dependencies": { - "ini": "2.0.0" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.0.0" } }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=10.19.0" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dependencies": { - "global-prefix": "^3.0.0" - }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "engines": { - "node": ">=6" + "node": ">=10.17.0" } }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" }, - "bin": { - "which": "bin/which" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "engines": { - "node": ">=4" + "node": ">= 4" } }, - "node_modules/globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, + "node_modules/immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, - "node_modules/got": { - "version": "11.8.2", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", - "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.1", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10.19.0" + "node": ">=6" }, "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" - }, - "node_modules/graphql": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.2.tgz", - "integrity": "sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==", + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "engines": { - "node": ">= 10.x" + "node": ">=4" } }, - "node_modules/graphql-compose": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/graphql-compose/-/graphql-compose-7.25.1.tgz", - "integrity": "sha512-TPXTe1BoQkMjp/MH93yA0SQo8PiXxJAv6Eo6K/+kpJELM9l2jZnd5PCduweuXFcKv+nH973wn/VYzYKDMQ9YoQ==", + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", "dependencies": { - "graphql-type-json": "0.3.2", - "object-path": "0.11.5" + "resolve-from": "^5.0.0" }, - "peerDependencies": { - "graphql": "^14.2.0 || ^15.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/graphql-config": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-3.4.1.tgz", - "integrity": "sha512-g9WyK4JZl1Ko++FSyE5Ir2g66njfxGzrDDhBOwnkoWf/t3TnnZG6BBkWP+pkqVJ5pqMJGPKHNrbew8jRxStjhw==", - "dependencies": { - "@endemolshinegroup/cosmiconfig-typescript-loader": "3.0.2", - "@graphql-tools/graphql-file-loader": "^6.0.0", - "@graphql-tools/json-file-loader": "^6.0.0", - "@graphql-tools/load": "^6.0.0", - "@graphql-tools/merge": "6.0.0 - 6.2.14", - "@graphql-tools/url-loader": "^6.0.0", - "@graphql-tools/utils": "^7.0.0", - "cosmiconfig": "7.0.0", - "cosmiconfig-toml-loader": "1.0.0", - "minimatch": "3.0.4", - "string-env-interpolation": "1.0.1" - }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + "node": ">=4" } }, - "node_modules/graphql-config/node_modules/cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "engines": { - "node": ">=10" + "node": ">=0.8.19" } }, - "node_modules/graphql-playground-html": { - "version": "1.6.30", - "resolved": "https://registry.npmjs.org/graphql-playground-html/-/graphql-playground-html-1.6.30.tgz", - "integrity": "sha512-tpCujhsJMva4aqE8ULnF7/l3xw4sNRZcSHu+R00VV+W0mfp+Q20Plvcrp+5UXD+2yS6oyCXncA+zoQJQqhGCEw==", - "dependencies": { - "xss": "^1.0.6" + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" } }, - "node_modules/graphql-playground-middleware-express": { - "version": "1.7.23", - "resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.23.tgz", - "integrity": "sha512-M/zbTyC1rkgiQjFSgmzAv6umMHOphYLNWZp6Ye5QrD77WfGOOoSqDsVmGUczc2pDkEPEzzGB/bvBO5rdzaTRgw==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dependencies": { - "graphql-playground-html": "^1.6.30" - }, - "peerDependencies": { - "express": "^4.16.2" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/graphql-subscriptions": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz", - "integrity": "sha512-95yD/tKi24q8xYa7Q9rhQN16AYj5wPbrb8tmHGM3WRc9EBmWrG/0kkMl+tQG8wcEuE9ibR4zyOM31p5Sdr2v4g==", - "dependencies": { - "iterall": "^1.3.0" - }, - "peerDependencies": { - "graphql": "^0.10.5 || ^0.11.3 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" - } + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/graphql-type-json": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", - "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", - "peerDependencies": { - "graphql": ">=0.8.0" - } + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/graphql-ws": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", - "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": ">=0.11 <=15" - } + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, - "node_modules/gzip-size": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dependencies": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" }, "engines": { - "node": ">=6" + "node": ">=8.0.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dependencies": { - "function-bind": "^1.1.1" + "type-fest": "^0.21.3" }, "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "node": ">=8" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dependencies": { - "has-symbols": "^1.0.2" + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/has-values": { + "node_modules/is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-accessor-descriptor": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/has-values/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dependencies": { - "kind-of": "^3.0.2" - }, + "node_modules/is-alphanumeric": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", + "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", "engines": { "node": ">=0.10.0" } }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "dependencies": { - "is-buffer": "^1.1.5" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dependencies": { - "is-buffer": "^1.1.5" + "has-bigints": "^1.0.1" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-yarn": { + "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasha/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/hicat": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/hicat/-/hicat-0.8.0.tgz", - "integrity": "sha512-om8L9O5XwqeSdwl5NtHgrzK3wcF4fT9T4gb/NktoH8EyoZipas/tvUZLV48xT7fQfMYr9qvb0WEutqdf0LWSqA==", - "dependencies": { - "highlight.js": "^10.4.1", - "minimist": "^1.2.5" + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "engines": { + "node": ">= 0.4" }, - "bin": { - "hicat": "bin/hicat" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "node_modules/is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dependencies": { - "react-is": "^16.7.0" + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/hosted-git-info": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", - "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { - "lru-cache": "^6.0.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dependencies": { - "yallist": "^4.0.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/html-entities": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==" - }, - "node_modules/htmlparser2": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.1.2.tgz", - "integrity": "sha512-d6cqsbJba2nRdg8WW2okyD4ceonFHn9jLFxhwlNcLhQWcFPdxXeJulgOLjLKtAK9T6ahd+GQNZwG9fjmGW7lyg==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.2", - "domutils": "^2.8.0", - "entities": "^3.0.1" + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "node_modules/http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" + "is-plain-object": "^2.0.4" }, "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" + "node": ">=0.10.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "engines": { "node": ">=0.10.0" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=8" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, "engines": { - "node": ">= 4" + "node": ">=0.10.0" } }, - "node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha1-MHqFWzzxqTi0TqcNLGEQYFNxTzQ=", + "dependencies": { + "is-glob": "^2.0.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/import-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "node_modules/is-invalid-path/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dependencies": { - "resolve-from": "^5.0.0" + "is-extglob": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", "engines": { - "node": ">=0.8.19" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { - "node": ">=8" + "node": ">=0.12.0" } }, - "node_modules/inflight": { + "node_modules/is-number-object": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "engines": { + "node": ">=6" + } }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/inquirer/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dependencies": { - "type-fest": "^0.21.3" + "isobject": "^3.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dependencies": { - "color-convert": "^2.0.1" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "is-unc-path": "^1.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/is-relative-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-3.0.0.tgz", + "integrity": "sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA==", "dependencies": { - "color-name": "~1.1.4" + "is-absolute-url": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "node_modules/is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ssh": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz", + "integrity": "sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ==", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "protocols": "^1.1.0" } }, - "node_modules/inquirer/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "has-symbols": "^1.0.2" + }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "engines": { - "node": ">=8" - } + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, - "node_modules/is-accessor-descriptor": { + "node_modules/is-unc-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dependencies": { - "kind-of": "^6.0.0" + "unc-path-regex": "^0.1.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" }, - "node_modules/is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha1-EQ+f90w39mPh7HkV60UfLbk6yd8=", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "node_modules/is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "call-bind": "^1.0.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-whitespace-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", + "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/is-bigint": { + "node_modules/is-word-character": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", + "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dependencies": { - "binary-extensions": "^2.0.0" + "is-docker": "^2.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" } }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "node_modules/jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", "dependencies": { - "kind-of": "^6.0.0" + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8.3" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/jest-diff/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dependencies": { - "has-tostringtag": "^1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "engines": { + "node": ">= 8.3" } }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/joi": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.2.tgz", + "integrity": "sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "node_modules/joi/node_modules/@hapi/hoek": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", + "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==" + }, + "node_modules/joi/node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" + "@hapi/hoek": "^9.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } + "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==" }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "node_modules/json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" + "minimist": "^1.2.5" }, - "engines": { - "node": ">=10" + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6" } }, - "node_modules/is-invalid-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", - "integrity": "sha1-MHqFWzzxqTi0TqcNLGEQYFNxTzQ=", + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "is-glob": "^2.0.0" + "universalify": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/is-invalid-path/node_modules/is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "node_modules/jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "dependencies": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0" } }, - "node_modules/is-invalid-path/node_modules/is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "node_modules/keyv": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", + "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", "dependencies": { - "is-extglob": "^1.0.0" - }, + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "node_modules/klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 8" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" + "node_modules/language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==" + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dependencies": { + "language-subtag-registry": "~0.3.2" } }, - "node_modules/is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", "dependencies": { - "has-tostringtag": "^1.0.0" + "package-json": "^6.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "node_modules/lilconfig": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" + "node_modules/lmdb-store": { + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/lmdb-store/-/lmdb-store-1.6.14.tgz", + "integrity": "sha512-4woZfvfgolMEngjoMJrwePjdLotr3QKGJsDWURlJmKBed5JtE00IfAKo7ryPowl4ksGcs21pcdLkwrPnKomIuA==", + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.5.0", + "nan": "^2.14.2", + "node-gyp-build": "^4.2.3", + "ordered-binary": "^1.0.0", + "weak-lru-cache": "^1.0.0" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", "engines": { - "node": ">=0.10.0" + "node": ">=6.11.5" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4.0.0" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dependencies": { - "is-unc-path": "^1.0.0" + "minimist": "^1.2.0" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "json5": "lib/cli.js" } }, - "node_modules/is-relative-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-3.0.0.tgz", - "integrity": "sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA==", + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dependencies": { - "is-absolute-url": "^3.0.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/is-resolvable": { + "node_modules/lock": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + "resolved": "https://registry.npmjs.org/lock/-/lock-1.1.0.tgz", + "integrity": "sha1-UxV0mdFlOxNspmRRBx/KYVcD+lU=" }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "node_modules/is-ssh": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz", - "integrity": "sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ==", - "dependencies": { - "protocols": "^1.1.0" - } + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=" }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/lodash.every": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz", + "integrity": "sha1-64mYS+vENkJ5uzrvu9HKGb+mxqc=" }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" }, - "node_modules/is-valid-path": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", - "integrity": "sha1-EQ+f90w39mPh7HkV60UfLbk6yd8=", - "dependencies": { - "is-invalid-path": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, - "node_modules/is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "dependencies": { - "call-bind": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, - "node_modules/is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" }, - "node_modules/is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "node_modules/lodash.maxby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.maxby/-/lodash.maxby-4.6.0.tgz", + "integrity": "sha1-CCJABo88eiJ6oAqDgOTzjPB4bj0=" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "node_modules/lodash.without": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", + "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" + }, + "node_modules/longest-streak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", + "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "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==", "dependencies": { - "is-docker": "^2.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=8" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "node_modules/lower-case/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, - "node_modules/isexe": { + "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "peerDependencies": { - "ws": "*" + "node_modules/lru-cache": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.0.tgz", + "integrity": "sha1-tcvwFVbBaWb+vlTO7A+03JDfbCg=", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" } }, - "node_modules/iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" - }, - "node_modules/jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", "dependencies": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "engines": { - "node": ">= 8.3" + "es5-ext": "~0.10.2" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dependencies": { - "color-convert": "^2.0.1" + "semver": "^6.0.0" }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "p-defer": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, + "node_modules/map-age-cleaner/node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "engines": { - "node": ">=7.0.0" + "node": ">=4" } }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dependencies": { - "has-flag": "^4.0.0" + "object-visit": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "engines": { - "node": ">= 8.3" + "node_modules/markdown-escapes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", + "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" + "repeat-string": "^1.0.0" }, - "engines": { - "node": ">= 10.13.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/md5-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", + "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", + "bin": { + "md5-file": "cli.js" + }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/mdast-util-compact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz", + "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==", "dependencies": { - "has-flag": "^4.0.0" + "unist-util-visit": "^2.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/joi": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.2.tgz", - "integrity": "sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==", + "node_modules/mdast-util-from-markdown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", + "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.0", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/joi/node_modules/@hapi/hoek": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", - "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==" - }, - "node_modules/joi/node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "node_modules/mdast-util-mdx": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-1.1.0.tgz", + "integrity": "sha512-leKb9uG7laXdyFlTleYV4ZEaCpsxeU1LlkkR/xp35pgKrfV1Y0fNCuOw9vaRc2a9YDpH22wd145Wt7UY5yzeZw==", "dependencies": { - "@hapi/hoek": "^9.0.0" + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdx-jsx": "^1.0.0", + "mdast-util-mdxjs-esm": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/mdast-util-mdx-expression": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.1.1.tgz", + "integrity": "sha512-RDLRkBFmBKCJl6/fQdxxKL2BqNtoPFoNBmQAlj5ZNKOijIWRKjdhPkeufsUOaexLj+78mhJc+L7d1MYka8/LdQ==", "dependencies": { - "argparse": "^2.0.1" + "@types/estree-jsx": "^0.0.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" + "node_modules/mdast-util-mdx-jsx": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-1.2.0.tgz", + "integrity": "sha512-5+ot/kfxYd3ChgEMwsMUO71oAfYjyRI3pADEK4I7xTmWLGQ8Y7ghm1CG36zUoUvDPxMlIYwQV/9DYHAUWdG4dA==", + "dependencies": { + "@types/estree-jsx": "^0.0.1", + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "node_modules/mdast-util-mdxjs-esm": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.1.1.tgz", + "integrity": "sha512-IpHNNMubCt6ue2FIQasx1ByvETglnqc7A3XvIc0Yyql1hNI73SEGa044dZG6jeJQE8boBdTn8nxs3DjQLvVN1w==", "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" + "@types/estree-jsx": "^0.0.1", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/mdast-util-to-markdown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.2.6.tgz", + "integrity": "sha512-doJZmTEGagHypWvJ8ltinmwUsT9ZaNgNIQW6Gl7jNdsI1QZkTHTimYW561Niy2s8AEPAqEgV0dIh2UOVlSXUJA==", "dependencies": { - "universalify": "^2.0.0" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/jsx-ast-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", - "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "node_modules/mdast-util-to-markdown/node_modules/unist-util-is": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", + "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz", + "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==", "dependencies": { - "array-includes": "^3.1.3", - "object.assign": "^4.1.2" + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" }, - "engines": { - "node": ">=4.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/keyv": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", - "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", + "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit-parents": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz", + "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==", "dependencies": { - "json-buffer": "3.0.1" + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" + "node_modules/mdast-util-to-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", + "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/meant": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.3.tgz", + "integrity": "sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==" - }, - "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dependencies": { - "language-subtag-registry": "~0.3.2" + "node_modules/mem/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" } }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "node_modules/memfs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.0.tgz", + "integrity": "sha512-o/RfP0J1d03YwsAxyHxAYs2kyJp55AFkMazlFAZFR2I2IXkxiUTXRabJ6RmNNCQ83LAD2jy52Khj0m3OffpNdA==", "dependencies": { - "package-json": "^6.3.0" + "fs-monkey": "1.0.3" }, "engines": { - "node": ">=8" + "node": ">= 4.0.0" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, + "node_modules/memoizee/node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "engines": { - "node": ">= 0.8.0" + "node": ">= 8" } }, - "node_modules/lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", + "node_modules/meros": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", "engines": { - "node": ">=10" + "node": ">=12" + }, + "peerDependencies": { + "@types/node": ">=12" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/lmdb-store": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/lmdb-store/-/lmdb-store-1.6.13.tgz", - "integrity": "sha512-WJPNfzSZXD6anGFdIEK/wq/HzAU5kfi7+LSUSzQ2Qo9uV9REeIYPGqWX+FKl/QCb6qK4ie1D4f44aEvvv7M7rw==", - "hasInstallScript": true, + "node_modules/microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==" + }, + "node_modules/micromark": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.9.tgz", + "integrity": "sha512-aWPjuXAqiFab4+oKLjH1vSNQm8S9GMnnf5sFNLrQaIggGYMBcQ9CS0Tt7+BJH6hbyv783zk3vgDhaORl3K33IQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "nan": "^2.14.2", - "node-gyp-build": "^4.2.3", - "ordered-binary": "^1.0.0", - "weak-lru-cache": "^1.0.0" - }, - "optionalDependencies": { - "msgpackr": "^1.4.7" + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "engines": { - "node": ">=6.11.5" + "node_modules/micromark-core-commonmark": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.5.tgz", + "integrity": "sha512-ZNtWumX94lpiyAu/lxvth6I5+XzxF+BLVUB7u60XzOBy4RojrbZqrx0mcRmbfqEMO6489vyvDfIQNv5hdulrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "node_modules/micromark-extension-mdx-expression": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.3.tgz", + "integrity": "sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "node_modules/micromark-extension-mdx-jsx": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.2.tgz", + "integrity": "sha512-MBppeDuXEBIL1uo4B/bL5eJ1q3m5pXzdzIWpOnJuzzBZF+S+9zbb5WnS2K/LEVQeoyiLzOuoteU4SFPuGJhhWw==", "dependencies": { - "minimist": "^1.2.0" + "@types/acorn": "^4.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" }, - "bin": { - "json5": "lib/cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "node_modules/micromark-extension-mdx-md": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.0.tgz", + "integrity": "sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==", "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "micromark-util-types": "^1.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lock": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/lock/-/lock-1.1.0.tgz", - "integrity": "sha1-UxV0mdFlOxNspmRRBx/KYVcD+lU=" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "node_modules/micromark-extension-mdxjs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.0.tgz", + "integrity": "sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "micromark-extension-mdx-jsx": "^1.0.0", + "micromark-extension-mdx-md": "^1.0.0", + "micromark-extension-mdxjs-esm": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, - "node_modules/lodash.deburr": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", - "integrity": "sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=" - }, - "node_modules/lodash.every": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz", - "integrity": "sha1-64mYS+vENkJ5uzrvu9HKGb+mxqc=" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, - "node_modules/lodash.maxby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.maxby/-/lodash.maxby-4.6.0.tgz", - "integrity": "sha1-CCJABo88eiJ6oAqDgOTzjPB4bj0=" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" - }, - "node_modules/lodash.without": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", - "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" - }, - "node_modules/longest-streak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", - "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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==", + "node_modules/micromark-extension-mdxjs-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.2.tgz", + "integrity": "sha512-bIaxblNIM+CCaJvp3L/V+168l79iuNmxEiTU6i3vB0YuDW+rumV64BFMxvhfRDxaJxQE1zD5vTPdyLBbW4efGA==", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "micromark-core-commonmark": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.1.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" }, - "bin": { - "loose-envify": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "node_modules/micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "tslib": "^2.0.3" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/lower-case/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "engines": { - "node": ">=8" + "node_modules/micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/lru-cache": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.0.tgz", - "integrity": "sha1-tcvwFVbBaWb+vlTO7A+03JDfbCg=", + "node_modules/micromark-factory-mdx-expression": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.5.tgz", + "integrity": "sha512-1DSMCBeCUj4m01P8uYbNWvOsv+FtpDTcBUcDCdE06sENTBX54lndRs9neWOgsNWfLDm2EzCyNKiUaoJ+mWa/WA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "node_modules/micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "es5-ext": "~0.10.2" + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/map-age-cleaner/node_modules/p-defer": { + "node_modules/micromark-factory-whitespace": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "engines": { - "node": ">=4" + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "engines": { - "node": ">=0.10.0" + "node_modules/micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/map-visit": { + "node_modules/micromark-util-chunked": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "node_modules/micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/md5-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", - "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", - "bin": { - "md5-file": "cli.js" - }, - "engines": { - "node": ">=10.13.0" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-compact": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz", - "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==", + "node_modules/micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-from-markdown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.1.0.tgz", - "integrity": "sha512-Mex7IIeIKRpGYNNywpxTfPhfFBTxBL5IVacPMU6GjYF+EkIvy++19cBgxVFyHVd2JpC/chG2IKGqZLffoo7Q1g==", + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "parse-entities": "^3.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-mdx": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-1.1.0.tgz", - "integrity": "sha512-leKb9uG7laXdyFlTleYV4ZEaCpsxeU1LlkkR/xp35pgKrfV1Y0fNCuOw9vaRc2a9YDpH22wd145Wt7UY5yzeZw==", + "node_modules/micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdx-jsx": "^1.0.0", - "mdast-util-mdxjs-esm": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-mdx-expression": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.1.1.tgz", - "integrity": "sha512-RDLRkBFmBKCJl6/fQdxxKL2BqNtoPFoNBmQAlj5ZNKOijIWRKjdhPkeufsUOaexLj+78mhJc+L7d1MYka8/LdQ==", - "dependencies": { - "@types/estree-jsx": "^0.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/micromark-util-encode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.0.tgz", + "integrity": "sha512-cJpFVM768h6zkd8qJ1LNRrITfY4gwFt+tziPcIf71Ui8yFzY9wG3snZQqiWVq93PG4Sw6YOtcNiKJfVIs9qfGg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/mdast-util-mdx-jsx": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-1.1.2.tgz", - "integrity": "sha512-1/bDUZvPGNVxiAjrVsehMZTaNomihpbIMoMr8/UcAQ4h7itDFhN93LtO9XzQPlEM+CMueCoRYGQfl7N+5R+8Mg==", + "node_modules/micromark-util-events-to-acorn": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.0.4.tgz", + "integrity": "sha512-dpo8ecREK5s/KMph7jJ46RLM6g7N21CMc9LAJQbDLdbQnTpijigkSJPTIfLXZ+h5wdXlcsQ+b6ufAE9v76AdgA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@types/estree-jsx": "^0.0.1", - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "parse-entities": "^3.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^4.0.0", - "unist-util-stringify-position": "^3.0.0", + "@types/acorn": "^4.0.0", + "@types/estree": "^0.0.50", + "estree-util-visit": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.1.1.tgz", - "integrity": "sha512-IpHNNMubCt6ue2FIQasx1ByvETglnqc7A3XvIc0Yyql1hNI73SEGa044dZG6jeJQE8boBdTn8nxs3DjQLvVN1w==", - "dependencies": { - "@types/estree-jsx": "^0.0.1", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.2.4.tgz", - "integrity": "sha512-Wive3NvrNS4OY5yYKBADdK1QSlbJUZyZ2ssanITUzNQ7sxMfBANTVjLrAA9BFXshaeG9G77xpOK/z+TTret5Hg==", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-is": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", - "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz", - "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit-parents": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz", - "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", - "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/meant": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.3.tgz", - "integrity": "sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mem": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", - "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/mem?sponsor=1" - } - }, - "node_modules/mem/node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/memfs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", - "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", - "dependencies": { - "fs-monkey": "1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - } - }, - "node_modules/memoizee/node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/meros": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", - "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/node": ">=12" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true + "node_modules/micromark-util-html-tag-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz", + "integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==" + ] }, - "node_modules/micromark": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.7.tgz", - "integrity": "sha512-67ipZ2CzQVsDyH1kqNLh7dLwe5QMPJwjFBGppW7JCLByaSc6ZufV0ywPOxt13MIDAzzmj3wctDL6Ov5w0fOHXw==", + "node_modules/micromark-util-normalize-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", + "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", "funding": [ { "type": "GitHub Sponsors", @@ -12323,29 +11914,13 @@ } ], "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "parse-entities": "^3.0.0", - "uvu": "^0.5.0" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/micromark-core-commonmark": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.4.tgz", - "integrity": "sha512-HAtoZisp1M/sQFuw2zoUKGo1pMKod7GSvdM6B2oBU0U2CEN5/C6Tmydmi1rmvEieEhGQsjMyiiSoYgxISNxGFA==", + "node_modules/micromark-util-resolve-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", + "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", "funding": [ { "type": "GitHub Sponsors", @@ -12357,28 +11932,13 @@ } ], "dependencies": { - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "parse-entities": "^3.0.0", - "uvu": "^0.5.0" + "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-extension-mdx-expression": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.2.tgz", - "integrity": "sha512-KbkM9D9x/ZOV6gLwaN8izl2ZMI3sg+C4JLuUSmd8zJ4Z2d3mSWrznJRLuXkZcyN9lLaRm4Dz2VPjae3AkC5X1A==", + "node_modules/micromark-util-sanitize-uri": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz", + "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==", "funding": [ { "type": "GitHub Sponsors", @@ -12390,109 +11950,15 @@ } ], "dependencies": { - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.2.tgz", - "integrity": "sha512-MBppeDuXEBIL1uo4B/bL5eJ1q3m5pXzdzIWpOnJuzzBZF+S+9zbb5WnS2K/LEVQeoyiLzOuoteU4SFPuGJhhWw==", - "dependencies": { - "@types/acorn": "^4.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.0.tgz", - "integrity": "sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.0.tgz", - "integrity": "sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^1.0.0", - "micromark-extension-mdx-jsx": "^1.0.0", - "micromark-extension-mdx-md": "^1.0.0", - "micromark-extension-mdxjs-esm": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.2.tgz", - "integrity": "sha512-bIaxblNIM+CCaJvp3L/V+168l79iuNmxEiTU6i3vB0YuDW+rumV64BFMxvhfRDxaJxQE1zD5vTPdyLBbW4efGA==", - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.1.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/micromark-factory-label": { + "node_modules/micromark-util-subtokenize": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", + "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", "funding": [ { "type": "GitHub Sponsors", @@ -12504,41 +11970,16 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, - "node_modules/micromark-factory-mdx-expression": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.4.tgz", - "integrity": "sha512-1mS1Cg/GmvvafgHQvxz4OqhcO1JGwzzTuGh1C8NIUrmnr6/3A1dJiAphHz7kJKI/b9WIr69Q8VswuwyWo576yQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-factory-space": { + "node_modules/micromark-util-symbol": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.0.tgz", + "integrity": "sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ==", "funding": [ { "type": "GitHub Sponsors", @@ -12548,16 +11989,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } + ] }, - "node_modules/micromark-factory-title": { + "node_modules/micromark-util-types": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", "funding": [ { "type": "GitHub Sponsors", @@ -12567,359 +12004,56 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], + ] + }, + "node_modules/micromark/node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "@types/ms": "*" } }, - "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" } }, - "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" } }, - "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "engines": { + "node": ">= 0.6" } }, - "node_modules/micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", - "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.1.tgz", - "integrity": "sha512-Wf3H6jLaO3iIlHEvblESXaKAr72nK7JtBbLLICPwuZc3eJkMcp4j8rJ5Xv1VbQWMCWWDvKUbVUbE2MfQNznwTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "parse-entities": "^3.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.0.tgz", - "integrity": "sha512-cJpFVM768h6zkd8qJ1LNRrITfY4gwFt+tziPcIf71Ui8yFzY9wG3snZQqiWVq93PG4Sw6YOtcNiKJfVIs9qfGg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.0.4.tgz", - "integrity": "sha512-dpo8ecREK5s/KMph7jJ46RLM6g7N21CMc9LAJQbDLdbQnTpijigkSJPTIfLXZ+h5wdXlcsQ+b6ufAE9v76AdgA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^0.0.50", - "estree-util-visit": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz", - "integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz", - "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.0.tgz", - "integrity": "sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.1.tgz", - "integrity": "sha512-UT0ylWEEy80RFYzK9pEaugTqaxoD/j0Y9WhHpSyitxd99zjoQz7JJ+iKuhPAgOW2MiPSUAx+c09dcqokeyaROA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark/node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "dependencies": { - "mime-db": "1.50.0" - }, - "engines": { - "node": ">= 0.6" + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/mimic-fn": { @@ -13054,10 +12188,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/msgpackr": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.4.7.tgz", - "integrity": "sha512-bhC8Ed1au3L3oHaR/fe4lk4w7PLGFcWQ5XY/Tk9N6tzDRz8YndjCG68TD8zcvYZoxNtw767eF/7VpaTpU9kf9w==", - "optional": true, + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.1.tgz", + "integrity": "sha512-I1CXFG8BYYSeIhtDlHpUVMsdDiyvP9JAh1d9QoBnkPx3ETPeH/1lR14hweM9GETs09wCWlaOyhtXxIc9boxAAA==", "optionalDependencies": { "msgpackr-extract": "^1.0.14" } @@ -13617,9 +12750,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.1.3.tgz", - "integrity": "sha512-tDTls+KllrZKJrqRXUYJtIcWIyoQycP7cVN7kzNNnhHKF2bMKHflcAQK+pF2Eb1iVaQodHxqZQr0yv4HWLGBhQ==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.2.1.tgz", + "integrity": "sha512-Zl2RCcj/wRCakW9/yI83gutgNf7JFOPEHrCK72z+boIrU+PWAnIt6HADd1w+3keDQ90GCKbp1BduKZgkeNbz7A==" }, "node_modules/os-tmpdir": { "version": "1.0.2", @@ -13904,14 +13037,15 @@ } }, "node_modules/parse-entities": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-3.1.0.tgz", - "integrity": "sha512-xf2yeHbsfg1vJySsQelVwgtI/67eAndVU05skrr/XN6KFMoVVA95BYrW8y78OfW4jqcuHwB7tlMlLkvbq4WbHQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", + "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", "dependencies": { "@types/unist": "^2.0.0", "character-entities": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" @@ -13963,23 +13097,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parse-path/node_modules/query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", - "dependencies": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse-url": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.0.tgz", @@ -14268,13 +13385,13 @@ } }, "node_modules/postcss": { - "version": "8.3.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", - "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", + "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", "dependencies": { "nanoid": "^3.1.30", "picocolors": "^1.0.0", - "source-map-js": "^0.6.2" + "source-map-js": "^1.0.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -14446,11 +13563,10 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/postcss-merge-longhand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz", - "integrity": "sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz", + "integrity": "sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw==", "dependencies": { - "css-color-names": "^1.0.1", "postcss-value-parser": "^4.1.0", "stylehacks": "^5.0.1" }, @@ -14462,15 +13578,14 @@ } }, "node_modules/postcss-merge-rules": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", - "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz", + "integrity": "sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg==", "dependencies": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", "cssnano-utils": "^2.0.1", - "postcss-selector-parser": "^6.0.5", - "vendors": "^1.0.3" + "postcss-selector-parser": "^6.0.5" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -14510,15 +13625,14 @@ } }, "node_modules/postcss-minify-params": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", - "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz", + "integrity": "sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg==", "dependencies": { "alphanum-sort": "^1.0.2", - "browserslist": "^4.16.0", + "browserslist": "^4.16.6", "cssnano-utils": "^2.0.1", - "postcss-value-parser": "^4.1.0", - "uniqs": "^2.0.0" + "postcss-value-parser": "^4.1.0" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -14697,9 +13811,9 @@ } }, "node_modules/postcss-normalize-url": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", - "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.3.tgz", + "integrity": "sha512-qWiUMbvkRx3kc1Dp5opzUwc7MBWZcSDK2yofCmdvFBCpx+zFPkxBC1FASQ59Pt+flYfj/nTZSkmF56+XG5elSg==", "dependencies": { "is-absolute-url": "^3.0.3", "normalize-url": "^6.0.1", @@ -14742,11 +13856,11 @@ } }, "node_modules/postcss-reduce-initial": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", - "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz", + "integrity": "sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw==", "dependencies": { - "browserslist": "^4.16.0", + "browserslist": "^4.16.6", "caniuse-api": "^3.0.0" }, "engines": { @@ -14799,13 +13913,12 @@ } }, "node_modules/postcss-unique-selectors": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", - "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz", + "integrity": "sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA==", "dependencies": { "alphanum-sort": "^1.0.2", - "postcss-selector-parser": "^6.0.5", - "uniqs": "^2.0.0" + "postcss-selector-parser": "^6.0.5" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -14815,9 +13928,9 @@ } }, "node_modules/postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -14836,9 +13949,9 @@ } }, "node_modules/prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz", + "integrity": "sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==", "bin": { "prettier": "bin-prettier.js" }, @@ -14881,36 +13994,6 @@ "node": ">= 8.3" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/pretty-format/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/pretty-format/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -15025,10 +14108,9 @@ } }, "node_modules/query-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", - "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", - "peer": true, + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", "dependencies": { "decode-uri-component": "^0.2.0", "filter-obj": "^1.1.0", @@ -15228,57 +14310,10 @@ "react": "^15.0.0 || ^16.0.0" } }, - "node_modules/react-aria-modal/node_modules/focus-trap-react": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-8.8.2.tgz", - "integrity": "sha512-YgacIMxeAOytHOEbzBWL7+itBkn4ARMwQhtt6hYVHqHzPUPhmfEyKJ/nqsyMerzOK1DzlDv8Q8phRAY8vpa0rA==", - "dependencies": { - "focus-trap": "^6.7.1" - }, - "peerDependencies": { - "prop-types": "^15.7.2", - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/react-aria-modal/node_modules/react-displace": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", - "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==", - "peerDependencies": { - "react": "0.14.x || ^15.0.0 || ^16.0.0", - "react-dom": "0.14.x || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/react-aria-modal/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, - "node_modules/react-aria-modal/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/react-datepicker": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.3.0.tgz", - "integrity": "sha512-cg+gp4YnPcZc6iZ0+v7VuRgcFG22KL7izHYrCxXeSLOn5VGkyxTfcu5qA/cGIsngurCt/u0NtgVTlHB1Fwap1g==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.4.0.tgz", + "integrity": "sha512-QWCDCKTW9i+L33+yyYaDWZkVCDcFjru7wEmSqO6dNsGo4hSaReQ/EaT9IKzjEmvMXfMHn9ixzgGh/4ydnLP08w==", "dependencies": { "@popperjs/core": "^2.9.2", "classnames": "^2.2.6", @@ -15334,6 +14369,17 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/react-dev-utils/node_modules/browserslist": { "version": "4.14.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", @@ -15355,6 +14401,40 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" } }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-dev-utils/node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "node_modules/react-dev-utils/node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -15394,6 +14474,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/react-dev-utils/node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "engines": { + "node": ">= 4" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -15479,6 +14575,26 @@ "node": ">=8" } }, + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-displace": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", + "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==", + "peerDependencies": { + "react": "0.14.x || ^15.0.0 || ^16.0.0", + "react-dom": "0.14.x || ^15.0.0 || ^16.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -15521,9 +14637,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-onclickoutside": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.0.tgz", - "integrity": "sha512-oPlOTYcISLHfpMog2lUZMFSbqOs4LFcA4+vo7fpfevB5v9Z0D5VBDBkfeO5lv+hpEcGoaGk67braLT+QT+eICA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", + "integrity": "sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==", "funding": { "type": "individual", "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" @@ -15689,15 +14805,14 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } }, "node_modules/redux-thunk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.0.tgz", - "integrity": "sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", "peerDependencies": { "redux": "^4" } @@ -15927,6 +15042,14 @@ "semver": "bin/semver" } }, + "node_modules/remark-mdxjs/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/remark-parse": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", @@ -16712,9 +15835,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "node_modules/single-trailing-newline": { "version": "1.0.0", @@ -16753,40 +15876,10 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/slugify": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.2.tgz", - "integrity": "sha512-XMtI8qD84LwCpthLMBHlIhcrj10cgA+U/Ot8G6FD6uFuWZtMfKK75JO7l81nzpFJsPlsW6LT+VKqWQJW3+6New==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.3.tgz", + "integrity": "sha512-1MPyqnIhgiq+/0iDJyqSJHENdnH5MMIlgJIBxmkRMzTNKlS/QsN5dXsB+MdDq4E6w0g9jFA4XOTRkVDjDae/2w==", "engines": { "node": ">=8.0.0" } @@ -16973,6 +16066,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/socket.io": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", @@ -17033,17 +16134,17 @@ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", "engines": { "node": ">=0.10.0" } @@ -17061,9 +16162,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -17516,6 +16617,25 @@ "react-is": ">= 16.8.0" } }, + "node_modules/styled-components/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/styled-components/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/stylehacks": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", @@ -17557,14 +16677,14 @@ "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==" }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/svgo": { @@ -17628,9 +16748,9 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", - "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -17667,9 +16787,9 @@ } }, "node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", "dependencies": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -17680,6 +16800,14 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "acorn": "^8.5.0" + }, + "peerDependenciesMeta": { + "acorn": { + "optional": true + } } }, "node_modules/terser-webpack-plugin": { @@ -17715,18 +16843,10 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", - "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.0.tgz", + "integrity": "sha512-4WuKcUxtzxBoKOUFbt1MtTY9fJwPVD4aN/4Cgxee7OLetPZn5as2bjfZz98XSf2Zq1JFfhqPZpS+43BmWXKgCA==", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -17788,14 +16908,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -17995,9 +17107,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.1", @@ -18052,11 +17164,9 @@ } }, "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "optional": true, - "peer": true, + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "engines": { "node": ">=10" }, @@ -18094,19 +17204,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -18216,11 +17313,6 @@ "node": ">=0.10.0" } }, - "node_modules/uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" - }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -18467,59 +17559,6 @@ "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, - "node_modules/update-notifier/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/update-notifier/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/update-notifier/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/update-notifier/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -18545,17 +17584,6 @@ "node": ">=10" } }, - "node_modules/update-notifier/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/update-notifier/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -18749,15 +17777,6 @@ "node": ">= 0.8" } }, - "node_modules/vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/vfile": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", @@ -18829,9 +17848,9 @@ } }, "node_modules/watchpack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", - "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz", + "integrity": "sha512-MnN0Q1OsvB/GGHETrFeZPQaOelWh/7O+EiFlj8sM9GPjtQkis7k01aAxrg/18kTfoIVcLL+haEVFlXDaSRwKRw==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -18851,9 +17870,9 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "node_modules/webpack": { - "version": "5.62.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.62.1.tgz", - "integrity": "sha512-jNLtnWChS2CMZ7vqWtztv0G6fYB5hz11Zsadp5tE7e4/66zVDj7/KUeQZOsOl8Hz5KrLJH1h2eIDl6AnlyE12Q==", + "version": "5.64.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz", + "integrity": "sha512-LWhqfKjCLoYJLKJY8wk2C3h77i8VyHowG3qYNZiIqD6D0ZS40439S/KVuc/PY48jp2yQmy0mhMknq8cys4jFMw==", "dependencies": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -18877,8 +17896,8 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.0" + "watchpack": "^2.3.0", + "webpack-sources": "^3.2.2" }, "bin": { "webpack": "bin/webpack.js" @@ -19012,9 +18031,9 @@ } }, "node_modules/webpack/node_modules/webpack-sources": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", - "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", + "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==", "engines": { "node": ">=10.13.0" } @@ -19110,36 +18129,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -19157,9 +18146,9 @@ } }, "node_modules/ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "engines": { "node": ">=8.3.0" }, @@ -19213,9 +18202,9 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/xstate": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.26.0.tgz", - "integrity": "sha512-l0tfRBhVYM17D6IWT4pVOzzN9kY/5lnPWCe4LXjJ3F9HCrJOPBn6tPRAb9mapSRBS8cOeByJFDCRSNopgaoC5w==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.26.1.tgz", + "integrity": "sha512-JLofAEnN26l/1vbODgsDa+Phqa61PwDlxWu8+2pK+YbXf+y9pQSDLRvcYH2H1kkeUBA5fGp+xFL/zfE8jNMw4g==", "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" @@ -19414,6 +18403,59 @@ "node": ">=6" } }, + "node_modules/yurnalist/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yurnalist/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yurnalist/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/yurnalist/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/yurnalist/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/yurnalist/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, "node_modules/yurnalist/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -19425,6 +18467,17 @@ "node": ">=6" } }, + "node_modules/yurnalist/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/zwitch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", @@ -19460,9 +18513,9 @@ } }, "@babel/compat-data": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.0.tgz", - "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==" + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==" }, "@babel/core": { "version": "7.16.0", @@ -19484,6 +18537,13 @@ "json5": "^2.1.2", "semver": "^6.3.0", "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } } }, "@babel/eslint-parser": { @@ -19504,6 +18564,13 @@ "@babel/types": "^7.16.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } } }, "@babel/helper-annotate-as-pure": { @@ -19524,13 +18591,13 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.0.tgz", - "integrity": "sha512-S7iaOT1SYlqK0sQaCi21RX4+13hmdmnxIEAnQUB/eh7GeAnRjOUgTYpLkUOiRXzD+yog1JxP0qyAQZ7ZxVxLVg==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz", + "integrity": "sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==", "requires": { "@babel/compat-data": "^7.16.0", "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", + "browserslist": "^4.17.5", "semver": "^6.3.0" } }, @@ -19557,9 +18624,9 @@ } }, "@babel/helper-define-polyfill-provider": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.4.tgz", - "integrity": "sha512-OrpPZ97s+aPi6h2n1OXzdhVis1SGSsMU2aMHgLcOKfsp4/v1NWpx3CWT3lBj5eeBq9cDkPkh+YCfdF7O12uNDQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.0.tgz", + "integrity": "sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==", "requires": { "@babel/helper-compilation-targets": "^7.13.0", "@babel/helper-module-imports": "^7.12.13", @@ -19650,9 +18717,9 @@ "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==" }, "@babel/helper-remap-async-to-generator": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.0.tgz", - "integrity": "sha512-MLM1IOMe9aQBqMWxcRw8dcb9jlM86NIw7KA0Wri91Xkfied+dE0QuBFSBjMNvqzmS0OSIDsMNC24dBEkPUi7ew==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz", + "integrity": "sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==", "requires": { "@babel/helper-annotate-as-pure": "^7.16.0", "@babel/helper-wrap-function": "^7.16.0", @@ -19716,12 +18783,12 @@ } }, "@babel/helpers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.0.tgz", - "integrity": "sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.3.tgz", + "integrity": "sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w==", "requires": { "@babel/template": "^7.16.0", - "@babel/traverse": "^7.16.0", + "@babel/traverse": "^7.16.3", "@babel/types": "^7.16.0" } }, @@ -19733,12 +18800,63 @@ "@babel/helper-validator-identifier": "^7.15.7", "chalk": "^2.0.0", "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/parser": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.2.tgz", - "integrity": "sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==" + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.16.2", @@ -19759,12 +18877,12 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.0.tgz", - "integrity": "sha512-nyYmIo7ZqKsY6P4lnVmBlxp9B3a96CscbLotlsNuktMHahkDwoPYEjXrZHU0Tj844Z9f1IthVxQln57mhkcExw==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz", + "integrity": "sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg==", "requires": { "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.16.0", + "@babel/helper-remap-async-to-generator": "^7.16.4", "@babel/plugin-syntax-async-generators": "^7.8.4" } }, @@ -20075,6 +19193,13 @@ "@babel/helper-replace-supers": "^7.16.0", "@babel/helper-split-export-declaration": "^7.16.0", "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + } } }, "@babel/plugin-transform-computed-properties": { @@ -20289,15 +19414,15 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.0.tgz", - "integrity": "sha512-zlPf1/XFn5+vWdve3AAhf+Sxl+MVa5VlwTwWgnLx23u4GlatSRQJ3Eoo9vllf0a9il3woQsT4SK+5Z7c06h8ag==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.4.tgz", + "integrity": "sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A==", "requires": { "@babel/helper-module-imports": "^7.16.0", "@babel/helper-plugin-utils": "^7.14.5", - "babel-plugin-polyfill-corejs2": "^0.2.3", - "babel-plugin-polyfill-corejs3": "^0.3.0", - "babel-plugin-polyfill-regenerator": "^0.2.3", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.4.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", "semver": "^6.3.0" } }, @@ -20370,17 +19495,17 @@ } }, "@babel/preset-env": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.0.tgz", - "integrity": "sha512-cdTu/W0IrviamtnZiTfixPfIncr2M1VqRrkjzZWlr1B4TVYimCFK5jkyOdP4qw2MrlKHi+b3ORj6x8GoCew8Dg==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.4.tgz", + "integrity": "sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==", "requires": { - "@babel/compat-data": "^7.16.0", - "@babel/helper-compilation-targets": "^7.16.0", + "@babel/compat-data": "^7.16.4", + "@babel/helper-compilation-targets": "^7.16.3", "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.2", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-async-generator-functions": "^7.16.0", + "@babel/plugin-proposal-async-generator-functions": "^7.16.4", "@babel/plugin-proposal-class-properties": "^7.16.0", "@babel/plugin-proposal-class-static-block": "^7.16.0", "@babel/plugin-proposal-dynamic-import": "^7.16.0", @@ -20430,7 +19555,7 @@ "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.0", "@babel/plugin-transform-new-target": "^7.16.0", "@babel/plugin-transform-object-super": "^7.16.0", - "@babel/plugin-transform-parameters": "^7.16.0", + "@babel/plugin-transform-parameters": "^7.16.3", "@babel/plugin-transform-property-literals": "^7.16.0", "@babel/plugin-transform-regenerator": "^7.16.0", "@babel/plugin-transform-reserved-words": "^7.16.0", @@ -20443,10 +19568,10 @@ "@babel/plugin-transform-unicode-regex": "^7.16.0", "@babel/preset-modules": "^0.1.5", "@babel/types": "^7.16.0", - "babel-plugin-polyfill-corejs2": "^0.2.3", - "babel-plugin-polyfill-corejs3": "^0.3.0", - "babel-plugin-polyfill-regenerator": "^0.2.3", - "core-js-compat": "^3.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.4.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.19.1", "semver": "^6.3.0" } }, @@ -20494,18 +19619,18 @@ } }, "@babel/runtime-corejs3": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.0.tgz", - "integrity": "sha512-Oi2qwQ21X7/d9gn3WiwkDTJmq3TQtYNz89lRnoFy8VeZpWlsyXvzSwiRrRZ8cXluvSwqKxqHJ6dBd9Rv+p0ZGQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.3.tgz", + "integrity": "sha512-IAdDC7T0+wEB4y2gbIL0uOXEYpiZEeuFUTVbdGq+UwCcF35T/tS8KrmMomEwEc5wBbyfH3PJVpTSUqrhPDXFcQ==", "requires": { "core-js-pure": "^3.19.0", "regenerator-runtime": "^0.13.4" } }, "@babel/standalone": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.16.3.tgz", - "integrity": "sha512-hoStDfHl2+EYUv1LNHhZTysa+lMSwIEkkT4HnDNX+F0zqvPdoE2QLF7qtkd45cgCGOwQjrvwe2mOKcX3f6Wr8A==" + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.16.4.tgz", + "integrity": "sha512-FDRLwjeQfPm5jaHNuB+vwNyGCp24Ah3kEsbLzKmh0eSru+QCr4DmjgbRPoz71AwXLVtXU+l/i7MlVlIj5XO7Gw==" }, "@babel/template": { "version": "7.16.0", @@ -20518,19 +19643,26 @@ } }, "@babel/traverse": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.0.tgz", - "integrity": "sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.3.tgz", + "integrity": "sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==", "requires": { "@babel/code-frame": "^7.16.0", "@babel/generator": "^7.16.0", "@babel/helper-function-name": "^7.16.0", "@babel/helper-hoist-variables": "^7.16.0", "@babel/helper-split-export-declaration": "^7.16.0", - "@babel/parser": "^7.16.0", + "@babel/parser": "^7.16.3", "@babel/types": "^7.16.0", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + } } }, "@babel/types": { @@ -20587,6 +19719,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", + "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -20597,26 +19730,6 @@ "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "requires": { - "type-fest": "^0.20.2" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - } } }, "@gatsbyjs/reach-router": { @@ -20766,6 +19879,11 @@ "slash": "^3.0.0" } }, + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -20859,8 +19977,7 @@ "ws": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", - "requires": {} + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" } } }, @@ -20938,6 +20055,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", + "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", @@ -20965,14 +20083,6 @@ "chalk": "^3.0.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -20981,32 +20091,6 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -21060,18 +20144,13 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, "@popperjs/core": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", - "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz", + "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==" }, "@sideway/address": { "version": "4.1.2", @@ -21110,13 +20189,6 @@ "requires": { "@sindresorhus/transliterate": "^0.1.1", "escape-string-regexp": "^4.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - } } }, "@sindresorhus/transliterate": { @@ -21216,9 +20288,9 @@ "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==" }, "@types/eslint": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz", - "integrity": "sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", "requires": { "@types/estree": "*", "@types/json-schema": "*" @@ -21319,9 +20391,9 @@ } }, "@types/lodash": { - "version": "4.14.176", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", - "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==" + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" }, "@types/mdast": { "version": "3.0.10", @@ -21350,9 +20422,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==" }, "@types/node-fetch": { "version": "2.5.12", @@ -21382,9 +20454,9 @@ } }, "@types/react": { - "version": "17.0.34", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz", - "integrity": "sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==", + "version": "17.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", + "integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -21464,18 +20536,10 @@ "tsutils": "^3.21.0" }, "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" }, "lru-cache": { "version": "6.0.0", @@ -21500,6 +20564,19 @@ } } }, + "@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "requires": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, "@typescript-eslint/parser": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", @@ -21735,21 +20812,19 @@ } }, "acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==" + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==" }, "acorn-import-assertions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "requires": {} + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==" }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" }, "address": { "version": "1.1.2", @@ -21779,8 +20854,7 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" }, "alphanum-sort": { "version": "1.0.2", @@ -21821,11 +20895,11 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "anymatch": { @@ -21860,7 +20934,8 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "aria-query": { "version": "4.2.2", @@ -22059,48 +21134,48 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.3.tgz", - "integrity": "sha512-NDZ0auNRzmAfE1oDDPW2JhzIMXUk+FFe2ICejmt5T4ocKgiQx3e0VCRx9NCAidcMtL2RUZaWtXnmjTCkx0tcbA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", + "integrity": "sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==", "requires": { "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.2.4", + "@babel/helper-define-polyfill-provider": "^0.3.0", "semver": "^6.1.1" } }, "babel-plugin-polyfill-corejs3": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.3.0.tgz", - "integrity": "sha512-JLwi9vloVdXLjzACL80j24bG6/T1gYxwowG44dg6HN/7aTPdyPbJJidf6ajoA3RPHHtW0j9KMrSOLpIZpAnPpg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz", + "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.4", + "@babel/helper-define-polyfill-provider": "^0.3.0", "core-js-compat": "^3.18.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.3.tgz", - "integrity": "sha512-JVE78oRZPKFIeUqFGrSORNzQnrDwZR16oiWeGM8ZyjBn2XAT5OjP+wXx5ESuo33nUsFUEJYjtklnsKbxW5L+7g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz", + "integrity": "sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.4" + "@babel/helper-define-polyfill-provider": "^0.3.0" } }, "babel-plugin-remove-graphql-queries": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.1.3.tgz", - "integrity": "sha512-84LUlWYfw+aQjUNsd81DwjdOPpQI9/EnxL7smdkFH1WjwfIhAaJYW+tNN4eZLurhexI5mYdHIJQ3kD6hpzwkNw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.2.0.tgz", + "integrity": "sha512-9WLJRInT+cr76wGT75FHf5qIVoFuq83fvgzWEidSYS7M6BXM/A8JK7vxuhom2aDwYmxWn+tXNQGYKp3BEIvV1Q==", "requires": { "@babel/runtime": "^7.15.4", - "gatsby-core-utils": "^3.1.3" + "gatsby-core-utils": "^3.2.0" } }, "babel-plugin-styled-components": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.3.tgz", - "integrity": "sha512-meGStRGv+VuKA/q0/jXxrPNWEm4LPfYIqxooDTdmh8kFsP/Ph7jJG5rUPwUPX3QHUvggwdbgdGpo88P/rRYsVw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", + "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", "requires": { - "@babel/helper-annotate-as-pure": "^7.15.4", - "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", "babel-plugin-syntax-jsx": "^6.18.0", "lodash": "^4.17.11" } @@ -22116,9 +21191,9 @@ "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" }, "babel-preset-gatsby": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-gatsby/-/babel-preset-gatsby-2.1.3.tgz", - "integrity": "sha512-O9yqDlzPrcY/Q1AsoMMbWcbt1cjc7rLP8Ga04/jxukPGI1GqK6FPFv8LVuPPI2lNKK2RLfhIoaaVgvcB5P2ZFw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-gatsby/-/babel-preset-gatsby-2.2.0.tgz", + "integrity": "sha512-SFlilnVwNFVEwbgz0vsLe3ckCN6PW0XUvSNJlnzkuClKSx9BcPItNXRQtpYKkVHA4W1XZX5Qm9NaIOflP2i9tw==", "requires": { "@babel/plugin-proposal-class-properties": "^7.14.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", @@ -22133,8 +21208,8 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-macros": "^2.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", - "gatsby-core-utils": "^3.1.3", - "gatsby-legacy-polyfills": "^2.1.0" + "gatsby-core-utils": "^3.2.0", + "gatsby-legacy-polyfills": "^2.2.0" } }, "backo2": { @@ -22321,56 +21396,6 @@ "type-fest": "^0.20.2", "widest-line": "^3.1.0", "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - } } }, "brace-expansion": { @@ -22391,12 +21416,12 @@ } }, "browserslist": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.6.tgz", - "integrity": "sha512-uPgz3vyRTlEiCv4ee9KlsKgo2V6qPk7Jsn0KAn2OBqbqKo3iNcPEC1Ti6J4dwnz+aIRfEEEuOzC9IBk8tXUomw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", + "integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", "requires": { - "caniuse-lite": "^1.0.30001274", - "electron-to-chromium": "^1.3.886", + "caniuse-lite": "^1.0.30001280", + "electron-to-chromium": "^1.3.896", "escalade": "^3.1.1", "node-releases": "^2.0.1", "picocolors": "^1.0.0" @@ -22537,9 +21562,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001278", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001278.tgz", - "integrity": "sha512-mpF9KeH8u5cMoEmIic/cr7PNS+F5LWBk0t2ekGT60lFf0Wq+n9LspAj0g3P+o7DQhD3sUdlMln4YFAWhFYn9jg==" + "version": "1.0.30001283", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz", + "integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==" }, "ccount": { "version": "1.1.0", @@ -22547,13 +21572,12 @@ "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==" }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "character-entities": { @@ -22821,27 +21845,6 @@ "wrap-ansi": "^6.2.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -22887,17 +21890,17 @@ } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "colord": { "version": "2.9.1", @@ -22928,9 +21931,9 @@ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" }, "common-tags": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.1.tgz", - "integrity": "sha512-uOZd85rJqrdEIE/JjhW5YAeatX8iqjjvVzIyfx7JL7G5r9Tep6YpYT9gEJWhWpVyDQEyzukWd6p2qULpJ8tmBw==" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" }, "commondir": { "version": "1.0.1", @@ -23074,9 +22077,9 @@ }, "dependencies": { "type-fest": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.3.tgz", - "integrity": "sha512-7VNmE7FlsrdcWjKbtuRuynZz96Gmf35p5DvoR2tbceNP0vd58ISx87PvUUInlhtRC49vSX6qlxEKc7AoiHRirg==" + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.6.0.tgz", + "integrity": "sha512-XN1FDGGtaSDA6CFsCW5iolTQqFsnJ+ZF6JqSz0SqXoh4F8GY0xqUv5RYnTilpmL+sOH8OH4FX8tf9YyAPM2LDA==" } } }, @@ -23131,16 +22134,16 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.1.tgz", - "integrity": "sha512-Tnc7E9iKd/b/ff7GFbhwPVzJzPztGrChB8X8GLqoYGdEOG8IpLnK1xPyo3ZoO3HsK6TodJS58VGPOxA+hLHQMg==" + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.2.tgz", + "integrity": "sha512-ciYCResnLIATSsXuXnIOH4CbdfgV+H1Ltg16hJFN7/v6OxqnFr/IFGeLacaZ+fHLAm0TBbXwNK9/DNBzBUrO/g==" }, "core-js-compat": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.19.1.tgz", - "integrity": "sha512-Q/VJ7jAF/y68+aUsQJ/afPOewdsGkDtcMb40J8MbuWKlK3Y+wtHq8bTHKPj2WKWLIqmS5JhHs4CzHtz6pT2W6g==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.19.2.tgz", + "integrity": "sha512-ObBY1W5vx/LFFMaL1P5Udo4Npib6fu+cMokeziWkA8Tns4FcDemKF5j9JvaI5JhdkW8EQJQGJN1EcrzmEwuAqQ==", "requires": { - "browserslist": "^4.17.6", + "browserslist": "^4.18.1", "semver": "7.0.0" }, "dependencies": { @@ -23152,9 +22155,9 @@ } }, "core-js-pure": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.19.1.tgz", - "integrity": "sha512-Q0Knr8Es84vtv62ei6/6jXH/7izKmOrtrxH9WJTHLCMAVeU+8TF8z8Nr08CsH4Ot0oJKzBzJJL9SJBYIv7WlfQ==" + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.19.2.tgz", + "integrity": "sha512-5LkcgQEy8pFeVnd/zomkUBSwnmIxuF1C8E9KrMAbOc8f34IBT9RGvTYeNDdp1PnvMJrrVhvk1hg/yVV5h/znlg==" }, "core-util-is": { "version": "1.0.3", @@ -23191,9 +22194,9 @@ } }, "create-gatsby": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/create-gatsby/-/create-gatsby-2.1.1.tgz", - "integrity": "sha512-TyTrJD5Wa2WTa5PVY3ng1aIo7vqyR3rmQJuGRNPNeRZIfrYBrMGILWIh8TV+pofTKiZsRhPPMmHlpDD62tqPcw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/create-gatsby/-/create-gatsby-2.2.0.tgz", + "integrity": "sha512-nQ3t2+qpSnepqxFeBrkL6os5TR2TN4Nc1cCX/3YCWzbMQ7etc54Yjsw/PRFBUFtbt9RJk/7CURtJKFHDNsHtZw==", "requires": { "@babel/runtime": "^7.15.4" } @@ -23238,11 +22241,6 @@ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" }, - "css-color-names": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", - "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==" - }, "css-declaration-sorter": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz", @@ -23401,20 +22399,20 @@ "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" }, "cssnano": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.10.tgz", - "integrity": "sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz", + "integrity": "sha512-U38V4x2iJ3ijPdeWqUrEr4eKBB5PbEKsNP5T8xcik2Au3LeMtiMHX0i2Hu9k51FcKofNZumbrcdC6+a521IUHg==", "requires": { - "cssnano-preset-default": "^5.1.6", + "cssnano-preset-default": "^5.1.8", "is-resolvable": "^1.1.0", "lilconfig": "^2.0.3", "yaml": "^1.10.2" } }, "cssnano-preset-default": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz", - "integrity": "sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.8.tgz", + "integrity": "sha512-zWMlP0+AMPBVE852SqTrP0DnhTcTA2C1wAF92TKZ3Va+aUVqLIhkqKlnJIXXdqXD7RN+S1ujuWmNpvrJBiM/vg==", "requires": { "css-declaration-sorter": "^6.0.3", "cssnano-utils": "^2.0.1", @@ -23425,11 +22423,11 @@ "postcss-discard-duplicates": "^5.0.1", "postcss-discard-empty": "^5.0.1", "postcss-discard-overridden": "^5.0.1", - "postcss-merge-longhand": "^5.0.3", - "postcss-merge-rules": "^5.0.2", + "postcss-merge-longhand": "^5.0.4", + "postcss-merge-rules": "^5.0.3", "postcss-minify-font-values": "^5.0.1", "postcss-minify-gradients": "^5.0.3", - "postcss-minify-params": "^5.0.1", + "postcss-minify-params": "^5.0.2", "postcss-minify-selectors": "^5.1.0", "postcss-normalize-charset": "^5.0.1", "postcss-normalize-display-values": "^5.0.1", @@ -23438,20 +22436,19 @@ "postcss-normalize-string": "^5.0.1", "postcss-normalize-timing-functions": "^5.0.1", "postcss-normalize-unicode": "^5.0.1", - "postcss-normalize-url": "^5.0.2", + "postcss-normalize-url": "^5.0.3", "postcss-normalize-whitespace": "^5.0.1", "postcss-ordered-values": "^5.0.2", - "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-initial": "^5.0.2", "postcss-reduce-transforms": "^5.0.1", "postcss-svgo": "^5.0.3", - "postcss-unique-selectors": "^5.0.1" + "postcss-unique-selectors": "^5.0.2" } }, "cssnano-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", - "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "requires": {} + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" }, "csso": { "version": "4.2.0", @@ -23462,9 +22459,9 @@ } }, "csstype": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" }, "d": { "version": "1.0.1", @@ -23486,14 +22483,14 @@ "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==" }, "date-fns": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz", - "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==" + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.26.0.tgz", + "integrity": "sha512-VQI812dRi3cusdY/fhoBKvc6l2W8BPWU1FNVnFH9Nttjx4AFBRzfSVb/Eyc7jBT6e9sg1XtAGsYpBQ6c/jygbg==" }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -23503,6 +22500,14 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decode-named-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.0.tgz", + "integrity": "sha512-KTiXDlRp9MMm/nlgI8rDGKoNNKiTJBl0RPjnBM680m2HlgJEA4JTASspK44lsvE4GQJildMRFp2HdEBiG+nqng==", + "requires": { + "character-entities": "^2.0.0" + } + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -23598,6 +22603,11 @@ "merge2": "^1.2.3", "slash": "^3.0.0" } + }, + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" } } }, @@ -23859,9 +22869,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.891", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.891.tgz", - "integrity": "sha512-3cpwR82QkIS01CN/dup/4Yr3BiOiRLlZlcAFn/5FbNCunMO9ojqDgEP9JEo1QNLflu3pEnPWve50gHOEKc7r6w==" + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.4.tgz", + "integrity": "sha512-teHtgwcmVcL46jlFvAaqjyiTLWuMrUQO1JqV303JKB4ysXG6m8fXSFhbjal9st0r9mNskI22AraJZorb1VcLVg==" }, "emoji-regex": { "version": "9.2.2", @@ -23903,8 +22913,7 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "requires": {} + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -23928,8 +22937,7 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "requires": {} + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -24069,479 +23077,110 @@ "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==", - "requires": { - "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.6.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^6.0.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.2.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint-scope": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", - "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==" - }, - "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "requires": { - "type-fest": "^0.20.2" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^14.2.1", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - }, - "dependencies": { - "eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - } - } - }, - "eslint-config-airbnb-typescript": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-14.0.2.tgz", - "integrity": "sha512-oaVR63DqpRUiOOeSVxIzhD3FXbqJRH+7Lt9GCMsS9SKgrRW3XpZINN2FO4JEsnaHEGkktumd0AHE9K7KQNuXSQ==", - "dev": true, - "peer": true, - "requires": { - "eslint-config-airbnb-base": "^14.2.1" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "peer": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "peer": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - } - }, - "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "peer": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "peer": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "peer": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "peer": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "peer": true - }, - "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "peer": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "peer": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "peer": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true - } - } - }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "peer": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true - } - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "peer": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.0.4", + "@humanwhocodes/config-array": "^0.6.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.2.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, - "peer": true, "requires": { - "type-fest": "^0.20.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "peer": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "peer": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "peer": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "peer": true, "requires": { "yallist": "^4.0.0" } @@ -24551,50 +23190,60 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, - "peer": true, "requires": { "lru-cache": "^6.0.0" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "peer": true - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true + "dev": true } } }, + "eslint-config-airbnb": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.1.tgz", + "integrity": "sha512-ooV8Vdt3gmV6hCSJutRw5beArd56Cd++vvy7j0Y7DBhUKqcTyI7cWNcJpCJlcftGPRLtzdU0hNMtquOUSSm8nA==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + } + }, + "eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + } + }, "eslint-config-prettier": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "requires": {} + "dev": true + }, + "eslint-config-react-app": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", + "integrity": "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==", + "requires": { + "confusing-browser-globals": "^1.0.10" + } }, "eslint-config-wesbos": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-config-wesbos/-/eslint-config-wesbos-2.1.0.tgz", "integrity": "sha512-qwuEFecMmfRoaV/7niB7StdaLwUQEWv9Oz67G5rQsiEm84ACC3mgMBe2NXeV5l2LHUuCiyHWKMWFpmWvPicnhQ==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -24635,6 +23284,15 @@ } } }, + "eslint-plugin-flowtype": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", + "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", + "requires": { + "lodash": "^4.17.15", + "string-natural-compare": "^3.0.1" + } + }, "eslint-plugin-graphql": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-4.0.0.tgz", @@ -24727,9 +23385,9 @@ } }, "eslint-plugin-react": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.0.tgz", - "integrity": "sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", "requires": { "array-includes": "^3.1.4", "array.prototype.flatmap": "^1.2.5", @@ -24769,8 +23427,7 @@ "eslint-plugin-react-hooks": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "requires": {} + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==" }, "eslint-scope": { "version": "5.1.1", @@ -24801,20 +23458,65 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" }, + "eslint-webpack-plugin": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.6.0.tgz", + "integrity": "sha512-V+LPY/T3kur5QO3u+1s34VDTcRxjXWPUGM4hlmTb5DwVD0OQz631yGTxJZf4SpAqAjdbBVe978S8BJeHpAdOhQ==", + "requires": { + "@types/eslint": "^7.28.2", + "arrify": "^2.0.1", + "jest-worker": "^27.3.1", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1" + }, + "dependencies": { + "jest-worker": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.0.tgz", + "integrity": "sha512-4WuKcUxtzxBoKOUFbt1MtTY9fJwPVD4aN/4Cgxee7OLetPZn5as2bjfZz98XSf2Zq1JFfhqPZpS+43BmWXKgCA==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "espree": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", - "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", + "dev": true, "requires": { - "acorn": "^8.5.0", + "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.0.0" + "eslint-visitor-keys": "^3.1.0" }, "dependencies": { "eslint-visitor-keys": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==" + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true } } }, @@ -25087,6 +23789,11 @@ "raw-body": "^2.4.1" }, "dependencies": { + "bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==" + }, "http-errors": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", @@ -25100,32 +23807,32 @@ } }, "raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.3", + "bytes": "3.1.1", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, "dependencies": { "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "requires": { "depd": "~1.1.2", "inherits": "2.0.4", - "setprototypeof": "1.1.1", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "toidentifier": "1.0.1" } }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" } } }, @@ -25300,6 +24007,13 @@ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + } } }, "file-entry-cache": { @@ -25474,9 +24188,9 @@ } }, "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" }, "focus-trap": { "version": "6.7.1", @@ -25486,6 +24200,14 @@ "tabbable": "^5.2.1" } }, + "focus-trap-react": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-8.8.2.tgz", + "integrity": "sha512-YgacIMxeAOytHOEbzBWL7+itBkn4ARMwQhtt6hYVHqHzPUPhmfEyKJ/nqsyMerzOK1DzlDv8Q8phRAY8vpa0rA==", + "requires": { + "focus-trap": "^6.7.1" + } + }, "follow-redirects": { "version": "1.14.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", @@ -25510,6 +24232,14 @@ "worker-rpc": "^0.1.0" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", @@ -25537,6 +24267,34 @@ } } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -25558,6 +24316,11 @@ } } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -25611,6 +24374,14 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -25652,9 +24423,9 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, "fraction.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", - "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==" + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz", + "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==" }, "fragment-cache": { "version": "0.2.1", @@ -25711,9 +24482,9 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, "gatsby": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/gatsby/-/gatsby-4.1.6.tgz", - "integrity": "sha512-cXgP3k9Jxw4yv7lKWt/F5RRMPbJREI/180zHYrfgT+KlOm9VQ0TImRzBq4XJBKQVGcYUxD9+QKY75e81onPNwg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby/-/gatsby-4.2.0.tgz", + "integrity": "sha512-VOLlviKLmfdlts/idCwtRvzUjkOo4WzDMdlh5thq/ai/5sBdVObOcrKnlcZin7n/MGl78t3k4tLS3UjybLhhIw==", "requires": { "@babel/code-frame": "^7.14.0", "@babel/core": "^7.15.5", @@ -25732,19 +24503,19 @@ "@typescript-eslint/parser": "^4.33.0", "@vercel/webpack-asset-relocator-loader": "^1.7.0", "address": "1.1.2", - "anser": "^2.0.2", - "autoprefixer": "^10.3.7", + "anser": "^2.1.0", + "autoprefixer": "^10.4.0", "axios": "^0.21.1", - "babel-loader": "^8.2.2", + "babel-loader": "^8.2.3", "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-lodash": "^3.3.4", - "babel-plugin-remove-graphql-queries": "^4.1.3", - "babel-preset-gatsby": "^2.1.3", + "babel-plugin-remove-graphql-queries": "^4.2.0", + "babel-preset-gatsby": "^2.2.0", "better-opn": "^2.1.1", "bluebird": "^3.7.2", "body-parser": "^1.19.0", - "browserslist": "^4.17.3", + "browserslist": "^4.17.5", "cache-manager": "^2.11.1", "chalk": "^4.1.2", "chokidar": "^3.5.2", @@ -25767,7 +24538,7 @@ "eslint-config-react-app": "^6.0.0", "eslint-plugin-flowtype": "^5.10.0", "eslint-plugin-graphql": "^4.0.0", - "eslint-plugin-import": "^2.24.2", + "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.26.1", "eslint-plugin-react-hooks": "^4.2.0", @@ -25782,20 +24553,20 @@ "find-cache-dir": "^3.3.2", "fs-exists-cached": "1.0.0", "fs-extra": "^10.0.0", - "gatsby-cli": "^4.1.4", - "gatsby-core-utils": "^3.1.3", - "gatsby-graphiql-explorer": "^2.1.0", - "gatsby-legacy-polyfills": "^2.1.0", - "gatsby-link": "^4.1.0", - "gatsby-plugin-page-creator": "^4.1.4", - "gatsby-plugin-typescript": "^4.1.3", - "gatsby-plugin-utils": "^2.1.1", - "gatsby-react-router-scroll": "^5.1.0", - "gatsby-telemetry": "^3.1.3", - "gatsby-worker": "^1.1.0", + "gatsby-cli": "^4.2.0", + "gatsby-core-utils": "^3.2.0", + "gatsby-graphiql-explorer": "^2.2.0", + "gatsby-legacy-polyfills": "^2.2.0", + "gatsby-link": "^4.2.0", + "gatsby-plugin-page-creator": "^4.2.0", + "gatsby-plugin-typescript": "^4.2.0", + "gatsby-plugin-utils": "^2.2.0", + "gatsby-react-router-scroll": "^5.2.0", + "gatsby-telemetry": "^3.2.0", + "gatsby-worker": "^1.2.0", "glob": "^7.2.0", "got": "^11.8.2", - "graphql": "^15.6.1", + "graphql": "^15.7.2", "graphql-compose": "~7.25.1", "graphql-playground-middleware-express": "^1.7.22", "hasha": "^5.2.2", @@ -25806,7 +24577,7 @@ "joi": "^17.4.2", "json-loader": "^0.5.7", "latest-version": "5.1.0", - "lmdb-store": "^1.6.8", + "lmdb-store": "^1.6.11", "lodash": "^4.17.21", "md5-file": "^5.0.0", "meant": "^1.0.3", @@ -25817,7 +24588,7 @@ "mitt": "^1.2.0", "moment": "^2.29.1", "multer": "^1.4.3", - "node-fetch": "^2.6.5", + "node-fetch": "^2.6.6", "normalize-path": "^3.0.0", "null-loader": "^4.0.1", "opentracing": "^0.14.5", @@ -25825,7 +24596,7 @@ "parseurl": "^1.3.3", "physical-cpu-count": "^2.0.0", "platform": "^1.3.6", - "postcss": "^8.3.9", + "postcss": "^8.3.11", "postcss-flexbugs-fixes": "^5.0.2", "postcss-loader": "^5.3.0", "prompts": "^2.4.2", @@ -25834,8 +24605,8 @@ "raw-loader": "^4.0.2", "react-dev-utils": "^11.0.4", "react-refresh": "^0.9.0", - "redux": "4.0.5", - "redux-thunk": "^2.3.0", + "redux": "4.1.2", + "redux-thunk": "^2.4.0", "resolve-from": "^5.0.0", "semver": "^7.3.5", "shallow-compare": "^1.2.2", @@ -25857,12 +24628,12 @@ "url-loader": "^4.1.1", "uuid": "^8.3.2", "v8-compile-cache": "^2.3.0", - "webpack": "^5.58.1", + "webpack": "^5.61.0", "webpack-dev-middleware": "^4.3.0", "webpack-merge": "^5.8.0", "webpack-stats-plugin": "^1.0.3", "webpack-virtual-modules": "^0.3.2", - "xstate": "^4.25.0", + "xstate": "^4.26.0", "yaml-loader": "^0.6.0" }, "dependencies": { @@ -25883,9 +24654,9 @@ }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -25899,84 +24670,32 @@ "requires": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - } - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "peer": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" + "minimatch": "^3.0.4" }, "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "peer": true + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } } } }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { - "color-name": "~1.1.4" + "sprintf-js": "~1.0.2" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -25990,11 +24709,6 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, "eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", @@ -26051,9 +24765,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -26068,23 +24782,6 @@ } } }, - "eslint-config-react-app": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", - "integrity": "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==", - "requires": { - "confusing-browser-globals": "^1.0.10" - } - }, - "eslint-plugin-flowtype": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", - "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", - "requires": { - "lodash": "^4.17.15", - "string-natural-compare": "^3.0.1" - } - }, "eslint-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", @@ -26100,19 +24797,6 @@ } } }, - "eslint-webpack-plugin": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.5.4.tgz", - "integrity": "sha512-7rYh0m76KyKSDE+B+2PUQrlNS4HJ51t3WKpkJg6vo2jFMbEPTG99cBV0Dm7LXSHucN4WGCG65wQcRiTFrj7iWw==", - "requires": { - "@types/eslint": "^7.2.6", - "arrify": "^2.0.1", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "normalize-path": "^3.0.0", - "schema-utils": "^3.0.0" - } - }, "espree": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", @@ -26138,24 +24822,6 @@ "is-glob": "^4.0.1" } }, - "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", - "requires": { - "type-fest": "^0.20.2" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -26173,36 +24839,6 @@ "yallist": "^4.0.0" } }, - "query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", - "requires": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, - "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -26211,11 +24847,6 @@ "lru-cache": "^6.0.0" } }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -26231,19 +24862,6 @@ } } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -26252,9 +24870,9 @@ } }, "gatsby-cli": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gatsby-cli/-/gatsby-cli-4.1.4.tgz", - "integrity": "sha512-B8PYO0FPZMuaEJnvLDPPfEO+xHci7pWoS0NnEKKd+12kneObi4doQp0vqqAG+PS3oNtBRGy1nLglf8BTpCex8Q==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-cli/-/gatsby-cli-4.2.0.tgz", + "integrity": "sha512-WC8sIdMpzTSsLcbvrvrfYazq1exTM+lZBoibLTxCBqKVcQ3dNMCSbzIbGieLtKaPs4pOKKvkivOSWEfPqMOdug==", "requires": { "@babel/code-frame": "^7.14.0", "@babel/runtime": "^7.15.4", @@ -26266,25 +24884,25 @@ "common-tags": "^1.8.0", "configstore": "^5.0.1", "convert-hrtime": "^3.0.0", - "create-gatsby": "^2.1.1", + "create-gatsby": "^2.2.0", "envinfo": "^7.8.1", "execa": "^5.1.1", "fs-exists-cached": "^1.0.0", "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-recipes": "^1.1.3", - "gatsby-telemetry": "^3.1.3", + "gatsby-core-utils": "^3.2.0", + "gatsby-recipes": "^1.2.0", + "gatsby-telemetry": "^3.2.0", "hosted-git-info": "^3.0.8", "is-valid-path": "^0.1.1", "joi": "^17.4.2", "lodash": "^4.17.21", "meant": "^1.0.3", - "node-fetch": "^2.6.5", + "node-fetch": "^2.6.6", "opentracing": "^0.14.5", "pretty-error": "^2.1.2", "progress": "^2.0.3", - "prompts": "^2.4.1", - "redux": "4.0.5", + "prompts": "^2.4.2", + "redux": "4.1.2", "resolve-cwd": "^3.0.0", "semver": "^7.3.5", "signal-exit": "^3.0.5", @@ -26303,41 +24921,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -26346,15 +24929,6 @@ "yallist": "^4.0.0" } }, - "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -26363,11 +24937,6 @@ "lru-cache": "^6.0.0" } }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -26376,14 +24945,6 @@ "ansi-regex": "^4.1.0" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -26397,9 +24958,9 @@ } }, "gatsby-core-utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-3.1.3.tgz", - "integrity": "sha512-+pg2i3DYVLzmJ/67SVGM+zVxerilGCbcgVxDoN58Y+Htv5TwogUWzPymfoFrJEsWGhlVKlYq7I8jVWSXPzwMHw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-3.2.0.tgz", + "integrity": "sha512-gPz02QD1kOGQmu49TZL8Fdo9rX8QBsA7XID0oXyIkZqkK80Tm1Uq1pOOfPE3cWSMEkzc71M79iKISCntk/wNuw==", "requires": { "@babel/runtime": "^7.15.4", "ci-info": "2.0.0", @@ -26407,24 +24968,24 @@ "file-type": "^16.5.3", "fs-extra": "^10.0.0", "got": "^11.8.2", - "node-object-hash": "^2.3.9", + "node-object-hash": "^2.3.10", "proper-lockfile": "^4.1.2", "tmp": "^0.2.1", "xdg-basedir": "^4.0.0" } }, "gatsby-graphiql-explorer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.1.0.tgz", - "integrity": "sha512-Gxr5Vv+Cmcxts/699FPnvp6t+xjtYUZsXyrJ4HoWEA8VU0h1KFVy56CtuZX8oMErTXMOkoueX89ty7b3ktJERw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.2.0.tgz", + "integrity": "sha512-K8LCrG4d9eFhyyHs4AKT1TKTm1Sw2zyk211TuKcTMwic3Qq0ldDUl75GCEMbMFxxyaqiNNjjhEx6ayySZ3ewJA==", "requires": { "@babel/runtime": "^7.15.4" } }, "gatsby-legacy-polyfills": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.1.0.tgz", - "integrity": "sha512-clDAz0Iv18bLPxfmg14YiL5nt/pCBUqFQcpswSUatfSo6O/PR3L5G8gRJNhgCVdaGp24opcOvh8y+sZWKza5rA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.2.0.tgz", + "integrity": "sha512-4WICOoxdsfjfVK369m/fjcTvneUC0noTvdFwWNSItfKnCau4MGNPyXJRP74xgfOIC/ST3KasO6XFdTnGdlWy0A==", "requires": { "@babel/runtime": "^7.15.4", "core-js-compat": "3.9.0" @@ -26447,9 +25008,9 @@ } }, "gatsby-link": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/gatsby-link/-/gatsby-link-4.1.0.tgz", - "integrity": "sha512-igOvc/ks6XNwT4vpwViC3n7KthP16XlKWejvIZA8yLKIwkOszcgV5/PYMTri8e2C2xpUAeutTreBWfmRFNcWtw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-link/-/gatsby-link-4.2.0.tgz", + "integrity": "sha512-R3I+rlkVcgDMEOv8MJGi9HdH4CSHyk7qQ9bZ2/HmtGLwEdhNbOlL48PGsCTq9MjGeq+XtWS4/R298TFr0ot1lQ==", "requires": { "@babel/runtime": "^7.15.4", "@types/reach__router": "^1.3.9", @@ -26457,42 +25018,42 @@ } }, "gatsby-page-utils": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/gatsby-page-utils/-/gatsby-page-utils-2.1.3.tgz", - "integrity": "sha512-5CrSKsXXF6gk6ZxueyE1z1HHZ4I/+y4nEcYliTl7fpNG+dGIKgmuQIjJoigcvZGEVhn9gk1BTDjBFUuXGrSgNA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-page-utils/-/gatsby-page-utils-2.2.0.tgz", + "integrity": "sha512-Q+RzAXTtyehPEFNNry+0iQuifOGZ/LrEljfIulJu9UZ+DXHzTI3KL/jugQ/UTHdyK2kIv0kssL7WM49ije7wcA==", "requires": { "@babel/runtime": "^7.15.4", "bluebird": "^3.7.2", "chokidar": "^3.5.2", "fs-exists-cached": "^1.0.0", - "gatsby-core-utils": "^3.1.3", - "glob": "^7.1.7", + "gatsby-core-utils": "^3.2.0", + "glob": "^7.2.0", "lodash": "^4.17.21", "micromatch": "^4.0.4" } }, "gatsby-plugin-page-creator": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.1.4.tgz", - "integrity": "sha512-MLs/C0Tc+uzRDmiWZyXdE5plDG/DeRnCgsd4iXBkeClzdVyToJSQWNlA30knMAS2jbtZdmYxQIPLvwO3CYd4/Q==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.2.0.tgz", + "integrity": "sha512-jezKI/ct5gRCPe9VZWxsdA69/IqcPBTbCLZMCPRXEg7PMoCzxmEZEKiA7mFZugRTu4TH8xdMiyvg3uaSdPLBYg==", "requires": { "@babel/runtime": "^7.15.4", "@babel/traverse": "^7.15.4", "@sindresorhus/slugify": "^1.1.2", "chokidar": "^3.5.2", "fs-exists-cached": "^1.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-page-utils": "^2.1.3", - "gatsby-plugin-utils": "^2.1.1", - "gatsby-telemetry": "^3.1.3", + "gatsby-core-utils": "^3.2.0", + "gatsby-page-utils": "^2.2.0", + "gatsby-plugin-utils": "^2.2.0", + "gatsby-telemetry": "^3.2.0", "globby": "^11.0.4", "lodash": "^4.17.21" } }, "gatsby-plugin-typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.1.3.tgz", - "integrity": "sha512-1yc3q6rszJztj6xd1phd6DnmYAlK7H3RBRT0Oy+tcdphkd307CktDJdrT+ix93O/7UEZnu9nGM5IC8DRCNdYpA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.2.0.tgz", + "integrity": "sha512-nQB0FS8vpo206iy/BTisfeyv8MZcXFarEL94tJPQ88AyC8kRCM7m4yCM4uknDGnTfYApqf1GHbDKn6SeCimtog==", "requires": { "@babel/core": "^7.15.5", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", @@ -26500,36 +25061,35 @@ "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/preset-typescript": "^7.15.0", "@babel/runtime": "^7.15.4", - "babel-plugin-remove-graphql-queries": "^4.1.3" + "babel-plugin-remove-graphql-queries": "^4.2.0" } }, "gatsby-plugin-use-query-params": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-use-query-params/-/gatsby-plugin-use-query-params-1.0.1.tgz", - "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==", - "requires": {} + "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==" }, "gatsby-plugin-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gatsby-plugin-utils/-/gatsby-plugin-utils-2.1.1.tgz", - "integrity": "sha512-LzLLSg9CWCi4qMqYNYkt8vAoySkNWKfB8QKJKnpU5qzo/3vjZAx9iWHmoNRYLCCHg1bgJn2Kr5cwOMYAEJspyg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gatsby-plugin-utils/-/gatsby-plugin-utils-2.2.0.tgz", + "integrity": "sha512-VcrvYk6LxsTs7DqzNCT7iYaqEoMuKr2gXO5HFG1sBMwS6XxLWUe47+CuenOvUcKn71C9a/Ir2pove62+0Xphjw==", "requires": { "@babel/runtime": "^7.15.4", "joi": "^17.4.2" } }, "gatsby-react-router-scroll": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.1.0.tgz", - "integrity": "sha512-hIFhYFScalUremdj8OfRow2U7E0+ULt5QgVsKsyPV7NbM1876iAZZhyzxK83jvoULKmhm2TyEgdVavylMCO3uA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.2.0.tgz", + "integrity": "sha512-iKzlCgmbOrWVr/5O4bZVLSXg12ZQSQLAvr+2HN/SGKFlF/cObb/znmu/oSssD4A6c3SN8XKv3d0XNUiADio2+Q==", "requires": { "@babel/runtime": "^7.15.4" } }, "gatsby-recipes": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gatsby-recipes/-/gatsby-recipes-1.1.3.tgz", - "integrity": "sha512-AdO9Y7vtpa0VSWXe3gZIeIUDJ6/j/MeAkpkuJHbbU8hix9gtbVPoOcmF8VFJUMALNRzhhFsgqRN2mK6jXJlang==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gatsby-recipes/-/gatsby-recipes-1.2.0.tgz", + "integrity": "sha512-v76Kt1EYBHwf5c9Ic8b0w/eEaDaRh3B/6spAVU6zN+VzEvQX3Oi/VckUyS2/anBOuSnOl3PJVWhFA3/aZpGuxw==", "requires": { "@babel/core": "^7.15.5", "@babel/generator": "^7.15.4", @@ -26555,8 +25115,8 @@ "express": "^4.17.1", "express-graphql": "^0.12.0", "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", - "gatsby-telemetry": "^3.1.3", + "gatsby-core-utils": "^3.2.0", + "gatsby-telemetry": "^3.2.0", "glob": "^7.1.6", "graphql": "^15.4.0", "graphql-compose": "~7.25.0", @@ -26572,7 +25132,7 @@ "mkdirp": "^0.5.1", "node-fetch": "^2.5.0", "pkg-dir": "^4.2.0", - "prettier": "^2.3.2", + "prettier": "^2.4.1", "prop-types": "^15.6.1", "remark-mdx": "^2.0.0-next.4", "remark-mdxjs": "^2.0.0-next.4", @@ -26667,9 +25227,9 @@ } }, "gatsby-telemetry": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-3.1.3.tgz", - "integrity": "sha512-vZSj67F4vzoqqK5CyNIRTDTWOqhBYkMey+i6swY3H51tehGTHRyzzOhJorQOJa76cQOcOMXr1/vvxX1T84Y96g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-3.2.0.tgz", + "integrity": "sha512-nLs/PPTPn7xPOiJxRe1Lmd8C0EVaH4rPT3KRT36ftaJBVDT5XhKfhR/tW9zirADD1k6pYW6vYvAQNFfKG5dpDg==", "requires": { "@babel/code-frame": "^7.14.0", "@babel/runtime": "^7.15.4", @@ -26679,21 +25239,13 @@ "boxen": "^4.2.0", "configstore": "^5.0.1", "fs-extra": "^10.0.0", - "gatsby-core-utils": "^3.1.3", + "gatsby-core-utils": "^3.2.0", "git-up": "^4.0.5", "is-docker": "^2.2.1", "lodash": "^4.17.21", - "node-fetch": "^2.6.5" + "node-fetch": "^2.6.6" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -26723,32 +25275,6 @@ "supports-color": "^7.1.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -26757,9 +25283,9 @@ } }, "gatsby-worker": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gatsby-worker/-/gatsby-worker-1.1.0.tgz", - "integrity": "sha512-+Ezi+lO+3bwPIgoGlcP7YAVFpn3iI8E44SwbFOvq9Mrfikdy81iWfOHAXB8TVJ6f0H0vATyJ9EoOAF0bZJWouQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gatsby-worker/-/gatsby-worker-1.2.0.tgz", + "integrity": "sha512-PxWs876LMSwu6gfZXqJiUFrIgnWPa8p4MysD9PhLd2+apaRsZJyMrPE3lblGQ62MehEAD5jpLDS0CpM6BaUCkA==", "requires": { "@babel/core": "^7.15.5", "@babel/runtime": "^7.15.4" @@ -26840,6 +25366,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -26893,9 +25420,12 @@ } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "requires": { + "type-fest": "^0.20.2" + } }, "globby": { "version": "11.0.4", @@ -26908,19 +25438,26 @@ "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" + } } }, "got": { - "version": "11.8.2", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", - "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "version": "11.8.3", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", + "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", "requires": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.1", + "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", @@ -27006,14 +25543,12 @@ "graphql-type-json": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", - "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", - "requires": {} + "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==" }, "graphql-ws": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", - "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", - "requires": {} + "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==" }, "gzip-size": { "version": "5.1.1", @@ -27043,9 +25578,9 @@ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-symbols": { "version": "1.0.2", @@ -27191,9 +25726,9 @@ "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==" }, "htmlparser2": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.1.2.tgz", - "integrity": "sha512-d6cqsbJba2nRdg8WW2okyD4ceonFHn9jLFxhwlNcLhQWcFPdxXeJulgOLjLKtAK9T6ahd+GQNZwG9fjmGW7lyg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", "dev": true, "requires": { "domelementtype": "^2.0.1", @@ -27261,8 +25796,7 @@ "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" }, "ieee754": { "version": "1.2.1", @@ -27270,9 +25804,9 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, "immer": { "version": "8.0.1", @@ -27370,49 +25904,6 @@ "type-fest": "^0.21.3" } }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -27835,8 +26326,7 @@ "isomorphic-ws": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "requires": {} + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==" }, "iterall": { "version": "1.3.0", @@ -27854,14 +26344,6 @@ "pretty-format": "^25.5.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -27870,32 +26352,6 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -27912,21 +26368,6 @@ "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } } }, "joi": { @@ -27965,6 +26406,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { "argparse": "^2.0.1" } @@ -28089,16 +26531,16 @@ "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==" }, "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "lmdb-store": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/lmdb-store/-/lmdb-store-1.6.13.tgz", - "integrity": "sha512-WJPNfzSZXD6anGFdIEK/wq/HzAU5kfi7+LSUSzQ2Qo9uV9REeIYPGqWX+FKl/QCb6qK4ie1D4f44aEvvv7M7rw==", + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/lmdb-store/-/lmdb-store-1.6.14.tgz", + "integrity": "sha512-4woZfvfgolMEngjoMJrwePjdLotr3QKGJsDWURlJmKBed5JtE00IfAKo7ryPowl4ksGcs21pcdLkwrPnKomIuA==", "requires": { - "msgpackr": "^1.4.7", + "msgpackr": "^1.5.0", "nan": "^2.14.2", "node-gyp-build": "^4.2.3", "ordered-binary": "^1.0.0", @@ -28357,12 +26799,13 @@ } }, "mdast-util-from-markdown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.1.0.tgz", - "integrity": "sha512-Mex7IIeIKRpGYNNywpxTfPhfFBTxBL5IVacPMU6GjYF+EkIvy++19cBgxVFyHVd2JpC/chG2IKGqZLffoo7Q1g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", + "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", "requires": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", "mdast-util-to-string": "^3.1.0", "micromark": "^3.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", @@ -28370,7 +26813,6 @@ "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", - "parse-entities": "^3.0.0", "unist-util-stringify-position": "^3.0.0", "uvu": "^0.5.0" } @@ -28394,14 +26836,14 @@ } }, "mdast-util-mdx-jsx": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-1.1.2.tgz", - "integrity": "sha512-1/bDUZvPGNVxiAjrVsehMZTaNomihpbIMoMr8/UcAQ4h7itDFhN93LtO9XzQPlEM+CMueCoRYGQfl7N+5R+8Mg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-1.2.0.tgz", + "integrity": "sha512-5+ot/kfxYd3ChgEMwsMUO71oAfYjyRI3pADEK4I7xTmWLGQ8Y7ghm1CG36zUoUvDPxMlIYwQV/9DYHAUWdG4dA==", "requires": { "@types/estree-jsx": "^0.0.1", "@types/mdast": "^3.0.0", "mdast-util-to-markdown": "^1.0.0", - "parse-entities": "^3.0.0", + "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-remove-position": "^4.0.0", "unist-util-stringify-position": "^3.0.0", @@ -28420,9 +26862,9 @@ } }, "mdast-util-to-markdown": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.2.4.tgz", - "integrity": "sha512-Wive3NvrNS4OY5yYKBADdK1QSlbJUZyZ2ssanITUzNQ7sxMfBANTVjLrAA9BFXshaeG9G77xpOK/z+TTret5Hg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.2.6.tgz", + "integrity": "sha512-doJZmTEGagHypWvJ8ltinmwUsT9ZaNgNIQW6Gl7jNdsI1QZkTHTimYW561Niy2s8AEPAqEgV0dIh2UOVlSXUJA==", "requires": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -28496,9 +26938,9 @@ } }, "memfs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", - "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.0.tgz", + "integrity": "sha512-o/RfP0J1d03YwsAxyHxAYs2kyJp55AFkMazlFAZFR2I2IXkxiUTXRabJ6RmNNCQ83LAD2jy52Khj0m3OffpNdA==", "requires": { "fs-monkey": "1.0.3" } @@ -28543,8 +26985,7 @@ "meros": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", - "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", - "requires": {} + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==" }, "methods": { "version": "1.1.2", @@ -28557,12 +26998,13 @@ "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==" }, "micromark": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.7.tgz", - "integrity": "sha512-67ipZ2CzQVsDyH1kqNLh7dLwe5QMPJwjFBGppW7JCLByaSc6ZufV0ywPOxt13MIDAzzmj3wctDL6Ov5w0fOHXw==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.9.tgz", + "integrity": "sha512-aWPjuXAqiFab4+oKLjH1vSNQm8S9GMnnf5sFNLrQaIggGYMBcQ9CS0Tt7+BJH6hbyv783zk3vgDhaORl3K33IQ==", "requires": { "@types/debug": "^4.0.0", "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", "micromark-core-commonmark": "^1.0.1", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", @@ -28576,7 +27018,6 @@ "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", - "parse-entities": "^3.0.0", "uvu": "^0.5.0" }, "dependencies": { @@ -28591,10 +27032,11 @@ } }, "micromark-core-commonmark": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.4.tgz", - "integrity": "sha512-HAtoZisp1M/sQFuw2zoUKGo1pMKod7GSvdM6B2oBU0U2CEN5/C6Tmydmi1rmvEieEhGQsjMyiiSoYgxISNxGFA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.5.tgz", + "integrity": "sha512-ZNtWumX94lpiyAu/lxvth6I5+XzxF+BLVUB7u60XzOBy4RojrbZqrx0mcRmbfqEMO6489vyvDfIQNv5hdulrPg==", "requires": { + "decode-named-character-reference": "^1.0.0", "micromark-factory-destination": "^1.0.0", "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", @@ -28609,14 +27051,13 @@ "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", - "parse-entities": "^3.0.0", "uvu": "^0.5.0" } }, "micromark-extension-mdx-expression": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.2.tgz", - "integrity": "sha512-KbkM9D9x/ZOV6gLwaN8izl2ZMI3sg+C4JLuUSmd8zJ4Z2d3mSWrznJRLuXkZcyN9lLaRm4Dz2VPjae3AkC5X1A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.3.tgz", + "integrity": "sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==", "requires": { "micromark-factory-mdx-expression": "^1.0.0", "micromark-factory-space": "^1.0.0", @@ -28703,9 +27144,9 @@ } }, "micromark-factory-mdx-expression": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.4.tgz", - "integrity": "sha512-1mS1Cg/GmvvafgHQvxz4OqhcO1JGwzzTuGh1C8NIUrmnr6/3A1dJiAphHz7kJKI/b9WIr69Q8VswuwyWo576yQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.5.tgz", + "integrity": "sha512-1DSMCBeCUj4m01P8uYbNWvOsv+FtpDTcBUcDCdE06sENTBX54lndRs9neWOgsNWfLDm2EzCyNKiUaoJ+mWa/WA==", "requires": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", @@ -28794,14 +27235,14 @@ } }, "micromark-util-decode-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.1.tgz", - "integrity": "sha512-Wf3H6jLaO3iIlHEvblESXaKAr72nK7JtBbLLICPwuZc3eJkMcp4j8rJ5Xv1VbQWMCWWDvKUbVUbE2MfQNznwTA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", "requires": { + "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "parse-entities": "^3.0.0" + "micromark-util-symbol": "^1.0.0" } }, "micromark-util-encode": { @@ -28870,9 +27311,9 @@ "integrity": "sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ==" }, "micromark-util-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.1.tgz", - "integrity": "sha512-UT0ylWEEy80RFYzK9pEaugTqaxoD/j0Y9WhHpSyitxd99zjoQz7JJ+iKuhPAgOW2MiPSUAx+c09dcqokeyaROA==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==" }, "micromatch": { "version": "4.0.4", @@ -28889,16 +27330,16 @@ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" }, "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.50.0" + "mime-db": "1.51.0" } }, "mimic-fn": { @@ -28994,10 +27435,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "msgpackr": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.4.7.tgz", - "integrity": "sha512-bhC8Ed1au3L3oHaR/fe4lk4w7PLGFcWQ5XY/Tk9N6tzDRz8YndjCG68TD8zcvYZoxNtw767eF/7VpaTpU9kf9w==", - "optional": true, + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.1.tgz", + "integrity": "sha512-I1CXFG8BYYSeIhtDlHpUVMsdDiyvP9JAh1d9QoBnkPx3ETPeH/1lR14hweM9GETs09wCWlaOyhtXxIc9boxAAA==", "requires": { "msgpackr-extract": "^1.0.14" } @@ -29413,9 +27853,9 @@ } }, "ordered-binary": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.1.3.tgz", - "integrity": "sha512-tDTls+KllrZKJrqRXUYJtIcWIyoQycP7cVN7kzNNnhHKF2bMKHflcAQK+pF2Eb1iVaQodHxqZQr0yv4HWLGBhQ==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.2.1.tgz", + "integrity": "sha512-Zl2RCcj/wRCakW9/yI83gutgNf7JFOPEHrCK72z+boIrU+PWAnIt6HADd1w+3keDQ90GCKbp1BduKZgkeNbz7A==" }, "os-tmpdir": { "version": "1.0.2", @@ -29629,14 +28069,15 @@ } }, "parse-entities": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-3.1.0.tgz", - "integrity": "sha512-xf2yeHbsfg1vJySsQelVwgtI/67eAndVU05skrr/XN6KFMoVVA95BYrW8y78OfW4jqcuHwB7tlMlLkvbq4WbHQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", + "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", "requires": { "@types/unist": "^2.0.0", "character-entities": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" @@ -29671,17 +28112,6 @@ "requires": { "side-channel": "^1.0.4" } - }, - "query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", - "requires": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } } } }, @@ -29903,13 +28333,13 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "postcss": { - "version": "8.3.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", - "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", + "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", "requires": { "nanoid": "^3.1.30", "picocolors": "^1.0.0", - "source-map-js": "^0.6.2" + "source-map-js": "^1.0.1" } }, "postcss-calc": { @@ -29943,32 +28373,27 @@ "postcss-discard-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", - "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "requires": {} + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", - "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "requires": {} + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", - "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "requires": {} + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", - "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "requires": {} + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" }, "postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "requires": {} + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" }, "postcss-loader": { "version": "5.3.0", @@ -30016,25 +28441,23 @@ } }, "postcss-merge-longhand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz", - "integrity": "sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz", + "integrity": "sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw==", "requires": { - "css-color-names": "^1.0.1", "postcss-value-parser": "^4.1.0", "stylehacks": "^5.0.1" } }, "postcss-merge-rules": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", - "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz", + "integrity": "sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg==", "requires": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", "cssnano-utils": "^2.0.1", - "postcss-selector-parser": "^6.0.5", - "vendors": "^1.0.3" + "postcss-selector-parser": "^6.0.5" } }, "postcss-minify-font-values": { @@ -30056,15 +28479,14 @@ } }, "postcss-minify-params": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", - "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz", + "integrity": "sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg==", "requires": { "alphanum-sort": "^1.0.2", - "browserslist": "^4.16.0", + "browserslist": "^4.16.6", "cssnano-utils": "^2.0.1", - "postcss-value-parser": "^4.1.0", - "uniqs": "^2.0.0" + "postcss-value-parser": "^4.1.0" } }, "postcss-minify-selectors": { @@ -30079,8 +28501,7 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -30111,8 +28532,7 @@ "postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", - "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "requires": {} + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -30167,9 +28587,9 @@ } }, "postcss-normalize-url": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", - "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.3.tgz", + "integrity": "sha512-qWiUMbvkRx3kc1Dp5opzUwc7MBWZcSDK2yofCmdvFBCpx+zFPkxBC1FASQ59Pt+flYfj/nTZSkmF56+XG5elSg==", "requires": { "is-absolute-url": "^3.0.3", "normalize-url": "^6.0.1", @@ -30194,11 +28614,11 @@ } }, "postcss-reduce-initial": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", - "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz", + "integrity": "sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw==", "requires": { - "browserslist": "^4.16.0", + "browserslist": "^4.16.6", "caniuse-api": "^3.0.0" } }, @@ -30230,19 +28650,18 @@ } }, "postcss-unique-selectors": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", - "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz", + "integrity": "sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA==", "requires": { "alphanum-sort": "^1.0.2", - "postcss-selector-parser": "^6.0.5", - "uniqs": "^2.0.0" + "postcss-selector-parser": "^6.0.5" } }, "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "prelude-ls": { "version": "1.2.1", @@ -30255,9 +28674,9 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz", + "integrity": "sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==" }, "prettier-linter-helpers": { "version": "1.0.0", @@ -30288,27 +28707,6 @@ "react-is": "^16.12.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -30409,10 +28807,9 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "query-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", - "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", - "peer": true, + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", "requires": { "decode-uri-component": "^0.2.0", "filter-obj": "^1.1.0", @@ -30529,8 +28926,7 @@ "react-alert-template-basic": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/react-alert-template-basic/-/react-alert-template-basic-1.0.2.tgz", - "integrity": "sha512-nJSIknM5xN/GzKdKkb5/X9zdiLxSqctww3mEouyeR891nUGP8guqFpCGD8vCjFYSZtq8z3tGcAKBG1XjtLHFHQ==", - "requires": {} + "integrity": "sha512-nJSIknM5xN/GzKdKkb5/X9zdiLxSqctww3mEouyeR891nUGP8guqFpCGD8vCjFYSZtq8z3tGcAKBG1XjtLHFHQ==" }, "react-aria-modal": { "version": "4.0.1", @@ -30540,50 +28936,12 @@ "focus-trap-react": "^8.1.0", "no-scroll": "^2.1.1", "react-displace": "^2.3.0" - }, - "dependencies": { - "focus-trap-react": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-8.8.2.tgz", - "integrity": "sha512-YgacIMxeAOytHOEbzBWL7+itBkn4ARMwQhtt6hYVHqHzPUPhmfEyKJ/nqsyMerzOK1DzlDv8Q8phRAY8vpa0rA==", - "requires": { - "focus-trap": "^6.7.1" - } - }, - "react-displace": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", - "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==", - "requires": {} - }, - "react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - } - }, - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } } }, "react-datepicker": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.3.0.tgz", - "integrity": "sha512-cg+gp4YnPcZc6iZ0+v7VuRgcFG22KL7izHYrCxXeSLOn5VGkyxTfcu5qA/cGIsngurCt/u0NtgVTlHB1Fwap1g==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.4.0.tgz", + "integrity": "sha512-QWCDCKTW9i+L33+yyYaDWZkVCDcFjru7wEmSqO6dNsGo4hSaReQ/EaT9IKzjEmvMXfMHn9ixzgGh/4ydnLP08w==", "requires": { "@popperjs/core": "^2.9.2", "classnames": "^2.2.6", @@ -30632,6 +28990,14 @@ "@babel/highlight": "^7.10.4" } }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, "browserslist": { "version": "4.14.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", @@ -30643,6 +29009,36 @@ "node-releases": "^1.1.61" } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -30670,6 +29066,16 @@ "slash": "^3.0.0" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==" + }, "loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -30730,9 +29136,22 @@ "requires": { "ansi-regex": "^5.0.0" } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, + "react-displace": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", + "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==" + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -30756,8 +29175,7 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", - "requires": {} + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "17.0.2", @@ -30770,10 +29188,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-onclickoutside": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.0.tgz", - "integrity": "sha512-oPlOTYcISLHfpMog2lUZMFSbqOs4LFcA4+vo7fpfevB5v9Z0D5VBDBkfeO5lv+hpEcGoaGk67braLT+QT+eICA==", - "requires": {} + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", + "integrity": "sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==" }, "react-popper": { "version": "2.2.5", @@ -30799,8 +29216,7 @@ "react-table": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz", - "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==", - "requires": {} + "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==" }, "react-transition-group": { "version": "4.4.2", @@ -30885,16 +29301,14 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "peer": true, "requires": { "@babel/runtime": "^7.9.2" } }, "redux-thunk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.0.tgz", - "integrity": "sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==", - "requires": {} + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" }, "regenerate": { "version": "1.4.2", @@ -31065,6 +29479,11 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -31560,8 +29979,7 @@ "serialize-query-params": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-1.3.5.tgz", - "integrity": "sha512-BrLH1RqgzVxm6dco+KP9S6BodeFiUVvKwtY3GSWQlupIdblT19KCGTRkHZ2yIU6Bjy0Prjts0tYe11VpTMbAeQ==", - "requires": {} + "integrity": "sha512-BrLH1RqgzVxm6dco+KP9S6BodeFiUVvKwtY3GSWQlupIdblT19KCGTRkHZ2yIU6Bjy0Prjts0tYe11VpTMbAeQ==" }, "serve-static": { "version": "1.14.1", @@ -31657,9 +30075,9 @@ } }, "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "single-trailing-newline": { "version": "1.0.0", @@ -31687,35 +30105,12 @@ "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } } }, "slugify": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.2.tgz", - "integrity": "sha512-XMtI8qD84LwCpthLMBHlIhcrj10cgA+U/Ot8G6FD6uFuWZtMfKK75JO7l81nzpFJsPlsW6LT+VKqWQJW3+6New==" + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.3.tgz", + "integrity": "sha512-1MPyqnIhgiq+/0iDJyqSJHENdnH5MMIlgJIBxmkRMzTNKlS/QsN5dXsB+MdDq4E6w0g9jFA4XOTRkVDjDae/2w==" }, "snapdragon": { "version": "0.8.2", @@ -31821,6 +30216,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -31918,14 +30318,14 @@ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, "source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" }, "source-map-resolve": { "version": "0.5.3", @@ -31940,9 +30340,9 @@ } }, "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -32278,6 +30678,21 @@ "hoist-non-react-statics": "^3.0.0", "shallowequal": "^1.1.0", "supports-color": "^5.5.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "stylehacks": { @@ -32314,11 +30729,11 @@ "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==" }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } }, "svgo": { @@ -32367,9 +30782,9 @@ }, "dependencies": { "ajv": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", - "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -32395,9 +30810,9 @@ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -32408,11 +30823,6 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, @@ -32428,15 +30838,10 @@ "terser": "^5.7.2" }, "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, "jest-worker": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", - "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.0.tgz", + "integrity": "sha512-4WuKcUxtzxBoKOUFbt1MtTY9fJwPVD4aN/4Cgxee7OLetPZn5as2bjfZz98XSf2Zq1JFfhqPZpS+43BmWXKgCA==", "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -32623,9 +31028,9 @@ } }, "tsconfig-paths": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", "requires": { "@types/json5": "^0.0.29", "json5": "^1.0.1", @@ -32670,11 +31075,9 @@ } }, "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "optional": true, - "peer": true + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" }, "type-is": { "version": "1.6.18", @@ -32703,12 +31106,6 @@ "is-typedarray": "^1.0.0" } }, - "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", - "peer": true - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -32788,11 +31185,6 @@ } } }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" - }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -32974,41 +31366,6 @@ "xdg-basedir": "^4.0.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -33025,14 +31382,6 @@ "lru-cache": "^6.0.0" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -33170,11 +31519,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" - }, "vfile": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", @@ -33228,9 +31572,9 @@ } }, "watchpack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", - "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz", + "integrity": "sha512-MnN0Q1OsvB/GGHETrFeZPQaOelWh/7O+EiFlj8sM9GPjtQkis7k01aAxrg/18kTfoIVcLL+haEVFlXDaSRwKRw==", "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -33247,9 +31591,9 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "webpack": { - "version": "5.62.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.62.1.tgz", - "integrity": "sha512-jNLtnWChS2CMZ7vqWtztv0G6fYB5hz11Zsadp5tE7e4/66zVDj7/KUeQZOsOl8Hz5KrLJH1h2eIDl6AnlyE12Q==", + "version": "5.64.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz", + "integrity": "sha512-LWhqfKjCLoYJLKJY8wk2C3h77i8VyHowG3qYNZiIqD6D0ZS40439S/KVuc/PY48jp2yQmy0mhMknq8cys4jFMw==", "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -33273,8 +31617,8 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.0" + "watchpack": "^2.3.0", + "webpack-sources": "^3.2.2" }, "dependencies": { "schema-utils": { @@ -33293,9 +31637,9 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "webpack-sources": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", - "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", + "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==" } } }, @@ -33440,29 +31784,6 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } } }, "wrappy": { @@ -33482,10 +31803,9 @@ } }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "requires": {} + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==" }, "xdg-basedir": { "version": "4.0.0", @@ -33514,9 +31834,9 @@ } }, "xstate": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.26.0.tgz", - "integrity": "sha512-l0tfRBhVYM17D6IWT4pVOzzN9kY/5lnPWCe4LXjJ3F9HCrJOPBn6tPRAb9mapSRBS8cOeByJFDCRSNopgaoC5w==" + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.26.1.tgz", + "integrity": "sha512-JLofAEnN26l/1vbODgsDa+Phqa61PwDlxWu8+2pK+YbXf+y9pQSDLRvcYH2H1kkeUBA5fGp+xFL/zfE8jNMw4g==" }, "xtend": { "version": "4.0.2", @@ -33661,6 +31981,47 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -33668,6 +32029,14 @@ "requires": { "ansi-regex": "^4.1.0" } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, diff --git a/ui/package.json b/ui/package.json index 02ae2adf4..593c22aee 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,9 +17,9 @@ "lint:fix": "eslint *.js src --fix" }, "dependencies": { - "date-fns": "^2.25.0", + "date-fns": "^2.26.0", "formik": "^2.2.9", - "gatsby": "^4.1.6", + "gatsby": "^4.2.0", "gatsby-plugin-use-query-params": "^1.0.1", "prop-types": "^15.7.2", "react": "^17.0.2", @@ -36,15 +36,15 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.16.3", - "eslint": "^8.2.0", - "eslint-config-airbnb": "^18.2.1", + "eslint": "^8.3.0", + "eslint-config-airbnb": "^19.0.1", "eslint-config-prettier": "^8.3.0", "eslint-config-wesbos": "^2.1.0", "eslint-plugin-html": "^6.2.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.3.0", "prettier": "^2.4.1" }, @@ -67,6 +67,10 @@ "Field" ] } + ], + "react/no-unstable-nested-components": [ + "error", + { "allowAsProps": true } ] } } diff --git a/ui/src/components/createTokenButton.js b/ui/src/components/createTokenButton.js index 7441aed8d..bdbfb083f 100644 --- a/ui/src/components/createTokenButton.js +++ b/ui/src/components/createTokenButton.js @@ -18,7 +18,7 @@ const StyledModal = styled.div` max-width: 100%; `; -function NewToken({ token, onAccept }) { +const NewToken = function ({ token, onAccept }) { return ( <>

Your new token is:

@@ -35,13 +35,13 @@ function NewToken({ token, onAccept }) { ); -} +}; NewToken.propTypes = { token: PropTypes.string.isRequired, onAccept: PropTypes.func.isRequired, }; -export default function CreateTokenButton({ onCreate }) { +const CreateTokenButton = function ({ onCreate }) { const { csrf, username, userScopes, config } = useContext(LoginContext); const [formActive, setFormActive] = useState(false); const [newToken, setNewToken] = useState(''); @@ -100,7 +100,9 @@ export default function CreateTokenButton({ onCreate }) { )} ); -} +}; CreateTokenButton.propTypes = { onCreate: PropTypes.func.isRequired, }; + +export default CreateTokenButton; diff --git a/ui/src/components/editTokenModal.js b/ui/src/components/editTokenModal.js index c0b9000f9..8794c2759 100644 --- a/ui/src/components/editTokenModal.js +++ b/ui/src/components/editTokenModal.js @@ -7,7 +7,7 @@ import { LoginContext } from './loginContext.js'; import TokenModal from './tokenModal.js'; import { apiGet, apiPatch } from '../functions/api.js'; -export default function EditTokenModal({ token, onSuccess, onExit }) { +const EditTokenModal = function ({ token, onSuccess, onExit }) { const alert = useAlert(); const { csrf, username, userScopes, config } = useContext(LoginContext); const [tokenData, setTokenData] = useState(null); @@ -59,9 +59,11 @@ export default function EditTokenModal({ token, onSuccess, onExit }) { onExit={onExit} /> ); -} +}; EditTokenModal.propTypes = { token: PropTypes.string.isRequired, onSuccess: PropTypes.func.isRequired, onExit: PropTypes.func.isRequired, }; + +export default EditTokenModal; diff --git a/ui/src/components/layout.js b/ui/src/components/layout.js index 839b90a8e..d3abf41ac 100644 --- a/ui/src/components/layout.js +++ b/ui/src/components/layout.js @@ -1,26 +1,35 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useMemo } from 'react'; import { positions, Provider as AlertProvider, useAlert } from 'react-alert'; import AlertTemplate from 'react-alert-template-basic'; import { LoginContext } from './loginContext'; import useLogin from '../hooks/login'; -function Application({ children }) { +const Application = function ({ children }) { const alert = useAlert(); const { csrf, username, userScopes, config } = useLogin(alert); + const value = useMemo( + () => ({ + csrf, + username, + userScopes, + config, + }), + [csrf, username, userScopes, config] + ); return ( - +
{children}
); -} +}; Application.propTypes = { children: PropTypes.element.isRequired, }; -export default function Layout({ children }) { +const Layout = function ({ children }) { return ( {children} ); -} +}; Layout.propTypes = { children: PropTypes.element.isRequired, }; + +export default Layout; diff --git a/ui/src/components/timestamp.js b/ui/src/components/timestamp.js index e46655465..e9165762e 100644 --- a/ui/src/components/timestamp.js +++ b/ui/src/components/timestamp.js @@ -3,7 +3,7 @@ import fromUnixTime from 'date-fns/fromUnixTime'; import PropTypes from 'prop-types'; import React from 'react'; -export default function Timestamp({ +const Timestamp = function ({ timestamp, expiration = false, className = undefined, @@ -24,9 +24,11 @@ export default function Timestamp({ {relative} ); -} +}; Timestamp.propTypes = { timestamp: PropTypes.number.isRequired, expiration: PropTypes.bool, className: PropTypes.string, }; + +export default Timestamp; diff --git a/ui/src/components/token.js b/ui/src/components/token.js index 8e85b1968..b0363c32d 100644 --- a/ui/src/components/token.js +++ b/ui/src/components/token.js @@ -2,7 +2,7 @@ import { Link } from 'gatsby'; import PropTypes from 'prop-types'; import React from 'react'; -export default function Token({ token, link = true }) { +const Token = function ({ token, link = true }) { return link ? ( {token} @@ -10,8 +10,10 @@ export default function Token({ token, link = true }) { ) : ( {token} ); -} +}; Token.propTypes = { token: PropTypes.string.isRequired, link: PropTypes.bool, }; + +export default Token; diff --git a/ui/src/components/tokenChangeHistory.js b/ui/src/components/tokenChangeHistory.js index c8c6b57fe..779ed721d 100644 --- a/ui/src/components/tokenChangeHistory.js +++ b/ui/src/components/tokenChangeHistory.js @@ -12,7 +12,7 @@ import TokenChangeTable from './tokenChangeTable'; import { LoginContext } from './loginContext'; import { apiGet } from '../functions/api'; -export default function TokenChangeHistory({ token }) { +const TokenChangeHistory = function ({ token }) { const alert = useAlert(); const { username } = useContext(LoginContext); const [_data, setData] = useState([]); @@ -28,7 +28,9 @@ export default function TokenChangeHistory({ token }) { useEffect(loadHistory, [loadHistory, token, username]); return ; -} +}; TokenChangeHistory.propTypes = { token: PropTypes.string.isRequired, }; + +export default TokenChangeHistory; diff --git a/ui/src/components/tokenChangeSearch.js b/ui/src/components/tokenChangeSearch.js index 6ed039fc5..be687d855 100644 --- a/ui/src/components/tokenChangeSearch.js +++ b/ui/src/components/tokenChangeSearch.js @@ -86,7 +86,7 @@ function exceptionToErrors(e, alert) { return errors; } -export default function TokenChangeSearch({ query, setQuery }) { +const TokenChangeSearch = function ({ query, setQuery }) { const alert = useAlert(); const { username } = useContext(LoginContext); const [_data, setData] = useState(null); @@ -120,7 +120,7 @@ export default function TokenChangeSearch({ query, setQuery }) { )} ); -} +}; TokenChangeSearch.propTypes = { query: PropTypes.shape({ key: PropTypes.string, @@ -131,3 +131,5 @@ TokenChangeSearch.propTypes = { }).isRequired, setQuery: PropTypes.func.isRequired, }; + +export default TokenChangeSearch; diff --git a/ui/src/components/tokenChangeSearchForm.js b/ui/src/components/tokenChangeSearchForm.js index f21ecebe6..67406a920 100644 --- a/ui/src/components/tokenChangeSearchForm.js +++ b/ui/src/components/tokenChangeSearchForm.js @@ -5,7 +5,7 @@ import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -export default function TokenChangeSearchForm({ initialValues, onSubmit }) { +const TokenChangeSearchForm = function ({ initialValues, onSubmit }) { return ( ); -} +}; TokenChangeSearchForm.propTypes = { initialValues: PropTypes.shape({ key: PropTypes.string, @@ -92,3 +92,5 @@ TokenChangeSearchForm.propTypes = { }).isRequired, onSubmit: PropTypes.func.isRequired, }; + +export default TokenChangeSearchForm; diff --git a/ui/src/components/tokenChangeTable.js b/ui/src/components/tokenChangeTable.js index b2c40e044..7f02e9f8e 100644 --- a/ui/src/components/tokenChangeTable.js +++ b/ui/src/components/tokenChangeTable.js @@ -5,7 +5,7 @@ import { useTable } from 'react-table'; import Timestamp from './timestamp'; import Token from './token'; -export default function TokenChangeTable({ data }) { +const TokenChangeTable = function ({ data }) { const columns = useMemo( () => [ { @@ -116,7 +116,9 @@ export default function TokenChangeTable({ data }) { ); /* eslint-enable react/jsx-props-no-spreading */ -} +}; TokenChangeTable.propTypes = { data: PropTypes.arrayOf(PropTypes.object).isRequired, }; + +export default TokenChangeTable; diff --git a/ui/src/components/tokenData.js b/ui/src/components/tokenData.js index 62001a187..66cee78fd 100644 --- a/ui/src/components/tokenData.js +++ b/ui/src/components/tokenData.js @@ -15,7 +15,7 @@ import TokenChangeHistory from './tokenChangeHistory'; import TokenName from './tokenName'; import { apiGet } from '../functions/api'; -export default function TokenData({ token }) { +const TokenData = function ({ token }) { const alert = useAlert(); const { username } = useContext(LoginContext); const [_tokenData, setTokenData] = useState(null); @@ -104,7 +104,9 @@ export default function TokenData({ token }) { ); -} +}; TokenData.propTypes = { token: PropTypes.string.isRequired, }; + +export default TokenData; diff --git a/ui/src/components/tokenForm.js b/ui/src/components/tokenForm.js index f96da8b65..6cf9deed9 100644 --- a/ui/src/components/tokenForm.js +++ b/ui/src/components/tokenForm.js @@ -23,7 +23,7 @@ function calculateExpires({ return getUnixTime(date); } -export default function TokenForm({ +const TokenForm = function ({ idPrefix, buttonLabel, name = '', @@ -158,7 +158,7 @@ export default function TokenForm({ )} ); -} +}; TokenForm.propTypes = { idPrefix: PropTypes.string.isRequired, buttonLabel: PropTypes.string.isRequired, @@ -175,3 +175,5 @@ TokenForm.propTypes = { onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, }; + +export default TokenForm; diff --git a/ui/src/components/tokenList.js b/ui/src/components/tokenList.js index c26bcfb1e..40c485785 100644 --- a/ui/src/components/tokenList.js +++ b/ui/src/components/tokenList.js @@ -13,7 +13,7 @@ import { LoginContext } from './loginContext.js'; import TokenTable from './tokenTable'; import { apiDelete, apiGet } from '../functions/api'; -export default function TokenList() { +const TokenList = function () { const alert = useAlert(); const { csrf, username } = useContext(LoginContext); const [data, setData] = useState(null); @@ -108,4 +108,6 @@ export default function TokenList() { ) : null} ); -} +}; + +export default TokenList; diff --git a/ui/src/components/tokenModal.js b/ui/src/components/tokenModal.js index a5a019b73..ec00f0d59 100644 --- a/ui/src/components/tokenModal.js +++ b/ui/src/components/tokenModal.js @@ -49,7 +49,7 @@ function exceptionToErrors(e, alert) { return errors; } -export default function TokenModal({ +const TokenModal = function ({ idPrefix, buttonLabel, name = '', @@ -88,7 +88,7 @@ export default function TokenModal({ ); -} +}; TokenModal.propTypes = { idPrefix: PropTypes.string.isRequired, buttonLabel: PropTypes.string.isRequired, @@ -105,3 +105,5 @@ TokenModal.propTypes = { onSubmit: PropTypes.func.isRequired, onExit: PropTypes.func.isRequired, }; + +export default TokenModal; diff --git a/ui/src/components/tokenName.js b/ui/src/components/tokenName.js index 0245100c1..e06744c07 100644 --- a/ui/src/components/tokenName.js +++ b/ui/src/components/tokenName.js @@ -1,9 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; -export default function TokenName({ name }) { +const TokenName = function ({ name }) { return {name}; -} +}; TokenName.propTypes = { name: PropTypes.string.isRequired, }; + +export default TokenName; diff --git a/ui/src/components/tokenTable.js b/ui/src/components/tokenTable.js index 25f477058..0ac2a521c 100644 --- a/ui/src/components/tokenTable.js +++ b/ui/src/components/tokenTable.js @@ -9,7 +9,7 @@ import Timestamp from './timestamp'; import Token from './token'; import TokenName from './tokenName'; -function DeleteTokenButton({ token, onDeleteToken }) { +const DeleteTokenButton = function ({ token, onDeleteToken }) { const onClick = () => { onDeleteToken(token); }; @@ -18,13 +18,13 @@ function DeleteTokenButton({ token, onDeleteToken }) { ); -} +}; DeleteTokenButton.propTypes = { token: PropTypes.string.isRequired, onDeleteToken: PropTypes.func.isRequired, }; -function EditTokenButton({ token, onEditToken }) { +const EditTokenButton = function ({ token, onEditToken }) { const onClick = () => { onEditToken(token); }; @@ -33,13 +33,13 @@ function EditTokenButton({ token, onEditToken }) { ); -} +}; EditTokenButton.propTypes = { token: PropTypes.string.isRequired, onEditToken: PropTypes.func.isRequired, }; -export default function TokenTable({ +const TokenTable = function ({ id, data, onEditToken, @@ -143,7 +143,7 @@ export default function TokenTable({ ); /* eslint-enable react/jsx-props-no-spreading */ -} +}; TokenTable.propTypes = { id: PropTypes.string.isRequired, data: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -151,3 +151,5 @@ TokenTable.propTypes = { onDeleteToken: PropTypes.func.isRequired, includeName: PropTypes.bool, }; + +export default TokenTable; diff --git a/ui/src/pages/changes.js b/ui/src/pages/changes.js index 2ee74179e..8082d6bcc 100644 --- a/ui/src/pages/changes.js +++ b/ui/src/pages/changes.js @@ -5,7 +5,7 @@ import { useQueryParams, StringParam, withDefault } from 'use-query-params'; import Layout from '../components/layout'; import TokenChangeSearch from '../components/tokenChangeSearch'; -export default function Changes() { +const Changes = function () { const [query, setQuery] = useQueryParams({ key: StringParam, tokenType: withDefault(StringParam, 'any'), @@ -22,4 +22,6 @@ export default function Changes() { ); -} +}; + +export default Changes; diff --git a/ui/src/pages/id/[token].js b/ui/src/pages/id/[token].js index fcf57eeb2..5b9da7445 100644 --- a/ui/src/pages/id/[token].js +++ b/ui/src/pages/id/[token].js @@ -5,7 +5,7 @@ import React from 'react'; import Layout from '../../components/layout'; import TokenData from '../../components/tokenData'; -export default function TokenPage({ params }) { +const TokenPage = function ({ params }) { return (

@@ -14,9 +14,11 @@ export default function TokenPage({ params }) { ); -} +}; TokenPage.propTypes = { params: PropTypes.shape({ token: PropTypes.string.isRequired, }), }; + +export default TokenPage; diff --git a/ui/src/pages/index.js b/ui/src/pages/index.js index 5d2bb2fa6..074bb3dd3 100644 --- a/ui/src/pages/index.js +++ b/ui/src/pages/index.js @@ -4,7 +4,7 @@ import React from 'react'; import Layout from '../components/layout'; import TokenList from '../components/tokenList'; -export default function Home() { +const Home = function () { return (

Tokens

@@ -14,4 +14,6 @@ export default function Home() {
); -} +}; + +export default Home; From cb296feb3badd022cad7d574345a851a4d9f3717 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 14:05:24 -0800 Subject: [PATCH 08/24] Drop support for mocking Redis We may re-add this later, but rather than trying to refactor code that currently isn't enabled or working, drop all of it for now. Currently the only supported testing method is with Redis running in another container. --- requirements/dev.in | 1 - requirements/dev.txt | 22 ----- src/gafaelfawr/dependencies/redis.py | 23 ++---- src/gafaelfawr/factory.py | 16 ++-- src/gafaelfawr/main.py | 2 +- tests/support/selenium.py | 30 +------ tests/support/setup.py | 13 +-- ui/package-lock.json | 116 ++++++++++++++------------- 8 files changed, 79 insertions(+), 144 deletions(-) diff --git a/requirements/dev.in b/requirements/dev.in index 9a95a07f6..98d393c20 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -13,7 +13,6 @@ diagrams documenteer holdup lsst-sphinx-bootstrap-theme<0.3 -mockaioredis mypy pre-commit pytest diff --git a/requirements/dev.txt b/requirements/dev.txt index 9e22bc5ad..10168ad2b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,12 +4,6 @@ # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/dev.txt requirements/dev.in # -aioredis==2.0.0 \ - --hash=sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db \ - --hash=sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb - # via - # -c requirements/main.txt - # mockaioredis alabaster==0.7.12 \ --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 @@ -30,12 +24,6 @@ async-generator==1.10 \ # via # trio # trio-websocket -async-timeout==4.0.1 \ - --hash=sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690 \ - --hash=sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51 - # via - # -c requirements/main.txt - # aioredis attrs==21.2.0 \ --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb @@ -515,14 +503,6 @@ markupsafe==2.0.1 \ # via # -c requirements/main.txt # jinja2 -mockaioredis==0.0.16 \ - --hash=sha256:46ec714d905eb62f35b12254bd8a892f5b5a11c6de77dd155d99d872e274a80b \ - --hash=sha256:d30d13e4f4312a2edae985dca2a8827f8c58ca0e64240f364530d182ede804fe - # via -r requirements/dev.in -mockredispy-kblin==2.9.3.4 \ - --hash=sha256:1cbc17ac65481a23390fb1ab12478a034db210b6b9759083d0cf84b9066d188d \ - --hash=sha256:34b32a8d5b81dfd2ade64167a38ff861fd182f4c1a5a65a255e00c83fd9f04e5 - # via mockaioredis mypy==0.910 \ --hash=sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9 \ --hash=sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a \ @@ -959,8 +939,6 @@ typing-extensions==4.0.0 \ --hash=sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9 # via # -c requirements/main.txt - # aioredis - # async-timeout # gitpython # mypy # sqlalchemy2-stubs diff --git a/src/gafaelfawr/dependencies/redis.py b/src/gafaelfawr/dependencies/redis.py index 8fd47d909..648a2e68a 100644 --- a/src/gafaelfawr/dependencies/redis.py +++ b/src/gafaelfawr/dependencies/redis.py @@ -20,46 +20,33 @@ class RedisDependency: Notes ----- Creation of the Redis pool has to be deferred until the configuration has - been loaded, which in turn is deferred for the first request. We also - want to provide an opportunity for the test suite to tell it to use a - mockaioredis pool instead. Do this by deferring creation of the pool - until the first time the dependency is called. + been loaded, which in turn is deferred for the first request. """ def __init__(self) -> None: self.redis: Optional[Redis] = None - self.is_mocked = False async def __call__( self, config: Config = Depends(config_dependency) ) -> Redis: """Creates the Redis pool if necessary and returns it.""" if not self.redis: - self.redis = Redis.from_url( - config.redis_url, password=config.redis_password - ) + password = config.redis_password + self.redis = Redis.from_url(config.redis_url, password=password) assert self.redis return self.redis - async def close(self) -> None: + async def aclose(self) -> None: """Close the open Redis pool. Should be called from a shutdown hook to ensure that the Redis clients are cleanly shut down and any pending writes are complete. """ - if self.redis and not self.is_mocked: + if self.redis: await self.redis.close() await self.redis.connection_pool.disconnect() self.redis = None - def set_redis(self, redis: Redis) -> None: - """Set the Redis object returned by ``__call__``. - - Used to inject a mock. - """ - self.redis = redis - self.is_mocked = True - redis_dependency = RedisDependency() """The dependency that will return the Redis pool.""" diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index 13500280e..274ac0c32 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -98,7 +98,7 @@ async def standalone(cls) -> AsyncIterator[ComponentFactory]: logger=logger, ) finally: - await redis_dependency.close() + await redis_dependency.aclose() await engine.dispose() def __init__( @@ -110,11 +110,11 @@ def __init__( http_client: AsyncClient, logger: BoundLogger, ) -> None: + self.session = session self._config = config self._redis = redis self._http_client = http_client self._logger = logger - self._session = session def create_admin_service(self) -> AdminService: """Create a new manager object for token administrators. @@ -124,8 +124,8 @@ def create_admin_service(self) -> AdminService: admin_service : `gafaelfawr.services.admin.AdminService` The new token administrator manager. """ - admin_store = AdminStore(self._session) - admin_history_store = AdminHistoryStore(self._session) + admin_store = AdminStore(self.session) + admin_history_store = AdminHistoryStore(self.session) return AdminService(admin_store, admin_history_store) def create_kubernetes_service(self) -> KubernetesService: @@ -135,7 +135,7 @@ def create_kubernetes_service(self) -> KubernetesService: return KubernetesService( token_service=token_service, storage=storage, - session=self._session, + session=self.session, logger=self._logger, ) @@ -226,15 +226,13 @@ def create_token_service(self) -> TokenService: token_service : `gafaelfawr.services.token.TokenService` The new token manager. """ - token_db_store = TokenDatabaseStore(self._session) + token_db_store = TokenDatabaseStore(self.session) key = self._config.session_secret storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) token_cache = TokenCache(token_redis_store) is_postgres = self._config.database_url.startswith("postgresql") - token_change_store = TokenChangeHistoryStore( - self._session, is_postgres - ) + token_change_store = TokenChangeHistoryStore(self.session, is_postgres) return TokenService( config=self._config, token_cache=token_cache, diff --git a/src/gafaelfawr/main.py b/src/gafaelfawr/main.py index a7da78e63..d141b4a8f 100644 --- a/src/gafaelfawr/main.py +++ b/src/gafaelfawr/main.py @@ -119,7 +119,7 @@ async def startup_event() -> None: async def shutdown_event() -> None: await http_client_dependency.aclose() await db_session_dependency.aclose() - await redis_dependency.close() + await redis_dependency.aclose() @app.exception_handler(PermissionDeniedError) diff --git a/tests/support/selenium.py b/tests/support/selenium.py index 58928fce0..9dcf76d2a 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -11,18 +11,13 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from typing import TYPE_CHECKING -from unittest.mock import MagicMock from urllib.parse import urlparse -import structlog from fastapi import FastAPI from seleniumwire import webdriver -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy.orm import sessionmaker from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency -from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory from gafaelfawr.main import app from gafaelfawr.models.token import Token, TokenUserInfo @@ -123,41 +118,22 @@ async def _selenium_startup(token_path: str) -> None: """Startup hook for the app run in Selenium testing mode.""" config = await config_dependency() is_postgres = config.database_url.startswith("postgresql") - logger = structlog.get_logger(config.safir.logger_name) user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) scopes = list(config.known_scopes.keys()) - # Mock out Redis if there is none running. - if not os.environ.get("REDIS_6379_TCP_PORT"): - import mockaioredis - - redis = await mockaioredis.create_redis_pool("") - redis_dependency.set_redis(redis) - - engine = create_async_engine(config.database_url, future=True) - session_factory = sessionmaker( - engine, expire_on_commit=False, class_=AsyncSession - ) - async with session_factory() as session: - async with session.begin(): + async with ComponentFactory.standalone() as factory: + async with factory.session.begin(): # Add an expired token so that we can test display of expired # tokens. await add_expired_session_token( user_info, scopes=scopes, ip_address="127.0.0.1", - session=session, + session=factory.session, is_postgres=is_postgres, ) # Add the valid session token. - factory = ComponentFactory( - config=config, - redis=await redis_dependency(config), - session=session, - http_client=MagicMock(), - logger=logger, - ) token_service = factory.create_token_service() token = await token_service.create_session_token( user_info, scopes=scopes, ip_address="127.0.0.1" diff --git a/tests/support/setup.py b/tests/support/setup.py index a6a266e57..f591d122b 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os from contextlib import asynccontextmanager from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -113,11 +112,6 @@ async def create( The mock for simulating `httpx.AsyncClient` calls. """ config = await initialize(tmp_path) - if not os.environ.get("REDIS_6379_TCP_PORT"): - import mockaioredis - - redis = await mockaioredis.create_redis_pool("") - redis_dependency.set_redis(redis) redis = await redis_dependency(config) # Create the database session that will be used by SetupTest and by @@ -148,12 +142,7 @@ async def create( ) finally: await http_client_dependency.aclose() - if os.environ.get("REDIS_6379_TCP_PORT"): - await redis_dependency.close() - else: - redis = await redis_dependency() - redis.close() - await redis.wait_closed() + await redis_dependency.aclose() await engine.dispose() def __init__( diff --git a/ui/package-lock.json b/ui/package-lock.json index 74853e0e4..e4ceb8531 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1838,7 +1838,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2230,7 +2229,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", @@ -3350,8 +3348,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "4.2.2", @@ -6786,7 +6783,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -6795,7 +6791,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6807,7 +6802,6 @@ "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -6821,8 +6815,7 @@ "node_modules/eslint/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/espree": { "version": "9.1.0", @@ -6842,7 +6835,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -9231,7 +9223,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -10694,7 +10685,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -19719,7 +19709,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", - "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -19977,7 +19966,8 @@ "ws": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "requires": {} } } }, @@ -20055,7 +20045,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", - "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", @@ -20819,12 +20808,14 @@ "acorn-import-assertions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==" + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "requires": {} }, "address": { "version": "1.1.2", @@ -20854,7 +20845,8 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -20934,8 +20926,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "4.2.2", @@ -22448,7 +22439,8 @@ "cssnano-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", - "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "requires": {} }, "csso": { "version": "4.2.0", @@ -22913,7 +22905,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} } } }, @@ -22937,7 +22930,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} } } }, @@ -23173,14 +23167,12 @@ "eslint-visitor-keys": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "dev": true + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==" }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -23189,7 +23181,6 @@ "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -23197,8 +23188,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -23229,7 +23219,8 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-config-react-app": { "version": "6.0.0", @@ -23243,7 +23234,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-config-wesbos/-/eslint-config-wesbos-2.1.0.tgz", "integrity": "sha512-qwuEFecMmfRoaV/7niB7StdaLwUQEWv9Oz67G5rQsiEm84ACC3mgMBe2NXeV5l2LHUuCiyHWKMWFpmWvPicnhQ==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -23427,7 +23419,8 @@ "eslint-plugin-react-hooks": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==" + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -23515,8 +23508,7 @@ "eslint-visitor-keys": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "dev": true + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==" } } }, @@ -25067,7 +25059,8 @@ "gatsby-plugin-use-query-params": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-use-query-params/-/gatsby-plugin-use-query-params-1.0.1.tgz", - "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==" + "integrity": "sha512-k3xaKuf8VhLq6/arocYRZqiQMTQ84ZRY0JklsO4tuKsRqi64b94zGf6B8SZn6yo0fvtJ/zw684DpH77y/iKdbA==", + "requires": {} }, "gatsby-plugin-utils": { "version": "2.2.0", @@ -25366,7 +25359,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -25543,12 +25535,14 @@ "graphql-type-json": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", - "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==" + "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", + "requires": {} }, "graphql-ws": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", - "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==" + "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", + "requires": {} }, "gzip-size": { "version": "5.1.1", @@ -25796,7 +25790,8 @@ "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -26326,7 +26321,8 @@ "isomorphic-ws": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==" + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "requires": {} }, "iterall": { "version": "1.3.0", @@ -26406,7 +26402,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -26985,7 +26980,8 @@ "meros": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", - "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==" + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", + "requires": {} }, "methods": { "version": "1.1.2", @@ -28373,27 +28369,32 @@ "postcss-discard-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", - "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", - "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", - "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", - "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "requires": {} }, "postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "requires": {} }, "postcss-loader": { "version": "5.3.0", @@ -28501,7 +28502,8 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -28532,7 +28534,8 @@ "postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", - "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -28926,7 +28929,8 @@ "react-alert-template-basic": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/react-alert-template-basic/-/react-alert-template-basic-1.0.2.tgz", - "integrity": "sha512-nJSIknM5xN/GzKdKkb5/X9zdiLxSqctww3mEouyeR891nUGP8guqFpCGD8vCjFYSZtq8z3tGcAKBG1XjtLHFHQ==" + "integrity": "sha512-nJSIknM5xN/GzKdKkb5/X9zdiLxSqctww3mEouyeR891nUGP8guqFpCGD8vCjFYSZtq8z3tGcAKBG1XjtLHFHQ==", + "requires": {} }, "react-aria-modal": { "version": "4.0.1", @@ -29150,7 +29154,8 @@ "react-displace": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", - "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==" + "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==", + "requires": {} }, "react-dom": { "version": "17.0.2", @@ -29175,7 +29180,8 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", + "requires": {} }, "react-is": { "version": "17.0.2", @@ -29216,7 +29222,8 @@ "react-table": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz", - "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==" + "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==", + "requires": {} }, "react-transition-group": { "version": "4.4.2", @@ -29979,7 +29986,8 @@ "serialize-query-params": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-1.3.5.tgz", - "integrity": "sha512-BrLH1RqgzVxm6dco+KP9S6BodeFiUVvKwtY3GSWQlupIdblT19KCGTRkHZ2yIU6Bjy0Prjts0tYe11VpTMbAeQ==" + "integrity": "sha512-BrLH1RqgzVxm6dco+KP9S6BodeFiUVvKwtY3GSWQlupIdblT19KCGTRkHZ2yIU6Bjy0Prjts0tYe11VpTMbAeQ==", + "requires": {} }, "serve-static": { "version": "1.14.1", From 84d0330f48af9d2f81964609040edacf0625a3ac Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 14:41:48 -0800 Subject: [PATCH 09/24] Drop support for non-PostgreSQL databases As with the support for mock Redis, the support for running with a SQLite database is not currently enabled and not tested. Dropping support allows some simplification, and we don't intend to deploy with SQLite. The only cost is that tests are now forced to take longer since testing with a real PostgreSQL database is quite a bit slower, but we were already paying that cost. --- .github/workflows/ci.yaml | 9 +-------- requirements/dev.in | 3 +-- src/gafaelfawr/config.py | 11 +---------- src/gafaelfawr/factory.py | 3 +-- src/gafaelfawr/storage/history.py | 25 +++++++------------------ tests/conftest.py | 5 ++--- tests/support/constants.py | 8 ++++++++ tests/support/selenium.py | 9 ++------- tests/support/settings.py | 28 ++-------------------------- tests/support/setup.py | 11 +++-------- tests/support/tokens.py | 5 +---- tox.ini | 28 +++++++++++----------------- 12 files changed, 40 insertions(+), 105 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 99f2ad97a..7fcad8b89 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -85,8 +85,6 @@ jobs: python: - "3.9" - "3.10" - database: - - PostgreSQL steps: - uses: actions/checkout@v2 @@ -117,13 +115,8 @@ jobs: restore-keys: | tox-${{ matrix.python }}-${{ hashFiles('requirements/*.txt') }}- - - name: Run tox (SQLite) + - name: Run tox run: tox -e py,coverage-report,typing - if: matrix.database == 'SQLite' - - - name: Run tox (PostgreSQL) - run: tox -e docker,coverage-report,typing - if: matrix.database == 'PostgreSQL' docs: runs-on: ubuntu-latest diff --git a/requirements/dev.in b/requirements/dev.in index 98d393c20..1672b599b 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -12,13 +12,12 @@ coverage[toml] diagrams documenteer holdup -lsst-sphinx-bootstrap-theme<0.3 +lsst-sphinx-bootstrap-theme mypy pre-commit pytest pytest-asyncio pytest-cov -pytest-xdist[psutil] respx selenium selenium-wire diff --git a/src/gafaelfawr/config.py b/src/gafaelfawr/config.py index b29d3668e..c8f3652ba 100644 --- a/src/gafaelfawr/config.py +++ b/src/gafaelfawr/config.py @@ -590,10 +590,6 @@ def from_file(cls, path: str) -> Config: # The database URL may have a separate secret in database_password, in # which case it needs to be added to the URL. It also needs to be # configured to use asyncpg. - # - # We have to avoid changing the URL if we're using SQLite, because - # urlparse cannot deal with the expected SQLite syntax of multiple - # consecutive / characters. parsed_url = urlparse(settings.database_url) if parsed_url.scheme == "postgresql": parsed_url = parsed_url._replace(scheme="postgresql+asyncpg") @@ -604,12 +600,7 @@ def from_file(cls, path: str) -> Config: f"@{parsed_url.hostname}" ) parsed_url = parsed_url._replace(netloc=database_netloc) - if parsed_url.scheme == "sqlite": - database_url = settings.database_url.replace( - "sqlite:", "sqlite+aiosqlite:" - ) - else: - database_url = parsed_url.geturl() + database_url = parsed_url.geturl() # If there is an OpenID Connect server configuration, load it from a # file in JSON format. (It contains secrets.) diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index 274ac0c32..facf4f22b 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -231,8 +231,7 @@ def create_token_service(self) -> TokenService: storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) token_cache = TokenCache(token_redis_store) - is_postgres = self._config.database_url.startswith("postgresql") - token_change_store = TokenChangeHistoryStore(self.session, is_postgres) + token_change_store = TokenChangeHistoryStore(self.session) return TokenService( config=self._config, token_cache=token_cache, diff --git a/src/gafaelfawr/storage/history.py b/src/gafaelfawr/storage/history.py index 2e220dfc4..d287e423c 100644 --- a/src/gafaelfawr/storage/history.py +++ b/src/gafaelfawr/storage/history.py @@ -2,7 +2,6 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from sqlalchemy import and_, func, or_ @@ -56,13 +55,10 @@ class TokenChangeHistoryStore: ---------- session : `sqlalchemy.ext.asyncio.AsyncSession` The database session proxy. - is_postgres : `bool` - Whether the backend database is PostgreSQL. """ - def __init__(self, session: AsyncSession, is_postgres: bool) -> None: + def __init__(self, session: AsyncSession) -> None: self._session = session - self._is_postgres = is_postgres async def add(self, entry: TokenChangeHistoryEntry) -> None: """Record a change to a token.""" @@ -284,20 +280,13 @@ def _apply_ip_or_cidr_filter( ) -> Select: """Apply an appropriate filter for an IP or CIDR block. - If the underlying database is not PostgreSQL, which supports native - CIDR membership queries, cheat and turn the CIDR block into a string - wildcard. This will only work for CIDR blocks on class boundaries, - but the intended supported database is PostgreSQL anyway. + Notes + ----- + If there is ever a need to support a database that does not have + native CIDR membership queries, fallback code (probably using a LIKE + expression) will need to be added here. """ if "/" in ip_or_cidr: - cidr = ip_or_cidr - if self._is_postgres: - return stmt.where(text(":c >> ip_address")).params(c=cidr) - else: - if ":" in str(ip_or_cidr): - net = re.sub("::/[0-9]+$", ":%", cidr) - else: - net = re.sub(r"(\.0)+/[0-9]+$", ".%", cidr) - return stmt.where(TokenChangeHistory.ip_address.like(net)) + return stmt.where(text(":c >> ip_address")).params(c=ip_or_cidr) else: return stmt.where(TokenChangeHistory.ip_address == str(ip_or_cidr)) diff --git a/tests/conftest.py b/tests/conftest.py index c2bea2ea3..43bc926e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from unittest.mock import patch -from urllib.parse import urljoin, urlparse +from urllib.parse import urljoin import kubernetes import pytest @@ -54,8 +54,7 @@ async def app(tmp_path: Path) -> AsyncIterator[FastAPI]: settings_path = build_settings(tmp_path, "github") config_dependency.set_settings_path(str(settings_path)) config = await config_dependency() - should_reset = not urlparse(config.database_url).scheme == "sqlite" - await initialize_database(config, reset=should_reset) + await initialize_database(config, reset=True) async with LifespanManager(main.app): yield main.app diff --git a/tests/support/constants.py b/tests/support/constants.py index 02d669b08..b1c1cb74c 100644 --- a/tests/support/constants.py +++ b/tests/support/constants.py @@ -1,4 +1,12 @@ """Constants used in test fixtures and setup.""" +TEST_DATABASE_URL = ( + "postgresql://gafaelfawr:INSECURE-PASSWORD@127.0.0.1/gafaelfawr" +) +"""The URL used for the test database. + +This must match the ``tox.ini`` configuration for the PostgreSQL container. +""" + TEST_HOSTNAME = "example.com" """The hostname used in ASGI requests to the application.""" diff --git a/tests/support/selenium.py b/tests/support/selenium.py index 9dcf76d2a..d9672afc0 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -11,7 +11,6 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from typing import TYPE_CHECKING -from urllib.parse import urlparse from fastapi import FastAPI from seleniumwire import webdriver @@ -117,7 +116,6 @@ def _wait_for_server(port: int, timeout: float = 5.0) -> None: async def _selenium_startup(token_path: str) -> None: """Startup hook for the app run in Selenium testing mode.""" config = await config_dependency() - is_postgres = config.database_url.startswith("postgresql") user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) scopes = list(config.known_scopes.keys()) @@ -130,7 +128,6 @@ async def _selenium_startup(token_path: str) -> None: scopes=scopes, ip_address="127.0.0.1", session=factory.session, - is_postgres=is_postgres, ) # Add the valid session token. @@ -183,10 +180,8 @@ async def run_app( config = await config_dependency() token_path = tmp_path / "token" - # Initialize the database. Non-SQLite databases need to be reset between - # tests. - should_reset = not urlparse(config.database_url).scheme == "sqlite" - await initialize_database(config, reset=should_reset) + # Initialize and clear the database. + await initialize_database(config, reset=True) # Create the socket that the app will listen on. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/tests/support/settings.py b/tests/support/settings.py index 3e64e2727..af51a3b23 100644 --- a/tests/support/settings.py +++ b/tests/support/settings.py @@ -3,13 +3,13 @@ from __future__ import annotations import json -import os from pathlib import Path from typing import TYPE_CHECKING from cryptography.fernet import Fernet from gafaelfawr.keypair import RSAKeyPair +from tests.support.constants import TEST_DATABASE_URL if TYPE_CHECKING: from typing import List, Optional, Union @@ -19,30 +19,6 @@ __all__ = ["build_settings", "build_settings_file", "store_secret"] -def _test_database_url(tmp_path: Path) -> str: - """Determine the database URL to use for testing. - - Default to a SQLite database stored in the temporary test directory, but - switch to PostgreSQL if the environment variable set by tox-docker is - present. Hardcodes the PostgreSQL password also set in - ``pyproject.toml``. - - Parameters - ---------- - tmp_path : `pathlib.Path` - The root of the temporary area. - - Returns - ------- - database_url : `str` - The database URL suitable for substituting into a settings file. - """ - if os.environ.get("POSTGRES_5432_TCP_PORT"): - return "postgresql://gafaelfawr:INSECURE-PASSWORD@127.0.0.1/gafaelfawr" - else: - return "sqlite:///" + str(tmp_path / "gafaelfawr.sqlite") - - def build_settings( tmp_path: Path, template: str, @@ -78,7 +54,7 @@ def build_settings( if settings and "database_url" in settings: database_url = settings["database_url"] else: - database_url = _test_database_url(tmp_path) + database_url = TEST_DATABASE_URL settings_path = build_settings_file( tmp_path, diff --git a/tests/support/setup.py b/tests/support/setup.py index f591d122b..7a6972d89 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -4,7 +4,6 @@ from contextlib import asynccontextmanager from typing import TYPE_CHECKING -from urllib.parse import urlparse import respx import structlog @@ -67,10 +66,8 @@ async def initialize(tmp_path: Path) -> Config: config = config_dependency._config assert config - # Initialize the database. Non-SQLite databases need to be reset between - # tests. - should_reset = not urlparse(config.database_url).scheme == "sqlite" - await initialize_database(config, reset=should_reset) + # Initialize and clear the database. + await initialize_database(config, reset=True) return config @@ -100,9 +97,7 @@ async def create( This is the only supported way to set up the test environment and should be called instead of calling the constructor directly. It initializes and starts the application and configures an - `httpx.AsyncClient` to talk to it. Whether to use a real PostgreSQL - and Redis server or to use SQLite and mock Redis is determined by the - environment variables set by ``tox``. + `httpx.AsyncClient` to talk to it. Parameters ---------- diff --git a/tests/support/tokens.py b/tests/support/tokens.py index 9777954eb..c49b9e9e3 100644 --- a/tests/support/tokens.py +++ b/tests/support/tokens.py @@ -31,7 +31,6 @@ async def add_expired_session_token( scopes: List[str], ip_address: str, session: AsyncSession, - is_postgres: bool, ) -> None: """Add an expired session token to the database. @@ -53,11 +52,9 @@ async def add_expired_session_token( The IP address from which the request came. session : `sqlalchemy.ext.asyncio.AsyncSession` The database session. - is_postgres : `bool` - Whether the underlying database is PostgreSQL. """ token_db_store = TokenDatabaseStore(session) - token_change_store = TokenChangeHistoryStore(session, is_postgres) + token_change_store = TokenChangeHistoryStore(session) token = Token() created = current_datetime() diff --git a/tox.ini b/tox.ini index 569c138f9..4197cfbf1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = docker,typing,lint,docs +envlist = py,typing,lint,docs,coverage-report isolated_build = True [docker:postgres] @@ -34,12 +34,9 @@ healthcheck_interval = 1 healthcheck_start_period = 1 [testenv] -description = Run pytest with SQLite and mock Redis. deps = -r{toxinidir}/requirements/main.txt -r{toxinidir}/requirements/dev.txt -commands = - pytest -vv --cov=gafaelfawr --cov-branch --cov-report= -n auto {posargs} setenv = GAFAELFAWR_UI_PATH = {toxinidir}/ui/public @@ -51,19 +48,6 @@ depends = py commands = coverage report -[testenv:docker] -description = Run pytest with PostgreSQL and Redis via Docker. -docker = - postgres - redis -deps = - -r{toxinidir}/requirements/main.txt - -r{toxinidir}/requirements/dev.txt -commands = - pytest -vv --cov=gafaelfawr --cov-branch --cov-report= {posargs} -setenv = - GAFAELFAWR_UI_PATH = {toxinidir}/ui/public - [testenv:docs] description = Build documentation (HTML) with Sphinx. whitelist_externals = @@ -86,6 +70,16 @@ deps = pre-commit commands = pre-commit run --all-files +[testenv:py] +description = Run pytest with PostgreSQL and Redis via Docker. +docker = + postgres + redis +commands = + pytest -vv --cov=gafaelfawr --cov-branch --cov-report= {posargs} +setenv = + GAFAELFAWR_UI_PATH = {toxinidir}/ui/public + [testenv:run] description = Run the development server with auto-reload for code changes. usedevelop = true From 5e3cfa68f2f0cba0a248db14621a34145bc17922 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 14:58:12 -0800 Subject: [PATCH 10/24] Share a database initialization fixture Refactor the database initialization (and simple configuration) code into a fixture that can be shared by the other fixtures and by the CLI tests. --- tests/cli_test.py | 9 +++----- tests/conftest.py | 44 ++++++++++++++++++++++++--------------- tests/support/selenium.py | 4 ---- tests/support/setup.py | 35 +------------------------------ 4 files changed, 31 insertions(+), 61 deletions(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index 85404132e..e81ef66b6 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -8,7 +8,6 @@ from __future__ import annotations -import asyncio from typing import TYPE_CHECKING from click.testing import CliRunner @@ -18,7 +17,6 @@ from gafaelfawr.models.token import Token from tests.support.kubernetes import MockKubernetesApi from tests.support.logging import parse_log -from tests.support.setup import initialize if TYPE_CHECKING: from pathlib import Path @@ -65,9 +63,8 @@ def test_help() -> None: def test_update_service_tokens( - tmp_path: Path, mock_kubernetes: MockKubernetesApi + tmp_path: Path, empty_database: None, mock_kubernetes: MockKubernetesApi ) -> None: - asyncio.run(initialize(tmp_path)) mock_kubernetes.create_namespaced_custom_object( "gafaelfawr.lsst.io", "v1alpha1", @@ -97,17 +94,17 @@ def test_update_service_tokens( def test_update_service_tokens_error( tmp_path: Path, + empty_database: None, mock_kubernetes: MockKubernetesApi, caplog: LogCaptureFixture, ) -> None: - asyncio.run(initialize(tmp_path)) + caplog.clear() def error_callback(method: str, *args: Any) -> None: if method == "list_cluster_custom_object": raise ApiException(status=500, reason="Some error") mock_kubernetes.error_callback = error_callback - caplog.clear() runner = CliRunner() result = runner.invoke(main, ["update-service-tokens"]) diff --git a/tests/conftest.py b/tests/conftest.py index 43bc926e1..30e5630f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,25 +36,12 @@ @pytest.fixture -async def app(tmp_path: Path) -> AsyncIterator[FastAPI]: +async def app(empty_database: None) -> AsyncIterator[FastAPI]: """Return a configured test application. Wraps the application in a lifespan manager so that startup and shutdown - events are sent during test execution. Initializes the database before - creating the app to ensure that data is dropped from a persistent database - between test cases. - - Notes - ----- - This always uses a settings file configured for GitHub authentication for - the database initialization and initial app configuration, since it - shouldn't matter. Use ``setup.configure()`` after the test has started to - change this if needed for a given test. + events are sent during test execution. """ - settings_path = build_settings(tmp_path, "github") - config_dependency.set_settings_path(str(settings_path)) - config = await config_dependency() - await initialize_database(config, reset=True) async with LifespanManager(main.app): yield main.app @@ -83,6 +70,29 @@ def driver() -> Iterator[webdriver.Chrome]: driver.quit() +@pytest.fixture +async def empty_database(tmp_path: Path) -> None: + """Initialize the database for a new test. + + This exists as a fixture so that multiple other fixtures can depend on it + and avoid any duplication of work if, say, we need both a configured + FastAPI app and a standalone factory. + + Notes + ----- + This always uses a settings file configured for GitHub authentication for + the database initialization and initial app configuration. Use + ``setup.configure()`` after the test has started to change this if needed + for a given test, or avoid this fixture and any that depend on it if + control over the configuration prior to database initialization is + required. + """ + settings_path = build_settings(tmp_path, "github") + config_dependency.set_settings_path(str(settings_path)) + config = await config_dependency() + await initialize_database(config, reset=True) + + @pytest.fixture def mock_kubernetes() -> Iterator[MockKubernetesApi]: """Replace the Kubernetes API with a mock class. @@ -107,7 +117,7 @@ def mock_kubernetes() -> Iterator[MockKubernetesApi]: @pytest.fixture async def selenium_config( - tmp_path: Path, driver: webdriver.Chrome + tmp_path: Path, driver: webdriver.Chrome, empty_database: None ) -> AsyncIterator[SeleniumConfig]: """Start a server for Selenium tests. @@ -142,7 +152,7 @@ async def selenium_config( @pytest.fixture async def setup( - tmp_path: Path, respx_mock: respx.Router + tmp_path: Path, empty_database: None, respx_mock: respx.Router ) -> AsyncIterator[SetupTest]: """Create a test setup object. diff --git a/tests/support/selenium.py b/tests/support/selenium.py index d9672afc0..6812bf1cd 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -15,7 +15,6 @@ from fastapi import FastAPI from seleniumwire import webdriver -from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.factory import ComponentFactory from gafaelfawr.main import app @@ -180,9 +179,6 @@ async def run_app( config = await config_dependency() token_path = tmp_path / "token" - # Initialize and clear the database. - await initialize_database(config, reset=True) - # Create the socket that the app will listen on. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("127.0.0.1", 0)) diff --git a/tests/support/setup.py b/tests/support/setup.py index 7a6972d89..37e08f2a9 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -13,7 +13,6 @@ from sqlalchemy.orm import sessionmaker from gafaelfawr.constants import COOKIE_NAME -from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.factory import ComponentFactory @@ -40,38 +39,6 @@ from gafaelfawr.providers.github import GitHubUserInfo -async def initialize(tmp_path: Path) -> Config: - """Do basic initialization and return a configuration. - - This shared logic can be used either with `SetupTest`, which assumes an - ASGI application and an async test, or with non-async tests such as the - tests of the command-line interface. - - Parameters - ---------- - tmp_path : `pathlib.Path` - The path for temporary files. - - Returns - ------- - config : `gafaelfawr.config.Config` - The generated config, using the same defaults as `SetupTest`. - """ - settings_path = build_settings(tmp_path, "github") - config_dependency.set_settings_path(str(settings_path)) - - # Avoid making an async call to config_dependency since this is the - # non-async initialization function, used for CLI testing. We're - # guaranteed that set_settings_path will populate this attribute. - config = config_dependency._config - assert config - - # Initialize and clear the database. - await initialize_database(config, reset=True) - - return config - - class SetupTest: """Utility class for test setup. @@ -106,7 +73,7 @@ async def create( respx_mock : `respx.Router` The mock for simulating `httpx.AsyncClient` calls. """ - config = await initialize(tmp_path) + config = await config_dependency() redis = await redis_dependency(config) # Create the database session that will be used by SetupTest and by From a3cf828d991a830cd4a7a03719ba84f9af5c8892 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 17:06:50 -0800 Subject: [PATCH 11/24] Remove config from SetupTest Continue the refactoring by separating the configuration from SetupTest and making configure its own function provided by tests.support.settings. This requires making some other helper methods async. --- tests/conftest.py | 22 ++-- tests/handlers/api_admins_test.py | 5 +- tests/handlers/api_login_test.py | 35 +++-- tests/handlers/api_tokens_test.py | 27 ++-- tests/handlers/auth_test.py | 79 ++++++------ tests/handlers/index_test.py | 6 +- tests/handlers/influxdb_test.py | 38 +++--- tests/handlers/login_github_test.py | 11 +- tests/handlers/login_oidc_test.py | 191 +++++++++++++++------------- tests/handlers/logout_test.py | 22 ++-- tests/handlers/oidc_test.py | 79 +++++++----- tests/issuer_test.py | 18 ++- tests/services/oidc_test.py | 29 +++-- tests/services/token_test.py | 33 ++--- tests/support/github.py | 15 ++- tests/support/headers.py | 32 +++++ tests/support/oidc.py | 15 +-- tests/support/settings.py | 118 +++++++++++------ tests/support/setup.py | 62 ++------- tests/token_cache_test.py | 9 +- tests/verify_test.py | 64 +++++----- 21 files changed, 509 insertions(+), 401 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 30e5630f8..369a6713f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ from fastapi import FastAPI from seleniumwire import webdriver + from gafaelfawr.config import Config from tests.support.selenium import SeleniumConfig @@ -54,6 +55,14 @@ async def client(app: FastAPI) -> AsyncIterator[AsyncClient]: yield client +@pytest.fixture +async def config(tmp_path: Path) -> Config: + """Set up and return the default test configuration.""" + settings_path = build_settings(tmp_path, "github") + config_dependency.set_settings_path(str(settings_path)) + return await config_dependency() + + @pytest.fixture(scope="session") def driver() -> Iterator[webdriver.Chrome]: """Create a driver for Selenium testing. @@ -71,7 +80,7 @@ def driver() -> Iterator[webdriver.Chrome]: @pytest.fixture -async def empty_database(tmp_path: Path) -> None: +async def empty_database(config: Config) -> None: """Initialize the database for a new test. This exists as a fixture so that multiple other fixtures can depend on it @@ -82,14 +91,11 @@ async def empty_database(tmp_path: Path) -> None: ----- This always uses a settings file configured for GitHub authentication for the database initialization and initial app configuration. Use - ``setup.configure()`` after the test has started to change this if needed - for a given test, or avoid this fixture and any that depend on it if - control over the configuration prior to database initialization is - required. + `tests.support.settings.configure` after the test has started to change + this if needed for a given test, or avoid this fixture and any that depend + on it if control over the configuration prior to database initialization + is required. """ - settings_path = build_settings(tmp_path, "github") - config_dependency.set_settings_path(str(settings_path)) - config = await config_dependency() await initialize_database(config, reset=True) diff --git a/tests/handlers/api_admins_test.py b/tests/handlers/api_admins_test.py index b1e924dfc..63843df0e 100644 --- a/tests/handlers/api_admins_test.py +++ b/tests/handlers/api_admins_test.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @@ -120,8 +121,8 @@ async def test_add_delete(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bootstrap(client: AsyncClient, setup: SetupTest) -> None: - token = str(setup.config.bootstrap_token) +async def test_bootstrap(client: AsyncClient, config: Config) -> None: + token = str(config.bootstrap_token) r = await client.post( "/auth/api/v1/admins", diff --git a/tests/handlers/api_login_test.py b/tests/handlers/api_login_test.py index 880d076f6..f778c7698 100644 --- a/tests/handlers/api_login_test.py +++ b/tests/handlers/api_login_test.py @@ -9,30 +9,23 @@ import pytest from cryptography.fernet import Fernet -from gafaelfawr.auth import AuthErrorChallenge, AuthType from gafaelfawr.constants import COOKIE_NAME from gafaelfawr.models.state import State from gafaelfawr.models.token import Token from tests.support.constants import TEST_HOSTNAME -from tests.support.headers import parse_www_authenticate +from tests.support.headers import assert_unauthorized_is_correct if TYPE_CHECKING: - from httpx import AsyncClient, Response + from httpx import AsyncClient from gafaelfawr.config import Config from tests.support.setup import SetupTest -def assert_unauthorized_is_correct(r: Response, config: Config) -> None: - assert r.status_code == 401 - challenge = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(challenge, AuthErrorChallenge) - assert challenge.auth_type == AuthType.Bearer - assert challenge.realm == config.realm - - @pytest.mark.asyncio -async def test_login(client: AsyncClient, setup: SetupTest) -> None: +async def test_login( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: token_data = await setup.create_session_token( username="example", scopes=["read:all", "exec:admin"] ) @@ -45,7 +38,7 @@ async def test_login(client: AsyncClient, setup: SetupTest) -> None: data = r.json() expected_scopes = [ {"name": n, "description": d} - for n, d in sorted(setup.config.known_scopes.items()) + for n, d in sorted(config.known_scopes.items()) ] assert data == { "csrf": ANY, @@ -59,9 +52,11 @@ async def test_login(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_login_no_auth( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: r = await client.get("/auth/api/v1/login") - assert_unauthorized_is_correct(r, setup.config) + assert_unauthorized_is_correct(r, config) # An Authorization header with a valid token still redirects. token_data = await setup.create_session_token() @@ -69,7 +64,7 @@ async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: "/auth/api/v1/login", headers={"Authorization": f"bearer {token_data.token}"}, ) - assert_unauthorized_is_correct(r, setup.config) + assert_unauthorized_is_correct(r, config) # A token with no underlying Redis representation is ignored. state = State(token=Token()) @@ -77,11 +72,11 @@ async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: "/auth/api/v1/login", cookies={COOKIE_NAME: await state.as_cookie()}, ) - assert_unauthorized_is_correct(r, setup.config) + assert_unauthorized_is_correct(r, config) # Likewise with a cookie containing a malformed token. This requires a # bit more work to assemble. - key = setup.config.session_secret.encode() + key = config.session_secret.encode() fernet = Fernet(key) data = {"token": "bad-token"} bad_cookie = fernet.encrypt(json.dumps(data).encode()).decode() @@ -89,7 +84,7 @@ async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: "/auth/api/v1/login", cookies={COOKIE_NAME: bad_cookie}, ) - assert_unauthorized_is_correct(r, setup.config) + assert_unauthorized_is_correct(r, config) # And finally check with a mangled state that won't decrypt. bad_cookie = "XXX" + await state.as_cookie() @@ -97,4 +92,4 @@ async def test_login_no_auth(client: AsyncClient, setup: SetupTest) -> None: "/auth/api/v1/login", cookies={COOKIE_NAME: bad_cookie}, ) - assert_unauthorized_is_correct(r, setup.config) + assert_unauthorized_is_correct(r, config) diff --git a/tests/handlers/api_tokens_test.py b/tests/handlers/api_tokens_test.py index 93669502e..73ef0c5dd 100644 --- a/tests/handlers/api_tokens_test.py +++ b/tests/handlers/api_tokens_test.py @@ -20,6 +20,7 @@ from _pytest.logging import LogCaptureFixture from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @@ -213,7 +214,9 @@ async def test_create_delete_modify( @pytest.mark.asyncio -async def test_token_info(client: AsyncClient, setup: SetupTest) -> None: +async def test_token_info( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: user_info = TokenUserInfo( username="example", name="Example Person", @@ -244,7 +247,7 @@ async def test_token_info(client: AsyncClient, setup: SetupTest) -> None: now = datetime.now(tz=timezone.utc) created = datetime.fromtimestamp(data["created"], tz=timezone.utc) assert now - timedelta(seconds=2) <= created <= now - expires = created + timedelta(minutes=setup.config.issuer.exp_minutes) + expires = created + timedelta(minutes=config.issuer.exp_minutes) assert datetime.fromtimestamp(data["expires"], tz=timezone.utc) == expires r = await client.get( @@ -424,10 +427,12 @@ async def test_csrf_required(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_bootstrap(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_bootstrap( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: token_data = await setup.create_session_token() token = token_data.token - bootstrap_token = str(setup.config.bootstrap_token) + bootstrap_token = str(config.bootstrap_token) r = await client.get( "/auth/api/v1/users/example/tokens", @@ -750,9 +755,11 @@ async def test_bad_expires(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_bad_scopes(client: AsyncClient, setup: SetupTest) -> None: +async def test_bad_scopes( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: """Test creating or modifying a token with bogus scopes.""" - known_scopes = list(setup.config.known_scopes.keys()) + known_scopes = list(config.known_scopes.keys()) assert len(known_scopes) > 4 token_data = await setup.create_session_token( scopes=known_scopes[1:3] + ["other:scope", "user:token"] @@ -795,7 +802,9 @@ async def test_bad_scopes(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_create_admin(client: AsyncClient, setup: SetupTest) -> None: +async def test_create_admin( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: """Test creating a token through the admin interface.""" token_data = await setup.create_session_token(scopes=["exec:admin"]) csrf = await setup.login(client, token_data.token) @@ -961,9 +970,7 @@ async def test_create_admin(client: AsyncClient, setup: SetupTest) -> None: # Check that the bootstrap token also works. r = await client.post( "/auth/api/v1/tokens", - headers={ - "Authorization": f"bearer {str(setup.config.bootstrap_token)}" - }, + headers={"Authorization": f"bearer {str(config.bootstrap_token)}"}, json={"username": "other-service", "token_type": "service"}, ) assert r.status_code == 201 diff --git a/tests/handlers/auth_test.py b/tests/handlers/auth_test.py index 403d8c1ea..eca68c03b 100644 --- a/tests/handlers/auth_test.py +++ b/tests/handlers/auth_test.py @@ -10,33 +10,29 @@ from gafaelfawr.auth import AuthError, AuthErrorChallenge, AuthType from gafaelfawr.models.token import Token, TokenUserInfo -from tests.support.headers import parse_www_authenticate +from tests.support.headers import ( + assert_unauthorized_is_correct, + parse_www_authenticate, +) if TYPE_CHECKING: from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_auth( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: r = await client.get("/auth", params={"scope": "exec:admin"}) - assert r.status_code == 401 - assert r.headers["Cache-Control"] == "no-cache, must-revalidate" - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config) r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "bearer"} ) - assert r.status_code == 401 - assert r.headers["Cache-Control"] == "no-cache, must-revalidate" - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config) r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "bogus"} @@ -46,12 +42,7 @@ async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: r = await client.get( "/auth", params={"scope": "exec:admin", "auth_type": "basic"} ) - assert r.status_code == 401 - assert r.headers["Cache-Control"] == "no-cache, must-revalidate" - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Basic - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config, AuthType.Basic) @pytest.mark.asyncio @@ -81,7 +72,9 @@ async def test_invalid(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_invalid_auth( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: r = await client.get( "/auth", params={"scope": "exec:admin"}, @@ -91,7 +84,7 @@ async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_request r = await client.get( @@ -103,7 +96,7 @@ async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_request r = await client.get( @@ -116,7 +109,7 @@ async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_token # Create a nonexistent token. @@ -131,12 +124,14 @@ async def test_invalid_auth(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_token @pytest.mark.asyncio -async def test_access_denied(client: AsyncClient, setup: SetupTest) -> None: +async def test_access_denied( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: token_data = await setup.create_session_token() r = await client.get( @@ -149,14 +144,16 @@ async def test_access_denied(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.insufficient_scope assert authenticate.scope == "exec:admin" assert "Token missing required scope" in r.text @pytest.mark.asyncio -async def test_auth_forbidden(client: AsyncClient, setup: SetupTest) -> None: +async def test_auth_forbidden( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: r = await client.get( "/auth/forbidden", params=[("scope", "exec:test"), ("scope", "exec:admin")], @@ -166,7 +163,7 @@ async def test_auth_forbidden(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.insufficient_scope assert authenticate.scope == "exec:admin exec:test" assert "Token missing required scope" in r.text @@ -179,12 +176,14 @@ async def test_auth_forbidden(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert not isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Basic - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert "Token missing required scope" in r.text @pytest.mark.asyncio -async def test_satisfy_all(client: AsyncClient, setup: SetupTest) -> None: +async def test_satisfy_all( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: token_data = await setup.create_session_token(scopes=["exec:test"]) r = await client.get( @@ -197,7 +196,7 @@ async def test_satisfy_all(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.insufficient_scope assert authenticate.scope == "exec:admin exec:test" assert "Token missing required scope" in r.text @@ -446,7 +445,9 @@ async def test_basic(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_basic_failure(client: AsyncClient, setup: SetupTest) -> None: +async def test_basic_failure( + client: AsyncClient, config: Config, setup: SetupTest +) -> None: basic_b64 = base64.b64encode(b"bogus-string").decode() r = await client.get( "/auth", @@ -457,7 +458,7 @@ async def test_basic_failure(client: AsyncClient, setup: SetupTest) -> None: authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_request for basic in (b"foo:foo", b"x-oauth-basic:foo", b"foo:x-oauth-basic"): @@ -467,16 +468,12 @@ async def test_basic_failure(client: AsyncClient, setup: SetupTest) -> None: params={"scope": "exec:admin", "auth_type": "basic"}, headers={"Authorization": f"Basic {basic_b64}"}, ) - assert r.status_code == 401 - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Basic - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config, AuthType.Basic) @pytest.mark.asyncio async def test_ajax_unauthorized( - client: AsyncClient, setup: SetupTest + client: AsyncClient, config: Config, setup: SetupTest ) -> None: """Test that AJAX requests without auth get 403, not 401.""" r = await client.get( @@ -488,7 +485,7 @@ async def test_ajax_unauthorized( authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert not isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm @pytest.mark.asyncio diff --git a/tests/handlers/index_test.py b/tests/handlers/index_test.py index 2356dc297..d517dc758 100644 --- a/tests/handlers/index_test.py +++ b/tests/handlers/index_test.py @@ -9,15 +9,15 @@ if TYPE_CHECKING: from httpx import AsyncClient - from tests.support.setup import SetupTest + from gafaelfawr.config import Config @pytest.mark.asyncio -async def test_get_index(client: AsyncClient, setup: SetupTest) -> None: +async def test_get_index(client: AsyncClient, config: Config) -> None: r = await client.get("/") assert r.status_code == 200 data = r.json() - assert data["name"] == setup.config.safir.name + assert data["name"] == config.safir.name assert isinstance(data["version"], str) assert isinstance(data["description"], str) assert isinstance(data["repository_url"], str) diff --git a/tests/handlers/influxdb_test.py b/tests/handlers/influxdb_test.py index 5855ad4c8..8440eaf41 100644 --- a/tests/handlers/influxdb_test.py +++ b/tests/handlers/influxdb_test.py @@ -8,24 +8,30 @@ import jwt import pytest -from gafaelfawr.auth import AuthErrorChallenge, AuthType -from tests.support.headers import parse_www_authenticate +from tests.support.headers import assert_unauthorized_is_correct from tests.support.logging import parse_log +from tests.support.settings import configure if TYPE_CHECKING: + from pathlib import Path + from _pytest.logging import LogCaptureFixture from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @pytest.mark.asyncio async def test_influxdb( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, + config: Config, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: token_data = await setup.create_session_token() assert token_data.expires - influxdb_secret = setup.config.issuer.influxdb_secret + influxdb_secret = config.issuer.influxdb_secret assert influxdb_secret caplog.clear() @@ -65,20 +71,19 @@ async def test_influxdb( @pytest.mark.asyncio -async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_auth(client: AsyncClient, config: Config) -> None: r = await client.get("/auth/tokens/influxdb/new") - assert r.status_code == 401 - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config) @pytest.mark.asyncio async def test_not_configured( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: - await setup.configure("oidc") + await configure(tmp_path, "oidc") token_data = await setup.create_session_token() caplog.clear() @@ -108,12 +113,15 @@ async def test_not_configured( @pytest.mark.asyncio async def test_influxdb_force_username( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: - await setup.configure("influxdb-username") + config = await configure(tmp_path, "influxdb-username") token_data = await setup.create_session_token() assert token_data.expires - influxdb_secret = setup.config.issuer.influxdb_secret + influxdb_secret = config.issuer.influxdb_secret assert influxdb_secret caplog.clear() diff --git a/tests/handlers/login_github_test.py b/tests/handlers/login_github_test.py index da8b1ab70..1dc335b93 100644 --- a/tests/handlers/login_github_test.py +++ b/tests/handlers/login_github_test.py @@ -8,6 +8,7 @@ import pytest +from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.providers.github import ( GitHubProvider, GitHubTeam, @@ -62,10 +63,11 @@ async def simulate_github_login( response : ``httpx.Response`` The response from the return to the ``/login`` handler. """ - assert setup.config.github + config = await config_dependency() + assert config.github if not headers: headers = {} - setup.set_github_response( + await setup.set_github_response( "some-code", user_info, paginate_teams=paginate_teams, @@ -81,7 +83,7 @@ async def simulate_github_login( assert url.query query = parse_qs(url.query) assert query == { - "client_id": [setup.config.github.client_id], + "client_id": [config.github.client_id], "scope": [" ".join(GitHubProvider._SCOPES)], "state": [ANY], } @@ -195,7 +197,7 @@ async def test_login_redirect_header( teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) return_url = "https://example.com/foo?a=bar&b=baz" - setup.set_github_response("some-code", user_info) + await setup.set_github_response("some-code", user_info) # Simulate the initial authentication request. r = await client.get( @@ -424,7 +426,6 @@ async def test_paginated_teams(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: - assert setup.config.github user_info = GitHubUserInfo( name="GitHub User", username="githubuser", diff --git a/tests/handlers/login_oidc_test.py b/tests/handlers/login_oidc_test.py index 29b14bf47..616d8bcff 100644 --- a/tests/handlers/login_oidc_test.py +++ b/tests/handlers/login_oidc_test.py @@ -10,8 +10,11 @@ from httpx import ConnectError from tests.support.logging import parse_log +from tests.support.settings import configure if TYPE_CHECKING: + from pathlib import Path + from _pytest.logging import LogCaptureFixture from httpx import AsyncClient @@ -20,36 +23,39 @@ @pytest.mark.asyncio async def test_login( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token( + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token( groups=["admin"], name="Some Person", email="person@example.com" ) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) - assert setup.config.oidc + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) + assert config.oidc return_url = "https://example.com:4444/foo?a=bar&b=baz" caplog.clear() r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 - assert r.headers["Location"].startswith(setup.config.oidc.login_url) + assert r.headers["Location"].startswith(config.oidc.login_url) url = urlparse(r.headers["Location"]) assert url.query query = parse_qs(url.query) - login_params = {p: [v] for p, v in setup.config.oidc.login_params.items()} + login_params = {p: [v] for p, v in config.oidc.login_params.items()} assert query == { - "client_id": [setup.config.oidc.client_id], - "redirect_uri": [setup.config.oidc.redirect_url], + "client_id": [config.oidc.client_id], + "redirect_uri": [config.oidc.redirect_url], "response_type": ["code"], - "scope": ["openid " + " ".join(setup.config.oidc.scopes)], + "scope": ["openid " + " ".join(config.oidc.scopes)], "state": [ANY], **login_params, } # Verify the logging. - login_url = setup.config.oidc.login_url + login_url = config.oidc.login_url assert parse_log(caplog) == [ { "event": f"Redirecting user to {login_url} for authentication", @@ -70,13 +76,13 @@ async def test_login( assert r.headers["Location"] == return_url # Verify the logging. - expected_scopes_set = set(setup.config.issuer.group_mapping["admin"]) + expected_scopes_set = set(config.issuer.group_mapping["admin"]) expected_scopes_set.add("user:token") expected_scopes = " ".join(sorted(expected_scopes_set)) event = f"Successfully authenticated user {token.username} ({token.uid})" assert parse_log(caplog) == [ { - "event": f"Retrieving ID token from {setup.config.oidc.token_url}", + "event": f"Retrieving ID token from {config.oidc.token_url}", "level": "info", "method": "GET", "path": "/login", @@ -110,13 +116,13 @@ async def test_login( @pytest.mark.asyncio async def test_login_redirect_header( - client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" - await setup.configure("oidc") - token = setup.create_upstream_oidc_token(groups=["admin"]) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token(groups=["admin"]) + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" r = await client.get( @@ -135,20 +141,22 @@ async def test_login_redirect_header( @pytest.mark.asyncio -async def test_oauth2_callback(client: AsyncClient, setup: SetupTest) -> None: +async def test_oauth2_callback( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: """Test the compatibility /oauth2/callback route.""" - await setup.configure("oidc") - token = setup.create_upstream_oidc_token(groups=["admin"]) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) - assert setup.config.oidc + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token(groups=["admin"]) + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) + assert config.oidc return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) - assert query["redirect_uri"][0] == setup.config.oidc.redirect_url + assert query["redirect_uri"][0] == config.oidc.redirect_url # Simulate the return from the OpenID Connect provider. r = await client.get( @@ -160,24 +168,26 @@ async def test_oauth2_callback(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_claim_names(client: AsyncClient, setup: SetupTest) -> None: +async def test_claim_names( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: """Uses an alternate settings environment with non-default claims.""" - await setup.configure( - "oidc", username_claim="username", uid_claim="numeric_uid" + config = await configure( + tmp_path, "oidc", username_claim="username", uid_claim="numeric_uid" ) - assert setup.config.oidc - token = setup.create_upstream_oidc_token( + assert config.oidc + token = await setup.create_upstream_oidc_token( groups=["admin"], username="alt-username", numeric_uid=7890 ) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) - assert query["redirect_uri"][0] == setup.config.oidc.redirect_url + assert query["redirect_uri"][0] == config.oidc.redirect_url # Simulate the return from the OpenID Connect provider. r = await client.get( @@ -197,11 +207,14 @@ async def test_claim_names(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_callback_error( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: """Test an error return from the OIDC token endpoint.""" - await setup.configure("oidc") - assert setup.config.oidc + config = await configure(tmp_path, "oidc") + assert config.oidc return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -215,9 +228,7 @@ async def test_callback_error( "error": "error_code", "error_description": "description", } - setup.respx_mock.post(setup.config.oidc.token_url).respond( - 400, json=response - ) + setup.respx_mock.post(config.oidc.token_url).respond(400, json=response) # Simulate the return from the OpenID Connect provider. caplog.clear() @@ -229,7 +240,7 @@ async def test_callback_error( assert "error_code: description" in r.text assert parse_log(caplog) == [ { - "event": f"Retrieving ID token from {setup.config.oidc.token_url}", + "event": f"Retrieving ID token from {config.oidc.token_url}", "level": "info", "method": "GET", "path": "/oauth2/callback", @@ -250,7 +261,7 @@ async def test_callback_error( # Change the mock error response to not contain an error. We should then # internally raise the exception for the return status, which should # translate into an internal server error. - setup.respx_mock.post(setup.config.oidc.token_url).respond( + setup.respx_mock.post(config.oidc.token_url).respond( 400, json={"foo": "bar"} ) r = await client.get("/login", params={"rd": return_url}) @@ -264,9 +275,7 @@ async def test_callback_error( # Now try a reply that returns 200 but doesn't have the field we # need. - setup.respx_mock.post(setup.config.oidc.token_url).respond( - json={"foo": "bar"} - ) + setup.respx_mock.post(config.oidc.token_url).respond(json={"foo": "bar"}) r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -277,7 +286,7 @@ async def test_callback_error( assert "No id_token in token reply" in r.text # Return invalid JSON, which should raise an error during JSON decoding. - setup.respx_mock.post(setup.config.oidc.token_url).respond(content=b"foo") + setup.respx_mock.post(config.oidc.token_url).respond(content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -288,9 +297,7 @@ async def test_callback_error( assert "not valid JSON" in r.text # Finally, return invalid JSON and an error reply. - setup.respx_mock.post(setup.config.oidc.token_url).respond( - 400, content=b"foo" - ) + setup.respx_mock.post(config.oidc.token_url).respond(400, content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -302,9 +309,11 @@ async def test_callback_error( @pytest.mark.asyncio -async def test_connection_error(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - assert setup.config.oidc +async def test_connection_error( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + assert config.oidc return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -314,7 +323,7 @@ async def test_connection_error(client: AsyncClient, setup: SetupTest) -> None: # Register a connection error for the callback request to the OIDC # provider and check that an appropriate error is shown to the user. - token_url = setup.config.oidc.token_url + token_url = config.oidc.token_url setup.respx_mock.post(token_url).mock(side_effect=ConnectError) r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} @@ -324,17 +333,18 @@ async def test_connection_error(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_verify_error(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token(groups=["admin"]) - assert setup.config.oidc - issuer = setup.config.oidc.issuer +async def test_verify_error( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token(groups=["admin"]) + assert config.oidc + issuer = config.oidc.issuer config_url = urljoin(issuer, "/.well-known/openid-configuration") jwks_url = urljoin(issuer, "/.well-known/jwks.json") setup.respx_mock.get(config_url).respond(404) setup.respx_mock.get(jwks_url).respond(404) - setup.set_oidc_token_response("some-code", token) - assert setup.config.oidc + await setup.set_oidc_token_response("some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -353,14 +363,15 @@ async def test_verify_error(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token( +async def test_invalid_username( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token( groups=["admin"], sub="invalid@user", uid="invalid@user" ) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) - assert setup.config.oidc + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -378,15 +389,14 @@ async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_invalid_group_syntax( - client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token( + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token( isMemberOf=[{"name": "foo", "id": ["bar"]}] ) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) - assert setup.config.oidc + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -403,9 +413,11 @@ async def test_invalid_group_syntax( @pytest.mark.asyncio -async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token( +async def test_invalid_groups( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token( isMemberOf=[ {"name": "foo"}, {"group": "bar", "id": 4567}, @@ -416,9 +428,8 @@ async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: {"name": "21341", "id": 41233}, ] ) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) - assert setup.config.oidc + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -439,11 +450,13 @@ async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token(groups=[]) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) +async def test_no_valid_groups( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token(groups=[]) + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" r = await client.get("/login", params={"rd": return_url}) @@ -467,11 +480,13 @@ async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_unicode_name(client: AsyncClient, setup: SetupTest) -> None: - await setup.configure("oidc") - token = setup.create_upstream_oidc_token(name="名字", groups=["admin"]) - setup.set_oidc_token_response("some-code", token) - setup.set_oidc_configuration_response(setup.config.issuer.keypair) +async def test_unicode_name( + tmp_path: Path, client: AsyncClient, setup: SetupTest +) -> None: + config = await configure(tmp_path, "oidc") + token = await setup.create_upstream_oidc_token(name="名字", groups=["admin"]) + await setup.set_oidc_token_response("some-code", token) + await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) diff --git a/tests/handlers/logout_test.py b/tests/handlers/logout_test.py index ae55e7d70..2dc30af03 100644 --- a/tests/handlers/logout_test.py +++ b/tests/handlers/logout_test.py @@ -14,12 +14,16 @@ from _pytest.logging import LogCaptureFixture from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @pytest.mark.asyncio async def test_logout( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, + config: Config, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: token_data = await setup.create_session_token(scopes=["read:all"]) await setup.login(client, token_data.token) @@ -34,7 +38,7 @@ async def test_logout( # Check the redirect and logging. assert r.status_code == 307 - assert r.headers["Location"] == setup.config.after_logout_url + assert r.headers["Location"] == config.after_logout_url assert parse_log(caplog) == [ { "event": "Successful logout", @@ -72,13 +76,13 @@ async def test_logout_with_url(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_logout_not_logged_in( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, config: Config, caplog: LogCaptureFixture ) -> None: caplog.clear() r = await client.get("/logout") assert r.status_code == 307 - assert r.headers["Location"] == setup.config.after_logout_url + assert r.headers["Location"] == config.after_logout_url assert parse_log(caplog) == [ { "event": "Logout of already-logged-out session", @@ -107,9 +111,11 @@ async def test_logout_bad_url(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_logout_github( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, + config: Config, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: - assert setup.config.github user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -121,7 +127,7 @@ async def test_logout_github( ) # Log in and log out. - setup.set_github_response("some-code", user_info, expect_revoke=True) + await setup.set_github_response("some-code", user_info, expect_revoke=True) r = await client.get("/login", params={"rd": "https://example.com"}) assert r.status_code == 307 query = query_from_url(r.headers["Location"]) @@ -134,7 +140,7 @@ async def test_logout_github( # Check the redirect and logging. assert r.status_code == 307 - assert r.headers["Location"] == setup.config.after_logout_url + assert r.headers["Location"] == config.after_logout_url assert parse_log(caplog) == [ { "event": "Revoked GitHub OAuth authorization", diff --git a/tests/handlers/oidc_test.py b/tests/handlers/oidc_test.py index 81aa7d338..29cd3697a 100644 --- a/tests/handlers/oidc_test.py +++ b/tests/handlers/oidc_test.py @@ -16,24 +16,34 @@ from gafaelfawr.models.oidc import OIDCAuthorizationCode, OIDCToken from gafaelfawr.util import number_to_base64 from tests.support.constants import TEST_HOSTNAME -from tests.support.headers import parse_www_authenticate, query_from_url +from tests.support.headers import ( + assert_unauthorized_is_correct, + parse_www_authenticate, + query_from_url, +) from tests.support.logging import parse_log +from tests.support.settings import configure if TYPE_CHECKING: + from pathlib import Path from typing import Dict from _pytest.logging import LogCaptureFixture from httpx import AsyncClient + from gafaelfawr.config import Config from tests.support.setup import SetupTest @pytest.mark.asyncio async def test_login( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] - await setup.configure(oidc_clients=clients) + config = await configure(tmp_path, "github", oidc_clients=clients) token_data = await setup.create_session_token() await setup.login(client, token_data.token) return_url = f"https://{TEST_HOSTNAME}:4444/foo?a=bar&b=baz" @@ -103,27 +113,27 @@ async def test_login( "id_token": ANY, } assert isinstance(data["expires_in"], int) - exp_seconds = setup.config.issuer.exp_minutes * 60 + exp_seconds = config.issuer.exp_minutes * 60 assert exp_seconds - 5 <= data["expires_in"] <= exp_seconds assert data["access_token"] == data["id_token"] verifier = setup.factory.create_token_verifier() token = verifier.verify_internal_token(OIDCToken(encoded=data["id_token"])) assert token.claims == { - "aud": setup.config.issuer.aud, + "aud": config.issuer.aud, "exp": ANY, "iat": ANY, - "iss": setup.config.issuer.iss, + "iss": config.issuer.iss, "jti": OIDCAuthorizationCode.from_str(code).key, "name": token_data.name, "preferred_username": token_data.username, "scope": "openid", "sub": token_data.username, - setup.config.issuer.username_claim: token_data.username, - setup.config.issuer.uid_claim: token_data.uid, + config.issuer.username_claim: token_data.username, + config.issuer.uid_claim: token_data.uid, } now = time.time() - expected_exp = now + setup.config.issuer.exp_minutes * 60 + expected_exp = now + config.issuer.exp_minutes * 60 assert expected_exp - 5 <= token.claims["exp"] <= expected_exp assert now - 5 <= token.claims["iat"] <= now @@ -143,10 +153,13 @@ async def test_login( @pytest.mark.asyncio async def test_unauthenticated( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] - await setup.configure(oidc_clients=clients) + await configure(tmp_path, "github", oidc_clients=clients) return_url = f"https://{TEST_HOSTNAME}:4444/foo?a=bar&b=baz" login_params = { "response_type": "code", @@ -182,10 +195,13 @@ async def test_unauthenticated( @pytest.mark.asyncio async def test_login_errors( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] - await setup.configure(oidc_clients=clients) + await configure(tmp_path, "github", oidc_clients=clients) token_data = await setup.create_session_token() await setup.login(client, token_data.token) @@ -293,13 +309,16 @@ async def test_login_errors( @pytest.mark.asyncio async def test_token_errors( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + tmp_path: Path, + client: AsyncClient, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: clients = [ OIDCClient(client_id="some-id", client_secret="some-secret"), OIDCClient(client_id="other-id", client_secret="other-secret"), ] - await setup.configure(oidc_clients=clients) + await configure(tmp_path, "github", oidc_clients=clients) token_data = await setup.create_session_token() token = token_data.token oidc_service = setup.factory.create_oidc_service() @@ -474,19 +493,17 @@ async def test_userinfo(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_auth(client: AsyncClient, config: Config) -> None: r = await client.get("/auth/userinfo") - - assert r.status_code == 401 - authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) - assert not isinstance(authenticate, AuthErrorChallenge) - assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert_unauthorized_is_correct(r, config) @pytest.mark.asyncio async def test_invalid( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, + config: Config, + setup: SetupTest, + caplog: LogCaptureFixture, ) -> None: token_data = await setup.create_session_token() issuer = setup.factory.create_token_issuer() @@ -502,7 +519,7 @@ async def test_invalid( authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_request assert authenticate.error_description == "Unknown Authorization type token" @@ -526,7 +543,7 @@ async def test_invalid( authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_request assert authenticate.error_description == "Malformed Authorization header" @@ -540,7 +557,7 @@ async def test_invalid( authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"]) assert isinstance(authenticate, AuthErrorChallenge) assert authenticate.auth_type == AuthType.Bearer - assert authenticate.realm == setup.config.realm + assert authenticate.realm == config.realm assert authenticate.error == AuthError.invalid_token assert authenticate.error_description @@ -558,12 +575,12 @@ async def test_invalid( @pytest.mark.asyncio -async def test_well_known_jwks(client: AsyncClient, setup: SetupTest) -> None: +async def test_well_known_jwks(client: AsyncClient, config: Config) -> None: r = await client.get("/.well-known/jwks.json") assert r.status_code == 200 result = r.json() - keypair = setup.config.issuer.keypair + keypair = config.issuer.keypair assert result == { "keys": [ { @@ -584,13 +601,13 @@ async def test_well_known_jwks(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_well_known_oidc(client: AsyncClient, setup: SetupTest) -> None: +async def test_well_known_oidc(client: AsyncClient, config: Config) -> None: r = await client.get("/.well-known/openid-configuration") assert r.status_code == 200 - base_url = setup.config.issuer.iss + base_url = config.issuer.iss assert r.json() == { - "issuer": setup.config.issuer.iss, + "issuer": config.issuer.iss, "authorization_endpoint": base_url + "/auth/openid/login", "token_endpoint": base_url + "/auth/openid/token", "userinfo_endpoint": base_url + "/auth/openid/userinfo", diff --git a/tests/issuer_test.py b/tests/issuer_test.py index 2f2599b62..8bc923d3f 100644 --- a/tests/issuer_test.py +++ b/tests/issuer_test.py @@ -8,33 +8,37 @@ import pytest +from tests.support.settings import configure + if TYPE_CHECKING: + from pathlib import Path + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_issue_token(setup: SetupTest) -> None: - await setup.configure("oidc") +async def test_issue_token(tmp_path: Path, setup: SetupTest) -> None: + config = await configure(tmp_path, "oidc") issuer = setup.factory.create_token_issuer() token_data = await setup.create_session_token() oidc_token = issuer.issue_token(token_data, jti="new-jti", scope="openid") assert oidc_token.claims == { - "aud": setup.config.issuer.aud, + "aud": config.issuer.aud, "exp": ANY, "iat": ANY, - "iss": setup.config.issuer.iss, + "iss": config.issuer.iss, "jti": "new-jti", "name": token_data.name, "preferred_username": token_data.username, "scope": "openid", "sub": token_data.username, - setup.config.issuer.username_claim: token_data.username, - setup.config.issuer.uid_claim: token_data.uid, + config.issuer.username_claim: token_data.username, + config.issuer.uid_claim: token_data.uid, } now = time.time() assert now - 5 <= oidc_token.claims["iat"] <= now + 5 - expected_exp = now + setup.config.issuer.exp_minutes * 60 + expected_exp = now + config.issuer.exp_minutes * 60 assert expected_exp - 5 <= oidc_token.claims["exp"] <= expected_exp + 5 diff --git a/tests/services/oidc_test.py b/tests/services/oidc_test.py index 31ff89f7b..cf140a262 100644 --- a/tests/services/oidc_test.py +++ b/tests/services/oidc_test.py @@ -17,22 +17,25 @@ UnauthorizedClientException, ) from gafaelfawr.models.oidc import OIDCAuthorizationCode +from tests.support.settings import configure if TYPE_CHECKING: + from pathlib import Path + from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_issue_code(setup: SetupTest) -> None: +async def test_issue_code(tmp_path: Path, setup: SetupTest) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] - await setup.configure(oidc_clients=clients) + config = await configure(tmp_path, "github", oidc_clients=clients) oidc_service = setup.factory.create_oidc_service() token_data = await setup.create_session_token() token = token_data.token redirect_uri = "https://example.com/" - assert setup.config.oidc_server - assert list(setup.config.oidc_server.clients) == clients + assert config.oidc_server + assert list(config.oidc_server.clients) == clients with pytest.raises(UnauthorizedClientException): await oidc_service.issue_code("unknown-client", redirect_uri, token) @@ -40,7 +43,7 @@ async def test_issue_code(setup: SetupTest) -> None: code = await oidc_service.issue_code("some-id", redirect_uri, token) encrypted_code = await setup.redis.get(f"oidc:{code.key}") assert encrypted_code - fernet = Fernet(setup.config.session_secret.encode()) + fernet = Fernet(config.session_secret.encode()) serialized_code = json.loads(fernet.decrypt(encrypted_code)) assert serialized_code == { "code": { @@ -60,12 +63,12 @@ async def test_issue_code(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_redeem_code(setup: SetupTest) -> None: +async def test_redeem_code(tmp_path: Path, setup: SetupTest) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] - await setup.configure(oidc_clients=clients) + config = await configure(tmp_path, "github", oidc_clients=clients) oidc_service = setup.factory.create_oidc_service() token_data = await setup.create_session_token() token = token_data.token @@ -76,29 +79,29 @@ async def test_redeem_code(setup: SetupTest) -> None: "client-2", "client-2-secret", redirect_uri, code ) assert oidc_token.claims == { - "aud": setup.config.issuer.aud, + "aud": config.issuer.aud, "iat": ANY, "exp": ANY, - "iss": setup.config.issuer.iss, + "iss": config.issuer.iss, "jti": code.key, "name": token_data.name, "preferred_username": token_data.username, "scope": "openid", "sub": token_data.username, - setup.config.issuer.username_claim: token_data.username, - setup.config.issuer.uid_claim: token_data.uid, + config.issuer.username_claim: token_data.username, + config.issuer.uid_claim: token_data.uid, } assert not await setup.redis.get(f"oidc:{code.key}") @pytest.mark.asyncio -async def test_redeem_code_errors(setup: SetupTest) -> None: +async def test_redeem_code_errors(tmp_path: Path, setup: SetupTest) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] - await setup.configure(oidc_clients=clients) + await configure(tmp_path, "github", oidc_clients=clients) oidc_service = setup.factory.create_oidc_service() token_data = await setup.create_session_token() token = token_data.token diff --git a/tests/services/token_test.py b/tests/services/token_test.py index b4f092274..1783e588a 100644 --- a/tests/services/token_test.py +++ b/tests/services/token_test.py @@ -29,11 +29,12 @@ from tests.support.util import assert_is_now if TYPE_CHECKING: + from gafaelfawr.config import Config from tests.support.setup import SetupTest @pytest.mark.asyncio -async def test_session_token(setup: SetupTest) -> None: +async def test_session_token(config: Config, setup: SetupTest) -> None: token_service = setup.factory.create_token_service() user_info = TokenUserInfo( username="example", @@ -64,7 +65,7 @@ async def test_session_token(setup: SetupTest) -> None: ], ) assert_is_now(data.created) - expires = data.created + timedelta(minutes=setup.config.issuer.exp_minutes) + expires = data.created + timedelta(minutes=config.issuer.exp_minutes) assert data.expires == expires info = await token_service.get_token_info_unchecked(token.key) @@ -178,7 +179,7 @@ async def test_user_token(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_notebook_token(setup: SetupTest) -> None: +async def test_notebook_token(config: Config, setup: SetupTest) -> None: user_info = TokenUserInfo( username="example", name="Example Person", @@ -300,12 +301,12 @@ async def test_notebook_token(setup: SetupTest) -> None: assert new_token != token info = await token_service.get_token_info_unchecked(new_token.key) assert info - expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) + expires = info.created + timedelta(minutes=config.issuer.exp_minutes) assert info.expires == expires @pytest.mark.asyncio -async def test_internal_token(setup: SetupTest) -> None: +async def test_internal_token(config: Config, setup: SetupTest) -> None: user_info = TokenUserInfo( username="example", name="Example Person", @@ -398,7 +399,7 @@ async def test_internal_token(setup: SetupTest) -> None: # easiest way to do this is to use the internals of the token service. second_internal_token = Token() created = current_datetime() - expires = created + setup.config.token_lifetime + expires = created + config.token_lifetime internal_token_data = TokenData( token=second_internal_token, username=data.username, @@ -460,12 +461,12 @@ async def test_internal_token(setup: SetupTest) -> None: assert new_internal_token != internal_token info = await token_service.get_token_info_unchecked(new_internal_token.key) assert info and info.scopes == [] - expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) + expires = info.created + timedelta(minutes=config.issuer.exp_minutes) assert info.expires == expires @pytest.mark.asyncio -async def test_child_token_lifetime(setup: SetupTest) -> None: +async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: """Test that a new internal token is generated at half its lifetime.""" session_token_data = await setup.create_session_token() token_service = setup.factory.create_token_service() @@ -474,7 +475,7 @@ async def test_child_token_lifetime(setup: SetupTest) -> None: # lifetime for an internal token. This will get us a short-lived internal # token that should be ineligible for handing out for a user token that # doesn't expire. - delta = timedelta(minutes=(setup.config.issuer.exp_minutes / 2) - 5) + delta = timedelta(minutes=(config.issuer.exp_minutes / 2) - 5) expires = current_datetime() + delta user_token = await token_service.create_user_token( session_token_data, @@ -513,7 +514,7 @@ async def test_child_token_lifetime(setup: SetupTest) -> None: # Change the expiration of the user token to longer than the maximum # internal token lifetime. - new_delta = timedelta(minutes=setup.config.issuer.exp_minutes * 2) + new_delta = timedelta(minutes=config.issuer.exp_minutes * 2) expires = current_datetime() + new_delta assert await token_service.modify_token( user_token.key, @@ -534,7 +535,7 @@ async def test_child_token_lifetime(setup: SetupTest) -> None: internal_token = new_internal_token internal_token_data = await token_service.get_data(internal_token) assert internal_token_data - delta = timedelta(minutes=setup.config.issuer.exp_minutes) + delta = timedelta(minutes=config.issuer.exp_minutes) assert internal_token_data.expires == internal_token_data.created + delta new_notebook_token = await token_service.get_notebook_token( user_token_data, ip_address="127.0.0.1" @@ -1041,7 +1042,7 @@ async def test_delete_cascade(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_modify_expires(setup: SetupTest) -> None: +async def test_modify_expires(config: Config, setup: SetupTest) -> None: """Test that expiration changes cascade to subtokens.""" token_service = setup.factory.create_token_service() session_token_data = await setup.create_session_token( @@ -1077,7 +1078,7 @@ async def test_modify_expires(setup: SetupTest) -> None: # Check the expiration of all of those tokens matches the default # expiration for generated tokens. - delta = timedelta(minutes=setup.config.issuer.exp_minutes) + delta = timedelta(minutes=config.issuer.exp_minutes) assert notebook_token_data.expires == notebook_token_data.created + delta assert internal_token_data.expires == internal_token_data.created + delta assert nested_token_data.expires == notebook_token_data.expires @@ -1088,7 +1089,7 @@ async def test_modify_expires(setup: SetupTest) -> None: assert ttl - 5 <= await setup.redis.ttl(f"token:{token.key}") <= ttl # Change the expiration of the user token. - new_delta = timedelta(minutes=setup.config.issuer.exp_minutes / 2) + new_delta = timedelta(minutes=config.issuer.exp_minutes / 2) new_expires = user_token_data.created + new_delta await token_service.modify_token( user_token.key, @@ -1115,7 +1116,7 @@ async def test_modify_expires(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid(setup: SetupTest) -> None: +async def test_invalid(config: Config, setup: SetupTest) -> None: token_service = setup.factory.create_token_service() expires = int(timedelta(days=1).total_seconds()) @@ -1128,7 +1129,7 @@ async def test_invalid(setup: SetupTest) -> None: assert await token_service.get_data(token) is None # Malformed session. - fernet = Fernet(setup.config.session_secret.encode()) + fernet = Fernet(config.session_secret.encode()) raw_data = fernet.encrypt(b"malformed json") await setup.redis.set(f"token:{token.key}", raw_data, ex=expires) assert await token_service.get_data(token) is None diff --git a/tests/support/github.py b/tests/support/github.py index f2e0fd8e2..2d2b2fc08 100644 --- a/tests/support/github.py +++ b/tests/support/github.py @@ -11,6 +11,7 @@ from httpx import Response +from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.providers.github import GitHubProvider if TYPE_CHECKING: @@ -151,9 +152,8 @@ def post_token(self, request: Request) -> Response: ) -def mock_github( +async def mock_github( respx_mock: respx.Router, - config: GitHubConfig, code: str, user_info: GitHubUserInfo, *, @@ -166,8 +166,6 @@ def mock_github( ---------- respx_mock : `respx.Router` The mock router. - config : `gafaelfawr.config.GitHubConfig` - Configuration of the GitHub provider. code : `str` The code that Gafaelfawr must send to redeem a token. user_info : `gafaelfawr.providers.github.GitHubUserInfo` @@ -178,8 +176,15 @@ def mock_github( Whether to expect a revocation of the token after returning all user information. Default: `False` """ + config = await config_dependency() + assert config.github mock = MockGitHub( - respx_mock, config, code, user_info, paginate_teams, expect_revoke + respx_mock, + config.github, + code, + user_info, + paginate_teams, + expect_revoke, ) token_url = GitHubProvider._TOKEN_URL respx_mock.post(token_url).mock(side_effect=mock.post_token) diff --git a/tests/support/headers.py b/tests/support/headers.py index 40ac3eb7d..a8442875c 100644 --- a/tests/support/headers.py +++ b/tests/support/headers.py @@ -16,6 +16,38 @@ if TYPE_CHECKING: from typing import Dict, List + from httpx import Response + + from gafaelfawr.config import Config + +__all__ = [ + "assert_unauthorized_is_correct", + "parse_www_authenticate", + "query_from_url", +] + + +def assert_unauthorized_is_correct( + r: Response, config: Config, auth_type: AuthType = AuthType.Bearer +) -> None: + """Check that an unauthorized response is correct. + + Parameters + ---------- + r : `httpx.Response` + The unauthorized response. + config : `gafaelfawr.config.Config` + The Gafaelfawr configuration. + auth_type : `gafaelfawr.auth.AuthType` + Expected authentication type. + """ + assert r.status_code == 401 + assert r.headers["Cache-Control"] == "no-cache, must-revalidate" + challenge = parse_www_authenticate(r.headers["WWW-Authenticate"]) + assert not isinstance(challenge, AuthErrorChallenge) + assert challenge.auth_type == auth_type + assert challenge.realm == config.realm + def parse_www_authenticate(header: str) -> AuthChallenge: """Parse a ``WWW-Authenticate`` header into this representation. diff --git a/tests/support/oidc.py b/tests/support/oidc.py index 373507104..a6a89d2a5 100644 --- a/tests/support/oidc.py +++ b/tests/support/oidc.py @@ -7,6 +7,8 @@ from httpx import Response +from gafaelfawr.dependencies.config import config_dependency + if TYPE_CHECKING: from typing import Optional @@ -95,9 +97,8 @@ def post_token(self, request: Request) -> Response: ) -def mock_oidc_provider_config( +async def mock_oidc_provider_config( respx_mock: respx.Router, - config: Config, keypair: Optional[RSAKeyPair] = None, kid: Optional[str] = None, ) -> None: @@ -107,14 +108,13 @@ def mock_oidc_provider_config( ---------- respx_mock : `respx.Router` The mock router. - config : `gafaelfawr.config.Config` - Configuration for Gafaelfawr. keypair : `gafaelfawr.keypair.RSAKeyPair`, optional The keypair to use. Defaults to the configured issuer keypair. kid : `str`, optional The key ID to return. Defaults to the first key ID in the configuration. """ + config = await config_dependency() assert config.oidc mock = MockOIDCConfig(config, keypair, kid) issuer = config.oidc.issuer @@ -124,8 +124,8 @@ def mock_oidc_provider_config( respx_mock.get(jwks_url).mock(side_effect=mock.get_jwks) -def mock_oidc_provider_token( - respx_mock: respx.Router, config: Config, code: str, token: OIDCToken +async def mock_oidc_provider_token( + respx_mock: respx.Router, code: str, token: OIDCToken ) -> None: """Mock out the API for the upstream OpenID Connect provider. @@ -133,13 +133,12 @@ def mock_oidc_provider_token( ---------- respx_mock : `respx.Router` The mock router. - config : `gafaelfawr.config.Config` - Configuration for Gafaelfawr. code : `str` The code that Gafaelfawr must send to redeem for a token. token : `gafaelfawr.models.oidc.OIDCToken` The token to return after authentication. """ + config = await config_dependency() assert config.oidc mock = MockOIDCToken(config, code, token) respx_mock.post(config.oidc.token_url).mock(side_effect=mock.post_token) diff --git a/tests/support/settings.py b/tests/support/settings.py index af51a3b23..7c860300c 100644 --- a/tests/support/settings.py +++ b/tests/support/settings.py @@ -8,15 +8,65 @@ from cryptography.fernet import Fernet +from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.keypair import RSAKeyPair from tests.support.constants import TEST_DATABASE_URL if TYPE_CHECKING: from typing import List, Optional, Union - from gafaelfawr.config import OIDCClient + from gafaelfawr.config import Config, OIDCClient -__all__ = ["build_settings", "build_settings_file", "store_secret"] +__all__ = [ + "build_settings", + "configure", + "store_secret", +] + + +def store_secret(tmp_path: Path, name: str, secret: bytes) -> Path: + """Store a secret in a temporary path. + + Parameters + ---------- + tmp_path : `pathlib.Path` + The root of the temporary area. + name : `str` + The name of the secret to construct nice file names. + secret : `bytes` + The value of the secret. + """ + secret_path = tmp_path / name + secret_path.write_bytes(secret) + return secret_path + + +def _build_settings_file( + tmp_path: Path, template: str, **kwargs: Union[str, Path] +) -> Path: + """Construct a settings file from a format template. + + Parameters + ---------- + tmp_path : `pathlib.Path` + The root of the temporary area. + template : `str` + Name of the configuration template to use. + **kwargs : `str` + The values to substitute into the template. + + Returns + ------- + settings_path : `pathlib.Path` + The path to the newly-constructed configuration file. + """ + template_file = template + ".yaml.in" + template_path = Path(__file__).parent.parent / "settings" / template_file + template = template_path.read_text() + settings = template.format(**kwargs) + settings_path = tmp_path / "gafaelfawr.yaml" + settings_path.write_text(settings) + return settings_path def build_settings( @@ -56,7 +106,7 @@ def build_settings( else: database_url = TEST_DATABASE_URL - settings_path = build_settings_file( + settings_path = _build_settings_file( tmp_path, template, database_url=database_url, @@ -84,46 +134,40 @@ def build_settings( return settings_path -def build_settings_file( - tmp_path: Path, template: str, **kwargs: Union[str, Path] -) -> Path: - """Construct a settings file from a format template. +async def configure( + tmp_path: Path, + template: str, + *, + oidc_clients: Optional[List[OIDCClient]] = None, + **settings: str, +) -> Config: + """Change the test application configuration. + + This cannot be used to change the database URL because sessions will not + be recreated or the database reinitialized. Parameters ---------- tmp_path : `pathlib.Path` - The root of the temporary area. + Root of the test temporary directory, used to write the settings + file. template : `str` - Name of the configuration template to use. - **kwargs : `str` - The values to substitute into the template. + Settings template to use. + oidc_clients : List[`gafaelfawr.config.OIDCClient`], optional + Configuration information for clients of the OpenID Connect server. + **settings : str, optional + Any additional settings to add to the settings file. Returns ------- - settings_path : `pathlib.Path` - The path to the newly-constructed configuration file. - """ - template_file = template + ".yaml.in" - template_path = Path(__file__).parent.parent / "settings" / template_file - template = template_path.read_text() - settings = template.format(**kwargs) - settings_path = tmp_path / "gafaelfawr.yaml" - settings_path.write_text(settings) - return settings_path - - -def store_secret(tmp_path: Path, name: str, secret: bytes) -> Path: - """Store a secret in a temporary path. - - Parameters - ---------- - tmp_path : `pathlib.Path` - The root of the temporary area. - name : `str` - The name of the secret to construct nice file names. - secret : `bytes` - The value of the secret. + config : `gafaelfawr.config.Config` + The new configuration. """ - secret_path = tmp_path / name - secret_path.write_bytes(secret) - return secret_path + settings_path = build_settings( + tmp_path, + template, + oidc_clients, + **settings, + ) + config_dependency.set_settings_path(str(settings_path)) + return await config_dependency() diff --git a/tests/support/setup.py b/tests/support/setup.py index 37e08f2a9..506730e4e 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -24,7 +24,6 @@ mock_oidc_provider_config, mock_oidc_provider_token, ) -from tests.support.settings import build_settings from tests.support.tokens import create_upstream_oidc_token if TYPE_CHECKING: @@ -33,7 +32,7 @@ from aioredis import Redis - from gafaelfawr.config import Config, OIDCClient + from gafaelfawr.config import Config from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken, OIDCVerifiedToken from gafaelfawr.providers.github import GitHubUserInfo @@ -119,7 +118,6 @@ def __init__( ) -> None: self.tmp_path = tmp_path self.respx_mock = respx_mock - self.config = config self.redis = redis self.session = session self.http_client = http_client @@ -138,44 +136,15 @@ def factory(self) -> ComponentFactory: factory : `gafaelfawr.factory.ComponentFactory` Newly-created factory. """ + assert config_dependency._config return ComponentFactory( - config=self.config, + config=config_dependency._config, redis=self.redis, http_client=self.http_client, session=self.session, logger=self.logger, ) - async def configure( - self, - template: str = "github", - *, - oidc_clients: Optional[List[OIDCClient]] = None, - **settings: str, - ) -> None: - """Change the test application configuration. - - This cannot be used to change the database URL because the internal - session is not recreated. - - Parameters - ---------- - template : `str` - Settings template to use. - oidc_clients : List[`gafaelfawr.config.OIDCClient`] or `None` - Configuration information for clients of the OpenID Connect server. - **settings : str - Any additional settings to add to the settings file. - """ - settings_path = build_settings( - self.tmp_path, - template, - oidc_clients, - **settings, - ) - config_dependency.set_settings_path(str(settings_path)) - self.config = await config_dependency() - async def create_session_token( self, *, @@ -223,7 +192,7 @@ async def create_session_token( await self.session.commit() return data - def create_upstream_oidc_token( + async def create_upstream_oidc_token( self, *, kid: Optional[str] = None, @@ -247,12 +216,11 @@ def create_upstream_oidc_token( token : `gafaelfawr.models..oidc.OIDCVerifiedToken` The generated token. """ + config = await config_dependency() if not kid: - assert self.config.oidc - kid = self.config.oidc.key_ids[0] - return create_upstream_oidc_token( - self.config, kid, groups=groups, **claims - ) + assert config.oidc + kid = config.oidc.key_ids[0] + return create_upstream_oidc_token(config, kid, groups=groups, **claims) async def login(self, client: AsyncClient, token: Token) -> str: """Create a valid Gafaelfawr session. @@ -288,7 +256,7 @@ def logout(self, client: AsyncClient) -> None: """ del client.cookies[COOKIE_NAME] - def set_github_response( + async def set_github_response( self, code: str, user_info: GitHubUserInfo, @@ -310,17 +278,15 @@ def set_github_response( Whether to expect a revocation of the token after returning all user information. Default: `False` """ - assert self.config.github - mock_github( + await mock_github( self.respx_mock, - self.config.github, code, user_info, paginate_teams=paginate_teams, expect_revoke=expect_revoke, ) - def set_oidc_configuration_response( + async def set_oidc_configuration_response( self, keypair: Optional[RSAKeyPair] = None, kid: Optional[str] = None, @@ -336,9 +302,9 @@ def set_oidc_configuration_response( Key ID for the key. If not given, defaults to the first key ID in the configured key_ids list. """ - mock_oidc_provider_config(self.respx_mock, self.config, keypair, kid) + await mock_oidc_provider_config(self.respx_mock, keypair, kid) - def set_oidc_token_response( + async def set_oidc_token_response( self, code: str, token: OIDCToken, @@ -352,4 +318,4 @@ def set_oidc_token_response( token : `gafaelfawr.tokens.Token` The token. """ - mock_oidc_provider_token(self.respx_mock, self.config, code, token) + await mock_oidc_provider_token(self.respx_mock, code, token) diff --git a/tests/token_cache_test.py b/tests/token_cache_test.py index 085e63c69..5708bbd6a 100644 --- a/tests/token_cache_test.py +++ b/tests/token_cache_test.py @@ -13,6 +13,7 @@ from gafaelfawr.util import current_datetime if TYPE_CHECKING: + from gafaelfawr.config import Config from tests.support.setup import SetupTest @@ -85,12 +86,12 @@ async def test_invalid(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_expiration(setup: SetupTest) -> None: +async def test_expiration(config: Config, setup: SetupTest) -> None: """The cache is valid until half the lifetime of the child token.""" token_data = await setup.create_session_token(scopes=["read:all"]) - lifetime = setup.config.token_lifetime + lifetime = config.token_lifetime now = current_datetime() - storage = RedisStorage(TokenData, setup.config.session_secret, setup.redis) + storage = RedisStorage(TokenData, config.session_secret, setup.redis) token_store = TokenRedisStore(storage, setup.logger) token_cache = setup.factory.create_token_cache() @@ -98,7 +99,7 @@ async def test_expiration(setup: SetupTest) -> None: # typical token lifetime in the future and cache that token as an internal # token for our session token. created = now - timedelta(seconds=lifetime.total_seconds() // 2) - expires = created + setup.config.token_lifetime + timedelta(seconds=5) + expires = created + lifetime + timedelta(seconds=5) internal_token_data = TokenData( token=Token(), username=token_data.username, diff --git a/tests/verify_test.py b/tests/verify_test.py index 815148805..04643ab1b 100644 --- a/tests/verify_test.py +++ b/tests/verify_test.py @@ -19,8 +19,10 @@ ) from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken +from tests.support.settings import configure if TYPE_CHECKING: + from pathlib import Path from typing import Any, Dict, Optional from _pytest._code import ExceptionInfo @@ -45,18 +47,18 @@ def encode_token( @pytest.mark.asyncio -async def test_verify_oidc(setup: SetupTest) -> None: - await setup.configure("oidc") +async def test_verify_oidc(tmp_path: Path, setup: SetupTest) -> None: + config = await configure(tmp_path, "oidc") verifier = setup.factory.create_token_verifier() now = datetime.now(timezone.utc) exp = now + timedelta(days=24) payload: Dict[str, Any] = { - "aud": setup.config.verifier.oidc_aud, + "aud": config.verifier.oidc_aud, "iat": int(now.timestamp()), "exp": int(exp.timestamp()), } - keypair = setup.config.issuer.keypair + keypair = config.issuer.keypair token = encode_token(payload, keypair) excinfo: ExceptionInfo[Exception] @@ -79,74 +81,72 @@ async def test_verify_oidc(setup: SetupTest) -> None: assert str(excinfo.value) == "Unknown issuer: https://bogus.example.com/" # Unknown kid. - payload["iss"] = setup.config.verifier.oidc_iss + payload["iss"] = config.verifier.oidc_iss token = encode_token(payload, keypair, kid="a-kid") with pytest.raises(UnknownKeyIdException) as excinfo: await verifier.verify_oidc_token(token) - expected = f"kid a-kid not allowed for {setup.config.verifier.oidc_iss}" + expected = f"kid a-kid not allowed for {config.verifier.oidc_iss}" assert str(excinfo.value) == expected # Missing username claim. - setup.set_oidc_configuration_response(keypair) - kid = setup.config.verifier.oidc_kids[0] - token = encode_token(payload, setup.config.issuer.keypair, kid=kid) + await setup.set_oidc_configuration_response(keypair) + kid = config.verifier.oidc_kids[0] + token = encode_token(payload, config.issuer.keypair, kid=kid) with pytest.raises(MissingClaimsException) as excinfo: await verifier.verify_oidc_token(token) - expected = f"No {setup.config.verifier.username_claim} claim in token" + expected = f"No {config.verifier.username_claim} claim in token" assert str(excinfo.value) == expected # Missing UID claim. - setup.set_oidc_configuration_response(keypair) - payload[setup.config.verifier.username_claim] = "some-user" - token = encode_token(payload, setup.config.issuer.keypair, kid=kid) + await setup.set_oidc_configuration_response(keypair) + payload[config.verifier.username_claim] = "some-user" + token = encode_token(payload, config.issuer.keypair, kid=kid) with pytest.raises(MissingClaimsException) as excinfo: await verifier.verify_oidc_token(token) - expected = f"No {setup.config.verifier.uid_claim} claim in token" + expected = f"No {config.verifier.uid_claim} claim in token" assert str(excinfo.value) == expected @pytest.mark.asyncio -async def test_verify_oidc_no_kids(setup: SetupTest) -> None: - await setup.configure("oidc-no-kids") +async def test_verify_oidc_no_kids(tmp_path: Path, setup: SetupTest) -> None: + config = await configure(tmp_path, "oidc-no-kids") verifier = setup.factory.create_token_verifier() - setup.set_oidc_configuration_response(setup.config.issuer.keypair, "kid") + await setup.set_oidc_configuration_response(config.issuer.keypair, "kid") now = datetime.now(timezone.utc) exp = now + timedelta(days=24) payload: Dict[str, Any] = { - "aud": setup.config.verifier.oidc_aud, + "aud": config.verifier.oidc_aud, "iat": int(now.timestamp()), - "iss": setup.config.verifier.oidc_iss, + "iss": config.verifier.oidc_iss, "exp": int(exp.timestamp()), } - keypair = setup.config.issuer.keypair + keypair = config.issuer.keypair token = encode_token(payload, keypair, kid="a-kid") with pytest.raises(UnknownKeyIdException) as excinfo: await verifier.verify_oidc_token(token) - expected = f"Issuer {setup.config.verifier.oidc_iss} has no kid a-kid" + expected = f"Issuer {config.verifier.oidc_iss} has no kid a-kid" assert str(excinfo.value) == expected @pytest.mark.asyncio -async def test_key_retrieval(setup: SetupTest) -> None: - await setup.configure("oidc-no-kids") - assert setup.config.oidc +async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: + config = await configure(tmp_path, "oidc-no-kids") + assert config.oidc verifier = setup.factory.create_token_verifier() # Initial working JWKS configuration. - jwks = setup.config.issuer.keypair.public_key_as_jwks("some-kid") + jwks = config.issuer.keypair.public_key_as_jwks("some-kid") # Register that handler at the well-known JWKS endpoint. This will return # a connection refused from the OpenID Connect endpoint. - jwks_url = urljoin(setup.config.oidc.issuer, "/.well-known/jwks.json") - oidc_url = urljoin( - setup.config.oidc.issuer, "/.well-known/openid-configuration" - ) + jwks_url = urljoin(config.oidc.issuer, "/.well-known/jwks.json") + oidc_url = urljoin(config.oidc.issuer, "/.well-known/openid-configuration") setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) setup.respx_mock.get(oidc_url).respond(404) # Check token verification with this configuration. - token = setup.create_upstream_oidc_token(kid="some-kid") + token = await setup.create_upstream_oidc_token(kid="some-kid") assert await verifier.verify_oidc_token(token) # Wrong algorithm for the key. @@ -166,7 +166,7 @@ async def test_key_retrieval(setup: SetupTest) -> None: # Try with a new key ID and return a malformed reponse. setup.respx_mock.get(jwks_url).respond(json=["foo"]) - token = setup.create_upstream_oidc_token(kid="malformed") + token = await setup.create_upstream_oidc_token(kid="malformed") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) @@ -180,7 +180,7 @@ async def test_key_retrieval(setup: SetupTest) -> None: jwks.keys[1].kid = "another-kid" setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) setup.respx_mock.get(oidc_url).respond(json=["foo"]) - token = setup.create_upstream_oidc_token(kid="another-kid") + token = await setup.create_upstream_oidc_token(kid="another-kid") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) From 92a713ca6cd6fdd10b1007495d47c2c06d06cf01 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 17:16:13 -0800 Subject: [PATCH 12/24] Remove create_upstream_oidc_token wrapper SetupTest now adds nothing of value and tests can call the underlying function directly, with a slight tweak to how the kid default is handled. --- tests/handlers/login_oidc_test.py | 21 ++++++++++--------- tests/support/setup.py | 35 ++----------------------------- tests/support/tokens.py | 11 ++++++---- tests/verify_test.py | 7 ++++--- 4 files changed, 24 insertions(+), 50 deletions(-) diff --git a/tests/handlers/login_oidc_test.py b/tests/handlers/login_oidc_test.py index 616d8bcff..c4ff1f036 100644 --- a/tests/handlers/login_oidc_test.py +++ b/tests/handlers/login_oidc_test.py @@ -11,6 +11,7 @@ from tests.support.logging import parse_log from tests.support.settings import configure +from tests.support.tokens import create_upstream_oidc_token if TYPE_CHECKING: from pathlib import Path @@ -29,7 +30,7 @@ async def test_login( caplog: LogCaptureFixture, ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token( + token = await create_upstream_oidc_token( groups=["admin"], name="Some Person", email="person@example.com" ) await setup.set_oidc_token_response("some-code", token) @@ -120,7 +121,7 @@ async def test_login_redirect_header( ) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token(groups=["admin"]) + token = await create_upstream_oidc_token(groups=["admin"]) await setup.set_oidc_token_response("some-code", token) await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" @@ -146,7 +147,7 @@ async def test_oauth2_callback( ) -> None: """Test the compatibility /oauth2/callback route.""" config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token(groups=["admin"]) + token = await create_upstream_oidc_token(groups=["admin"]) await setup.set_oidc_token_response("some-code", token) await setup.set_oidc_configuration_response(config.issuer.keypair) assert config.oidc @@ -176,7 +177,7 @@ async def test_claim_names( tmp_path, "oidc", username_claim="username", uid_claim="numeric_uid" ) assert config.oidc - token = await setup.create_upstream_oidc_token( + token = await create_upstream_oidc_token( groups=["admin"], username="alt-username", numeric_uid=7890 ) await setup.set_oidc_token_response("some-code", token) @@ -337,7 +338,7 @@ async def test_verify_error( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token(groups=["admin"]) + token = await create_upstream_oidc_token(groups=["admin"]) assert config.oidc issuer = config.oidc.issuer config_url = urljoin(issuer, "/.well-known/openid-configuration") @@ -367,7 +368,7 @@ async def test_invalid_username( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token( + token = await create_upstream_oidc_token( groups=["admin"], sub="invalid@user", uid="invalid@user" ) await setup.set_oidc_token_response("some-code", token) @@ -392,7 +393,7 @@ async def test_invalid_group_syntax( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token( + token = await create_upstream_oidc_token( isMemberOf=[{"name": "foo", "id": ["bar"]}] ) await setup.set_oidc_token_response("some-code", token) @@ -417,7 +418,7 @@ async def test_invalid_groups( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token( + token = await create_upstream_oidc_token( isMemberOf=[ {"name": "foo"}, {"group": "bar", "id": 4567}, @@ -454,7 +455,7 @@ async def test_no_valid_groups( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token(groups=[]) + token = await create_upstream_oidc_token(groups=[]) await setup.set_oidc_token_response("some-code", token) await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo?a=bar&b=baz" @@ -484,7 +485,7 @@ async def test_unicode_name( tmp_path: Path, client: AsyncClient, setup: SetupTest ) -> None: config = await configure(tmp_path, "oidc") - token = await setup.create_upstream_oidc_token(name="名字", groups=["admin"]) + token = await create_upstream_oidc_token(name="名字", groups=["admin"]) await setup.set_oidc_token_response("some-code", token) await setup.set_oidc_configuration_response(config.issuer.keypair) return_url = "https://example.com/foo" diff --git a/tests/support/setup.py b/tests/support/setup.py index 506730e4e..c6ab02cbb 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -24,17 +24,16 @@ mock_oidc_provider_config, mock_oidc_provider_token, ) -from tests.support.tokens import create_upstream_oidc_token if TYPE_CHECKING: from pathlib import Path - from typing import Any, AsyncIterator, List, Optional + from typing import AsyncIterator, List, Optional from aioredis import Redis from gafaelfawr.config import Config from gafaelfawr.keypair import RSAKeyPair - from gafaelfawr.models.oidc import OIDCToken, OIDCVerifiedToken + from gafaelfawr.models.oidc import OIDCToken from gafaelfawr.providers.github import GitHubUserInfo @@ -192,36 +191,6 @@ async def create_session_token( await self.session.commit() return data - async def create_upstream_oidc_token( - self, - *, - kid: Optional[str] = None, - groups: Optional[List[str]] = None, - **claims: Any, - ) -> OIDCVerifiedToken: - """Create a signed OpenID Connect token. - - Parameters - ---------- - kid : `str`, optional - Key ID for the token header. Defaults to the first key in the - key_ids configuration for the OpenID Connect provider. - groups : List[`str`], optional - Group memberships the generated token should have. - **claims : `str`, optional - Other claims to set or override in the token. - - Returns - ------- - token : `gafaelfawr.models..oidc.OIDCVerifiedToken` - The generated token. - """ - config = await config_dependency() - if not kid: - assert config.oidc - kid = config.oidc.key_ids[0] - return create_upstream_oidc_token(config, kid, groups=groups, **claims) - async def login(self, client: AsyncClient, token: Token) -> str: """Create a valid Gafaelfawr session. diff --git a/tests/support/tokens.py b/tests/support/tokens.py index c49b9e9e3..0ac3f7408 100644 --- a/tests/support/tokens.py +++ b/tests/support/tokens.py @@ -8,6 +8,7 @@ import jwt from gafaelfawr.constants import ALGORITHM +from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.models.history import TokenChange, TokenChangeHistoryEntry from gafaelfawr.models.oidc import OIDCVerifiedToken from gafaelfawr.models.token import Token, TokenData, TokenType, TokenUserInfo @@ -148,10 +149,9 @@ def create_test_token( ) -def create_upstream_oidc_token( - config: Config, - kid: str, +async def create_upstream_oidc_token( *, + kid: Optional[str] = None, groups: Optional[List[str]] = None, **claims: Any, ) -> OIDCVerifiedToken: @@ -164,7 +164,7 @@ def create_upstream_oidc_token( ---------- config : `gafaelfawr.config.Config` The configuration. - kid : `str` + kid : `str`, optional Key ID for the token header. groups : List[`str`], optional Group memberships the generated token should have. @@ -176,7 +176,10 @@ def create_upstream_oidc_token( token : `gafaelfawr.tokens.VerifiedToken` The new token. """ + config = await config_dependency() assert config.oidc + if not kid: + kid = config.oidc.key_ids[0] payload = { "aud": config.oidc.audience, "iss": config.oidc.issuer, diff --git a/tests/verify_test.py b/tests/verify_test.py index 04643ab1b..a7811756f 100644 --- a/tests/verify_test.py +++ b/tests/verify_test.py @@ -20,6 +20,7 @@ from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken from tests.support.settings import configure +from tests.support.tokens import create_upstream_oidc_token if TYPE_CHECKING: from pathlib import Path @@ -146,7 +147,7 @@ async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: setup.respx_mock.get(oidc_url).respond(404) # Check token verification with this configuration. - token = await setup.create_upstream_oidc_token(kid="some-kid") + token = await create_upstream_oidc_token(kid="some-kid") assert await verifier.verify_oidc_token(token) # Wrong algorithm for the key. @@ -166,7 +167,7 @@ async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: # Try with a new key ID and return a malformed reponse. setup.respx_mock.get(jwks_url).respond(json=["foo"]) - token = await setup.create_upstream_oidc_token(kid="malformed") + token = await create_upstream_oidc_token(kid="malformed") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) @@ -180,7 +181,7 @@ async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: jwks.keys[1].kid = "another-kid" setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) setup.respx_mock.get(oidc_url).respond(json=["foo"]) - token = await setup.create_upstream_oidc_token(kid="another-kid") + token = await create_upstream_oidc_token(kid="another-kid") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) From b9a29b0348c270e030ba5474908b7b0edf3c17f4 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 17:29:53 -0800 Subject: [PATCH 13/24] Remove wrapper around mock_github There's now no need to call this via SetupTest. Add the respx_mock mocks directly to the relevant tests and have them call the underlying function directly. --- tests/handlers/login_github_test.py | 77 ++++++++++++++++++----------- tests/handlers/logout_test.py | 8 +-- tests/support/setup.py | 32 ------------ 3 files changed, 53 insertions(+), 64 deletions(-) diff --git a/tests/handlers/login_github_test.py b/tests/handlers/login_github_test.py index 1dc335b93..2ba23bf39 100644 --- a/tests/handlers/login_github_test.py +++ b/tests/handlers/login_github_test.py @@ -14,11 +14,13 @@ GitHubTeam, GitHubUserInfo, ) +from tests.support.github import mock_github from tests.support.logging import parse_log if TYPE_CHECKING: from typing import Dict, Optional + import respx from _pytest.logging import LogCaptureFixture from httpx import AsyncClient, Response @@ -27,7 +29,7 @@ async def simulate_github_login( client: AsyncClient, - setup: SetupTest, + respx_mock: respx.Router, user_info: GitHubUserInfo, headers: Optional[Dict[str, str]] = None, return_url: str = "https://example.com/", @@ -42,8 +44,10 @@ async def simulate_github_login( Parameters ---------- - setup : `tests.support.setup.SetupTest` - The Gafaelfawr test setup. + client : `httpx.AsyncClient` + Client to use to make calls to the application. + respx_mock : `respx.Router` + Mock for httpx calls. user_info : `gafaelfawr.providers.github.GitHubUserInfo` The user information that GitHub should return. headers : Dict[`str`, `str`], optional @@ -67,7 +71,8 @@ async def simulate_github_login( assert config.github if not headers: headers = {} - await setup.set_github_response( + await mock_github( + respx_mock, "some-code", user_info, paginate_teams=paginate_teams, @@ -100,7 +105,7 @@ async def simulate_github_login( @pytest.mark.asyncio async def test_login( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, respx_mock: respx.Router, caplog: LogCaptureFixture ) -> None: user_info = GitHubUserInfo( name="GitHub User", @@ -122,7 +127,7 @@ async def test_login( # Simulate the GitHub login. caplog.clear() r = await simulate_github_login( - client, setup, user_info, return_url=return_url + client, respx_mock, user_info, return_url=return_url ) assert r.status_code == 307 assert parse_log(caplog) == [ @@ -186,7 +191,7 @@ async def test_login( @pytest.mark.asyncio async def test_login_redirect_header( - client: AsyncClient, setup: SetupTest + client: AsyncClient, respx_mock: respx.Router ) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" user_info = GitHubUserInfo( @@ -197,7 +202,7 @@ async def test_login_redirect_header( teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) return_url = "https://example.com/foo?a=bar&b=baz" - await setup.set_github_response("some-code", user_info) + await mock_github(respx_mock, "some-code", user_info) # Simulate the initial authentication request. r = await client.get( @@ -216,16 +221,14 @@ async def test_login_redirect_header( @pytest.mark.asyncio -async def test_login_no_destination( - client: AsyncClient, setup: SetupTest -) -> None: +async def test_login_no_destination(client: AsyncClient) -> None: r = await client.get("/login") assert r.status_code == 422 @pytest.mark.asyncio async def test_cookie_auth_with_token( - client: AsyncClient, setup: SetupTest + client: AsyncClient, respx_mock: respx.Router ) -> None: """Test that cookie auth takes precedence over an Authorization header. @@ -246,7 +249,7 @@ async def test_cookie_auth_with_token( # Simulate the GitHub login. r = await simulate_github_login( client, - setup, + respx_mock, user_info, headers={"Authorization": "token some-jupyterhub-token"}, ) @@ -262,7 +265,9 @@ async def test_cookie_auth_with_token( @pytest.mark.asyncio -async def test_bad_redirect(client: AsyncClient, setup: SetupTest) -> None: +async def test_bad_redirect( + client: AsyncClient, respx_mock: respx.Router +) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -284,7 +289,7 @@ async def test_bad_redirect(client: AsyncClient, setup: SetupTest) -> None: # X-Forwarded-Host header, this will be allowed. r = await simulate_github_login( client, - setup, + respx_mock, user_info, headers={ "X-Forwarded-For": "192.168.0.1", @@ -296,7 +301,9 @@ async def test_bad_redirect(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_github_uppercase(client: AsyncClient, setup: SetupTest) -> None: +async def test_github_uppercase( + client: AsyncClient, respx_mock: respx.Router +) -> None: """Tests that usernames and organization names are forced to lowercase. We do not test that slugs are forced to lowercase (and do not change the @@ -311,7 +318,7 @@ async def test_github_uppercase(client: AsyncClient, setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) - r = await simulate_github_login(client, setup, user_info) + r = await simulate_github_login(client, respx_mock, user_info) assert r.status_code == 307 # The user returned by the /auth route should be forced to lowercase. @@ -321,7 +328,9 @@ async def test_github_uppercase(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_github_admin(client: AsyncClient, setup: SetupTest) -> None: +async def test_github_admin( + client: AsyncClient, respx_mock: respx.Router, setup: SetupTest +) -> None: """Test that a token administrator gets the admin:token scope.""" admin_service = setup.factory.create_admin_service() await admin_service.add_admin( @@ -336,7 +345,7 @@ async def test_github_admin(client: AsyncClient, setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")], ) - r = await simulate_github_login(client, setup, user_info) + r = await simulate_github_login(client, respx_mock, user_info) assert r.status_code == 307 # The user should have admin:token scope. @@ -345,7 +354,9 @@ async def test_github_admin(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: +async def test_invalid_username( + client: AsyncClient, respx_mock: respx.Router +) -> None: """Test that invalid usernames are rejected.""" user_info = GitHubUserInfo( name="A User", @@ -356,14 +367,16 @@ async def test_invalid_username(client: AsyncClient, setup: SetupTest) -> None: ) r = await simulate_github_login( - client, setup, user_info, expect_revoke=True + client, respx_mock, user_info, expect_revoke=True ) assert r.status_code == 403 assert "Invalid username: invalid user" in r.text @pytest.mark.asyncio -async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: +async def test_invalid_groups( + client: AsyncClient, respx_mock: respx.Router +) -> None: user_info = GitHubUserInfo( name="A User", username="someuser", @@ -376,7 +389,7 @@ async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: ], ) - r = await simulate_github_login(client, setup, user_info) + r = await simulate_github_login(client, respx_mock, user_info) assert r.status_code == 307 # The invalid groups should not appear but the valid group should still be @@ -387,7 +400,9 @@ async def test_invalid_groups(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_paginated_teams(client: AsyncClient, setup: SetupTest) -> None: +async def test_paginated_teams( + client: AsyncClient, respx_mock: respx.Router +) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -406,7 +421,7 @@ async def test_paginated_teams(client: AsyncClient, setup: SetupTest) -> None: ) r = await simulate_github_login( - client, setup, user_info, paginate_teams=True + client, respx_mock, user_info, paginate_teams=True ) assert r.status_code == 307 @@ -425,7 +440,9 @@ async def test_paginated_teams(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_valid_groups( + client: AsyncClient, respx_mock: respx.Router +) -> None: user_info = GitHubUserInfo( name="GitHub User", username="githubuser", @@ -435,7 +452,7 @@ async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: ) r = await simulate_github_login( - client, setup, user_info, expect_revoke=True + client, respx_mock, user_info, expect_revoke=True ) assert r.status_code == 403 assert r.headers["Cache-Control"] == "no-cache, must-revalidate" @@ -448,7 +465,9 @@ async def test_no_valid_groups(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_unicode_name(client: AsyncClient, setup: SetupTest) -> None: +async def test_unicode_name( + client: AsyncClient, respx_mock: respx.Router +) -> None: user_info = GitHubUserInfo( name="名字", username="githubuser", @@ -457,7 +476,7 @@ async def test_unicode_name(client: AsyncClient, setup: SetupTest) -> None: teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")], ) - r = await simulate_github_login(client, setup, user_info) + r = await simulate_github_login(client, respx_mock, user_info) assert r.status_code == 307 # Check that the name as returned from the user-info API is correct. diff --git a/tests/handlers/logout_test.py b/tests/handlers/logout_test.py index 2dc30af03..9646e327d 100644 --- a/tests/handlers/logout_test.py +++ b/tests/handlers/logout_test.py @@ -7,10 +7,12 @@ import pytest from gafaelfawr.providers.github import GitHubTeam, GitHubUserInfo +from tests.support.github import mock_github from tests.support.headers import query_from_url from tests.support.logging import parse_log if TYPE_CHECKING: + import respx from _pytest.logging import LogCaptureFixture from httpx import AsyncClient @@ -95,7 +97,7 @@ async def test_logout_not_logged_in( @pytest.mark.asyncio -async def test_logout_bad_url(client: AsyncClient, setup: SetupTest) -> None: +async def test_logout_bad_url(client: AsyncClient) -> None: r = await client.get("/logout", params={"rd": "https://foo.example.com/"}) assert r.status_code == 422 assert r.json() == { @@ -113,7 +115,7 @@ async def test_logout_bad_url(client: AsyncClient, setup: SetupTest) -> None: async def test_logout_github( client: AsyncClient, config: Config, - setup: SetupTest, + respx_mock: respx.Router, caplog: LogCaptureFixture, ) -> None: user_info = GitHubUserInfo( @@ -127,7 +129,7 @@ async def test_logout_github( ) # Log in and log out. - await setup.set_github_response("some-code", user_info, expect_revoke=True) + await mock_github(respx_mock, "some-code", user_info, expect_revoke=True) r = await client.get("/login", params={"rd": "https://example.com"}) assert r.status_code == 307 query = query_from_url(r.headers["Location"]) diff --git a/tests/support/setup.py b/tests/support/setup.py index c6ab02cbb..c3cf071fd 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -19,7 +19,6 @@ from gafaelfawr.models.state import State from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo from tests.support.constants import TEST_HOSTNAME -from tests.support.github import mock_github from tests.support.oidc import ( mock_oidc_provider_config, mock_oidc_provider_token, @@ -34,7 +33,6 @@ from gafaelfawr.config import Config from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken - from gafaelfawr.providers.github import GitHubUserInfo class SetupTest: @@ -225,36 +223,6 @@ def logout(self, client: AsyncClient) -> None: """ del client.cookies[COOKIE_NAME] - async def set_github_response( - self, - code: str, - user_info: GitHubUserInfo, - *, - paginate_teams: bool = False, - expect_revoke: bool = False, - ) -> None: - """Mock the GitHub API. - - Parameters - ---------- - code : `str` - The code that Gafaelfawr must send to redeem a token. - user_info : `gafaelfawr.providers.github.GitHubUserInfo` - User information to use to synthesize GitHub API responses. - paginate_teams : `bool`, optional - Whether to paginate the team results. Default: `False` - expect_revoke : `bool`, optional - Whether to expect a revocation of the token after returning all - user information. Default: `False` - """ - await mock_github( - self.respx_mock, - code, - user_info, - paginate_teams=paginate_teams, - expect_revoke=expect_revoke, - ) - async def set_oidc_configuration_response( self, keypair: Optional[RSAKeyPair] = None, From badb889aea4001cf1842039f98da2bfe53343c73 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 19 Nov 2021 18:05:27 -0800 Subject: [PATCH 14/24] Remove wrappers around mock_oidc_* calls These can now be called directly instead of via SetupTest. --- tests/conftest.py | 5 +- tests/handlers/login_oidc_test.py | 87 ++++++++++++++++--------------- tests/support/setup.py | 50 +----------------- tests/verify_test.py | 42 +++++++++------ 4 files changed, 72 insertions(+), 112 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 369a6713f..ed106fa9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ import kubernetes import pytest -import respx from asgi_lifespan import LifespanManager from httpx import AsyncClient @@ -158,7 +157,7 @@ async def selenium_config( @pytest.fixture async def setup( - tmp_path: Path, empty_database: None, respx_mock: respx.Router + tmp_path: Path, empty_database: None ) -> AsyncIterator[SetupTest]: """Create a test setup object. @@ -172,5 +171,5 @@ async def setup( setup : `tests.support.setup.SetupTest` The setup object. """ - async with SetupTest.create(tmp_path, respx_mock) as setup: + async with SetupTest.create(tmp_path) as setup: yield setup diff --git a/tests/handlers/login_oidc_test.py b/tests/handlers/login_oidc_test.py index c4ff1f036..8f828f76f 100644 --- a/tests/handlers/login_oidc_test.py +++ b/tests/handlers/login_oidc_test.py @@ -10,31 +10,34 @@ from httpx import ConnectError from tests.support.logging import parse_log +from tests.support.oidc import ( + mock_oidc_provider_config, + mock_oidc_provider_token, +) from tests.support.settings import configure from tests.support.tokens import create_upstream_oidc_token if TYPE_CHECKING: from pathlib import Path + import respx from _pytest.logging import LogCaptureFixture from httpx import AsyncClient - from tests.support.setup import SetupTest - @pytest.mark.asyncio async def test_login( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + respx_mock: respx.Router, caplog: LogCaptureFixture, ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token( groups=["admin"], name="Some Person", email="person@example.com" ) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) assert config.oidc return_url = "https://example.com:4444/foo?a=bar&b=baz" @@ -117,13 +120,13 @@ async def test_login( @pytest.mark.asyncio async def test_login_redirect_header( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: """Test receiving the redirect header via X-Auth-Request-Redirect.""" config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token(groups=["admin"]) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo?a=bar&b=baz" r = await client.get( @@ -143,13 +146,13 @@ async def test_login_redirect_header( @pytest.mark.asyncio async def test_oauth2_callback( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: """Test the compatibility /oauth2/callback route.""" config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token(groups=["admin"]) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) assert config.oidc return_url = "https://example.com/foo" @@ -170,7 +173,7 @@ async def test_oauth2_callback( @pytest.mark.asyncio async def test_claim_names( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: """Uses an alternate settings environment with non-default claims.""" config = await configure( @@ -180,8 +183,8 @@ async def test_claim_names( token = await create_upstream_oidc_token( groups=["admin"], username="alt-username", numeric_uid=7890 ) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -210,7 +213,7 @@ async def test_claim_names( async def test_callback_error( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + respx_mock: respx.Router, caplog: LogCaptureFixture, ) -> None: """Test an error return from the OIDC token endpoint.""" @@ -229,7 +232,7 @@ async def test_callback_error( "error": "error_code", "error_description": "description", } - setup.respx_mock.post(config.oidc.token_url).respond(400, json=response) + respx_mock.post(config.oidc.token_url).respond(400, json=response) # Simulate the return from the OpenID Connect provider. caplog.clear() @@ -262,9 +265,7 @@ async def test_callback_error( # Change the mock error response to not contain an error. We should then # internally raise the exception for the return status, which should # translate into an internal server error. - setup.respx_mock.post(config.oidc.token_url).respond( - 400, json={"foo": "bar"} - ) + respx_mock.post(config.oidc.token_url).respond(400, json={"foo": "bar"}) r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -276,7 +277,7 @@ async def test_callback_error( # Now try a reply that returns 200 but doesn't have the field we # need. - setup.respx_mock.post(config.oidc.token_url).respond(json={"foo": "bar"}) + respx_mock.post(config.oidc.token_url).respond(json={"foo": "bar"}) r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -287,7 +288,7 @@ async def test_callback_error( assert "No id_token in token reply" in r.text # Return invalid JSON, which should raise an error during JSON decoding. - setup.respx_mock.post(config.oidc.token_url).respond(content=b"foo") + respx_mock.post(config.oidc.token_url).respond(content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -298,7 +299,7 @@ async def test_callback_error( assert "not valid JSON" in r.text # Finally, return invalid JSON and an error reply. - setup.respx_mock.post(config.oidc.token_url).respond(400, content=b"foo") + respx_mock.post(config.oidc.token_url).respond(400, content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( @@ -311,7 +312,7 @@ async def test_callback_error( @pytest.mark.asyncio async def test_connection_error( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") assert config.oidc @@ -325,7 +326,7 @@ async def test_connection_error( # Register a connection error for the callback request to the OIDC # provider and check that an appropriate error is shown to the user. token_url = config.oidc.token_url - setup.respx_mock.post(token_url).mock(side_effect=ConnectError) + respx_mock.post(token_url).mock(side_effect=ConnectError) r = await client.get( "/login", params={"code": "some-code", "state": query["state"][0]} ) @@ -335,7 +336,7 @@ async def test_connection_error( @pytest.mark.asyncio async def test_verify_error( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token(groups=["admin"]) @@ -343,9 +344,9 @@ async def test_verify_error( issuer = config.oidc.issuer config_url = urljoin(issuer, "/.well-known/openid-configuration") jwks_url = urljoin(issuer, "/.well-known/jwks.json") - setup.respx_mock.get(config_url).respond(404) - setup.respx_mock.get(jwks_url).respond(404) - await setup.set_oidc_token_response("some-code", token) + respx_mock.get(config_url).respond(404) + respx_mock.get(jwks_url).respond(404) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -365,14 +366,14 @@ async def test_verify_error( @pytest.mark.asyncio async def test_invalid_username( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token( groups=["admin"], sub="invalid@user", uid="invalid@user" ) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -390,14 +391,14 @@ async def test_invalid_username( @pytest.mark.asyncio async def test_invalid_group_syntax( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token( isMemberOf=[{"name": "foo", "id": ["bar"]}] ) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -415,7 +416,7 @@ async def test_invalid_group_syntax( @pytest.mark.asyncio async def test_invalid_groups( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token( @@ -429,8 +430,8 @@ async def test_invalid_groups( {"name": "21341", "id": 41233}, ] ) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) @@ -452,12 +453,12 @@ async def test_invalid_groups( @pytest.mark.asyncio async def test_no_valid_groups( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token(groups=[]) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo?a=bar&b=baz" r = await client.get("/login", params={"rd": return_url}) @@ -482,12 +483,12 @@ async def test_no_valid_groups( @pytest.mark.asyncio async def test_unicode_name( - tmp_path: Path, client: AsyncClient, setup: SetupTest + tmp_path: Path, client: AsyncClient, respx_mock: respx.Router ) -> None: config = await configure(tmp_path, "oidc") token = await create_upstream_oidc_token(name="名字", groups=["admin"]) - await setup.set_oidc_token_response("some-code", token) - await setup.set_oidc_configuration_response(config.issuer.keypair) + await mock_oidc_provider_config(respx_mock, config.issuer.keypair) + await mock_oidc_provider_token(respx_mock, "some-code", token) return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) diff --git a/tests/support/setup.py b/tests/support/setup.py index c3cf071fd..af4826c41 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -5,7 +5,6 @@ from contextlib import asynccontextmanager from typing import TYPE_CHECKING -import respx import structlog from httpx import AsyncClient from safir.dependencies.http_client import http_client_dependency @@ -19,10 +18,6 @@ from gafaelfawr.models.state import State from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo from tests.support.constants import TEST_HOSTNAME -from tests.support.oidc import ( - mock_oidc_provider_config, - mock_oidc_provider_token, -) if TYPE_CHECKING: from pathlib import Path @@ -31,8 +26,6 @@ from aioredis import Redis from gafaelfawr.config import Config - from gafaelfawr.keypair import RSAKeyPair - from gafaelfawr.models.oidc import OIDCToken class SetupTest: @@ -52,9 +45,7 @@ class SetupTest: @classmethod @asynccontextmanager - async def create( - cls, tmp_path: Path, respx_mock: respx.Router - ) -> AsyncIterator[SetupTest]: + async def create(cls, tmp_path: Path) -> AsyncIterator[SetupTest]: """Create a new `SetupTest` instance. This is the only supported way to set up the test environment and @@ -66,8 +57,6 @@ async def create( ---------- tmp_path : `pathlib.Path` The path for temporary files. - respx_mock : `respx.Router` - The mock for simulating `httpx.AsyncClient` calls. """ config = await config_dependency() redis = await redis_dependency(config) @@ -92,7 +81,6 @@ async def create( async with session_factory() as session: yield cls( tmp_path=tmp_path, - respx_mock=respx_mock, config=config, redis=redis, session=session, @@ -107,14 +95,12 @@ def __init__( self, *, tmp_path: Path, - respx_mock: respx.Router, config: Config, redis: Redis, session: AsyncSession, http_client: AsyncClient, ) -> None: self.tmp_path = tmp_path - self.respx_mock = respx_mock self.redis = redis self.session = session self.http_client = http_client @@ -222,37 +208,3 @@ def logout(self, client: AsyncClient) -> None: The client from which to remove the session cookie. """ del client.cookies[COOKIE_NAME] - - async def set_oidc_configuration_response( - self, - keypair: Optional[RSAKeyPair] = None, - kid: Optional[str] = None, - ) -> None: - """Register configuration callbacks for upstream OpenID Connect. - - Parameters - ---------- - keypair : `gafaelfawr.keypair.RSAKeyPair`, optional - The key pair used to sign the token, which will be used to - register the keys callback. - kid : `str`, optional - Key ID for the key. If not given, defaults to the first key ID in - the configured key_ids list. - """ - await mock_oidc_provider_config(self.respx_mock, keypair, kid) - - async def set_oidc_token_response( - self, - code: str, - token: OIDCToken, - ) -> None: - """Register token callbacks for upstream OpenID Connect provider. - - Parameters - ---------- - code : `str` - The code that Gafaelfawr must send. - token : `gafaelfawr.tokens.Token` - The token. - """ - await mock_oidc_provider_token(self.respx_mock, code, token) diff --git a/tests/verify_test.py b/tests/verify_test.py index a7811756f..5064b854b 100644 --- a/tests/verify_test.py +++ b/tests/verify_test.py @@ -19,6 +19,7 @@ ) from gafaelfawr.keypair import RSAKeyPair from gafaelfawr.models.oidc import OIDCToken +from tests.support.oidc import mock_oidc_provider_config from tests.support.settings import configure from tests.support.tokens import create_upstream_oidc_token @@ -26,6 +27,7 @@ from pathlib import Path from typing import Any, Dict, Optional + import respx from _pytest._code import ExceptionInfo from tests.support.setup import SetupTest @@ -48,7 +50,9 @@ def encode_token( @pytest.mark.asyncio -async def test_verify_oidc(tmp_path: Path, setup: SetupTest) -> None: +async def test_verify_oidc( + tmp_path: Path, respx_mock: respx.Router, setup: SetupTest +) -> None: config = await configure(tmp_path, "oidc") verifier = setup.factory.create_token_verifier() @@ -90,7 +94,7 @@ async def test_verify_oidc(tmp_path: Path, setup: SetupTest) -> None: assert str(excinfo.value) == expected # Missing username claim. - await setup.set_oidc_configuration_response(keypair) + await mock_oidc_provider_config(respx_mock, keypair) kid = config.verifier.oidc_kids[0] token = encode_token(payload, config.issuer.keypair, kid=kid) with pytest.raises(MissingClaimsException) as excinfo: @@ -99,7 +103,7 @@ async def test_verify_oidc(tmp_path: Path, setup: SetupTest) -> None: assert str(excinfo.value) == expected # Missing UID claim. - await setup.set_oidc_configuration_response(keypair) + await mock_oidc_provider_config(respx_mock, keypair) payload[config.verifier.username_claim] = "some-user" token = encode_token(payload, config.issuer.keypair, kid=kid) with pytest.raises(MissingClaimsException) as excinfo: @@ -109,10 +113,13 @@ async def test_verify_oidc(tmp_path: Path, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_verify_oidc_no_kids(tmp_path: Path, setup: SetupTest) -> None: +async def test_verify_oidc_no_kids( + tmp_path: Path, respx_mock: respx.Router, setup: SetupTest +) -> None: config = await configure(tmp_path, "oidc-no-kids") + keypair = config.issuer.keypair verifier = setup.factory.create_token_verifier() - await setup.set_oidc_configuration_response(config.issuer.keypair, "kid") + await mock_oidc_provider_config(respx_mock, keypair, "kid") now = datetime.now(timezone.utc) exp = now + timedelta(days=24) @@ -122,7 +129,6 @@ async def test_verify_oidc_no_kids(tmp_path: Path, setup: SetupTest) -> None: "iss": config.verifier.oidc_iss, "exp": int(exp.timestamp()), } - keypair = config.issuer.keypair token = encode_token(payload, keypair, kid="a-kid") with pytest.raises(UnknownKeyIdException) as excinfo: await verifier.verify_oidc_token(token) @@ -131,7 +137,9 @@ async def test_verify_oidc_no_kids(tmp_path: Path, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: +async def test_key_retrieval( + tmp_path: Path, respx_mock: respx.Router, setup: SetupTest +) -> None: config = await configure(tmp_path, "oidc-no-kids") assert config.oidc verifier = setup.factory.create_token_verifier() @@ -143,8 +151,8 @@ async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: # a connection refused from the OpenID Connect endpoint. jwks_url = urljoin(config.oidc.issuer, "/.well-known/jwks.json") oidc_url = urljoin(config.oidc.issuer, "/.well-known/openid-configuration") - setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) - setup.respx_mock.get(oidc_url).respond(404) + respx_mock.get(jwks_url).respond(json=jwks.dict()) + respx_mock.get(oidc_url).respond(404) # Check token verification with this configuration. token = await create_upstream_oidc_token(kid="some-kid") @@ -152,39 +160,39 @@ async def test_key_retrieval(tmp_path: Path, setup: SetupTest) -> None: # Wrong algorithm for the key. jwks.keys[0].alg = "ES256" - setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) + respx_mock.get(jwks_url).respond(json=jwks.dict()) with pytest.raises(UnknownAlgorithmException): await verifier.verify_oidc_token(token) # Should go back to working if we fix the algorithm and add more keys. # Add an explicit 404 from the OpenID connect endpoint. - setup.respx_mock.get(oidc_url).respond(404) + respx_mock.get(oidc_url).respond(404) jwks.keys[0].alg = ALGORITHM keypair = RSAKeyPair.generate() jwks.keys.insert(0, keypair.public_key_as_jwks("a-kid").keys[0]) - setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) + respx_mock.get(jwks_url).respond(json=jwks.dict()) assert await verifier.verify_oidc_token(token) # Try with a new key ID and return a malformed reponse. - setup.respx_mock.get(jwks_url).respond(json=["foo"]) + respx_mock.get(jwks_url).respond(json=["foo"]) token = await create_upstream_oidc_token(kid="malformed") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) # Return a 404 error. - setup.respx_mock.get(jwks_url).respond(404) + respx_mock.get(jwks_url).respond(404) with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) # Fix the JWKS handler but register a malformed URL as the OpenID Connect # configuration endpoint, which should be checked first. jwks.keys[1].kid = "another-kid" - setup.respx_mock.get(jwks_url).respond(json=jwks.dict()) - setup.respx_mock.get(oidc_url).respond(json=["foo"]) + respx_mock.get(jwks_url).respond(json=jwks.dict()) + respx_mock.get(oidc_url).respond(json=["foo"]) token = await create_upstream_oidc_token(kid="another-kid") with pytest.raises(FetchKeysException): await verifier.verify_oidc_token(token) # Try again with a working OpenID Connect configuration. - setup.respx_mock.get(oidc_url).respond(json={"jwks_uri": jwks_url}) + respx_mock.get(oidc_url).respond(json={"jwks_uri": jwks_url}) assert await verifier.verify_oidc_token(token) From 087bfee7c9396c626324c3fd1f4ff2dbc2af6d4d Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Mon, 29 Nov 2021 16:53:22 -0800 Subject: [PATCH 15/24] Refactor token caching Rather than making caching independent of creating new internal and notebook tokens, have the cache manage the token creation and create new tokens under a per-user asyncio lock. This will ensure that token creation is done only once and then cached and all subsequent requests are served from that cache. In the previous logic, all requests would fall through the cache, and then all requests would have to query the database to reconstruct the token, which in turn relies heavily on database synchronization to ensure multiple tokens are not generated. --- src/gafaelfawr/factory.py | 18 +- src/gafaelfawr/services/token.py | 162 +-------------- src/gafaelfawr/token_cache.py | 327 ++++++++++++++++++++++++++++--- tests/services/token_test.py | 8 +- tests/token_cache_test.py | 64 +++--- 5 files changed, 364 insertions(+), 215 deletions(-) diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index facf4f22b..c4c965cd2 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -206,7 +206,15 @@ def create_token_cache(self) -> TokenCache: key = self._config.session_secret storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) - return TokenCache(token_redis_store) + token_db_store = TokenDatabaseStore(self.session) + token_change_store = TokenChangeHistoryStore(self.session) + return TokenCache( + config=self._config, + token_db_store=token_db_store, + token_redis_store=token_redis_store, + token_change_store=token_change_store, + logger=self._logger, + ) def create_token_issuer(self) -> TokenIssuer: """Create a TokenIssuer. @@ -230,8 +238,14 @@ def create_token_service(self) -> TokenService: key = self._config.session_secret storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) - token_cache = TokenCache(token_redis_store) token_change_store = TokenChangeHistoryStore(self.session) + token_cache = TokenCache( + config=self._config, + token_db_store=token_db_store, + token_redis_store=token_redis_store, + token_change_store=token_change_store, + logger=self._logger, + ) return TokenService( config=self._config, token_cache=token_cache, diff --git a/src/gafaelfawr/services/token.py b/src/gafaelfawr/services/token.py index 734d29b87..ca8fa6292 100644 --- a/src/gafaelfawr/services/token.py +++ b/src/gafaelfawr/services/token.py @@ -5,7 +5,7 @@ import ipaddress import re import time -from datetime import datetime, timedelta +from datetime import datetime from typing import TYPE_CHECKING from gafaelfawr.constants import MINIMUM_LIFETIME, USERNAME_REGEX @@ -461,11 +461,6 @@ async def get_internal_token( ) -> Token: """Get or create a new internal token. - The new token will have the same expiration time as the existing token - on which it's based unless that expiration time is longer than the - expiration time of normal interactive tokens, in which case it will be - capped at the interactive token expiration time. - Parameters ---------- token_data : `gafaelfawr.models.token.TokenData` @@ -490,87 +485,15 @@ async def get_internal_token( self._validate_scopes(scopes, token_data) self._validate_username(token_data.username) scopes = sorted(scopes) - - # See if there is a cached token. - token = await self._token_cache.get_internal_token( - token_data, service, scopes - ) - if token: - return token - - # See if there's already a matching internal token. - key = await self._token_db_store.get_internal_token_key( - token_data, service, scopes, self._minimum_expiration(token_data) - ) - if key: - data = await self._token_redis_store.get_data_by_key(key) - if data: - self._token_cache.store_internal_token( - data.token, token_data, service, scopes - ) - return data.token - - # There is not, so we need to create a new one. - token = Token() - created = current_datetime() - expires = created + self._config.token_lifetime - if token_data.expires and token_data.expires < expires: - expires = token_data.expires - data = TokenData( - token=token, - username=token_data.username, - token_type=TokenType.internal, - scopes=scopes, - created=created, - expires=expires, - name=token_data.name, - email=token_data.email, - uid=token_data.uid, - groups=token_data.groups, - ) - history_entry = TokenChangeHistoryEntry( - token=token.key, - username=data.username, - token_type=TokenType.internal, - parent=token_data.token.key, - scopes=scopes, - service=service, - expires=expires, - actor=token_data.username, - action=TokenChange.create, - ip_address=ip_address, - event_time=created, - ) - - await self._token_redis_store.store_data(data) - await self._token_db_store.add( - data, service=service, parent=token_data.token.key + return await self._token_cache.get_internal_token( + token_data, service, scopes, ip_address ) - await self._token_change_store.add(history_entry) - - self._logger.info( - "Created new internal token", - key=token.key, - service=service, - token_scope=",".join(data.scopes), - ) - - # Cache the token and return it. - self._token_cache.store_internal_token( - token, token_data, service, scopes - ) - return token async def get_notebook_token( self, token_data: TokenData, ip_address: str ) -> Token: """Get or create a new notebook token. - The new token will have the same expiration time as the existing token - on which it's based unless that expiration time is longer than the - expiration time of normal interactive tokens, in which case it will be - capped at the interactive token expiration time. - Parameters ---------- token_data : `gafaelfawr.models.token.TokenData` @@ -589,62 +512,10 @@ async def get_notebook_token( If the username is invalid. """ self._validate_username(token_data.username) - - # See if there is a cached token. - token = await self._token_cache.get_notebook_token(token_data) - if token: - return token - - # See if there's already a matching notebook token. - key = await self._token_db_store.get_notebook_token_key( - token_data, self._minimum_expiration(token_data) - ) - if key: - data = await self._token_redis_store.get_data_by_key(key) - if data: - self._token_cache.store_notebook_token(data.token, token_data) - return data.token - - # There is not, so we need to create a new one. - token = Token() - created = current_datetime() - expires = created + self._config.token_lifetime - if token_data.expires and token_data.expires < expires: - expires = token_data.expires - data = TokenData( - token=token, - username=token_data.username, - token_type=TokenType.notebook, - scopes=token_data.scopes, - created=created, - expires=expires, - name=token_data.name, - email=token_data.email, - uid=token_data.uid, - groups=token_data.groups, - ) - history_entry = TokenChangeHistoryEntry( - token=token.key, - username=data.username, - token_type=TokenType.notebook, - parent=token_data.token.key, - scopes=data.scopes, - expires=expires, - actor=token_data.username, - action=TokenChange.create, - ip_address=ip_address, - event_time=created, + return await self._token_cache.get_notebook_token( + token_data, ip_address ) - await self._token_redis_store.store_data(data) - await self._token_db_store.add(data, parent=token_data.token.key) - await self._token_change_store.add(history_entry) - - # Cache the token and return it. - self._logger.info("Created new notebook token", key=token.key) - self._token_cache.store_notebook_token(token, token_data) - return token - async def get_token_info( self, key: str, auth_data: TokenData, username: Optional[str] ) -> Optional[TokenInfo]: @@ -946,29 +817,6 @@ async def _delete_one_token( self._logger.info("Deleted token", key=key, username=info.username) return success - def _minimum_expiration(self, token_data: TokenData) -> datetime: - """Determine the minimum expiration for a child token. - - Parameters - ---------- - token_data : `gafaelfawr.models.token.TokenData` - The data for the parent token for which a child token was - requested. - - Returns - ------- - min_expires : `datetime.datetime` - The minimum acceptable expiration time for the child token. If - no child tokens with at least this expiration time exist, a new - child token should be created. - """ - min_expires = current_datetime() + timedelta( - seconds=self._config.token_lifetime.total_seconds() / 2 - ) - if token_data.expires and min_expires > token_data.expires: - min_expires = token_data.expires - return min_expires - async def _modify_expires( self, key: str, diff --git a/src/gafaelfawr/token_cache.py b/src/gafaelfawr/token_cache.py index 80920c7f3..1a872f4b3 100644 --- a/src/gafaelfawr/token_cache.py +++ b/src/gafaelfawr/token_cache.py @@ -2,18 +2,25 @@ from __future__ import annotations +import asyncio +from datetime import datetime, timedelta from typing import TYPE_CHECKING from cachetools import LRUCache from gafaelfawr.constants import TOKEN_CACHE_SIZE +from gafaelfawr.models.history import TokenChange, TokenChangeHistoryEntry +from gafaelfawr.models.token import Token, TokenData, TokenType from gafaelfawr.util import current_datetime if TYPE_CHECKING: - from typing import List, Optional, Tuple + from typing import Dict, List, Optional, Tuple - from gafaelfawr.models.token import Token, TokenData - from gafaelfawr.storage.token import TokenRedisStore + from structlog.stdlib import BoundLogger + + from gafaelfawr.config import Config + from gafaelfawr.storage.history import TokenChangeHistoryStore + from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore __all__ = ["TokenCache"] @@ -35,10 +42,10 @@ class TokenCache: Notes ----- - The cache storage is process-global. It isn't protected by a lock and - thus isn't thread-safe. The expectation is that this code will be used by - a single-process asyncio server, and scaling will be done by adding more - processes. + The cache storage is process-global and is locked only for asyncio access, + not for threaded access. It is not thread-safe. The expectation is that + this code will be used by a single-process asyncio server, and scaling + will be done by adding more processes. Notebook tokens are cached under the key of the parent token and its expiration. Internal tokens add the service name and the requested @@ -51,20 +58,54 @@ class TokenCache: _cache: LRUCache[Tuple[str, ...], Token] = LRUCache(TOKEN_CACHE_SIZE) """Shared cache storage for the tokens, global to each process.""" - def __init__(self, store: TokenRedisStore) -> None: - self._store = store + _cache_lock = asyncio.Lock() + """Lock around the per-user cache locks.""" + + _cache_user_lock: Dict[str, asyncio.Lock] = {} + """Per-user locks to wait for token issuance for a particular user.""" + + def __init__( + self, + *, + config: Config, + token_redis_store: TokenRedisStore, + token_db_store: TokenDatabaseStore, + token_change_store: TokenChangeHistoryStore, + logger: BoundLogger, + ) -> None: + self._config = config + self._token_redis_store = token_redis_store + self._token_db_store = token_db_store + self._token_change_store = token_change_store + self._logger = logger - def clear(self) -> None: + async def clear(self) -> None: """Invalidate the cache. Used primarily for testing. """ - self._cache = LRUCache(TOKEN_CACHE_SIZE) + async with self._cache_lock: + self._cache = LRUCache(TOKEN_CACHE_SIZE) + for user, lock in list(self._cache_user_lock.items()): + async with lock: + del self._cache_user_lock[user] async def get_internal_token( - self, token_data: TokenData, service: str, scopes: List[str] - ) -> Optional[Token]: - """Retrieve a cached internal token. + self, + token_data: TokenData, + service: str, + scopes: List[str], + ip_address: str, + ) -> Token: + """Retrieve or create an internal token. + + Return the cached token if one is available, a matching token if one + exists in the database, or a newly-created token if necessary. + + The new token will have the same expiration time as the existing token + on which it's based unless that expiration time is longer than the + expiration time of normal interactive tokens, in which case it will be + capped at the interactive token expiration time. Parameters ---------- @@ -74,24 +115,51 @@ async def get_internal_token( The service of the internal token. scopes : List[`str`] The scopes the internal token should have. + ip_address : `str` + The IP address from which the request came. Returns ------- - token : `gafaelfawr.models.token.Token` or `None` - The cached token or `None` if no matching token is cached. + token : `gafaelfawr.models.token.Token` + The cached token or newly-created token. """ key = self._internal_key(token_data, service, scopes) - return await self._get_token(key, token_data.scopes) + token = await self._get_token(key, token_data.scopes) + if not token: + lock = await self._acquire_user_lock(token_data.username) + try: + # Check again now that we've taken the lock, since another + # thread of execution may have created and cached a token. + token = await self._get_token(key, token_data.scopes) + if token: + return token + token = await self._create_internal_token( + token_data, service, scopes, ip_address + ) + self._cache[key] = token + finally: + lock.release() + return token async def get_notebook_token( - self, token_data: TokenData - ) -> Optional[Token]: - """Retrieve a cached notebook token. + self, token_data: TokenData, ip_address: str + ) -> Token: + """Retrieve or create a notebook token. + + Return the cached token if one is available, a matching token if one + exists in the database, or a newly-created token if necessary. + + The new token will have the same expiration time as the existing token + on which it's based unless that expiration time is longer than the + expiration time of normal interactive tokens, in which case it will be + capped at the interactive token expiration time. Parameters ---------- token_data : `gafaelfawr.models.token.TokenData` The authentication data for the parent token. + ip_address : `str` + The IP address from which the request came. Returns ------- @@ -99,7 +167,22 @@ async def get_notebook_token( The cached token or `None` if no matching token is cached. """ key = self._notebook_key(token_data) - return await self._get_token(key) + token = await self._get_token(key) + if not token: + lock = await self._acquire_user_lock(token_data.username) + try: + # Check again now that we've taken the lock, since another + # thread of execution may have created and cached a token. + token = await self._get_token(key) + if token: + return token + token = await self._create_notebook_token( + token_data, ip_address + ) + self._cache[key] = token + finally: + lock.release() + return token def store_internal_token( self, @@ -110,6 +193,8 @@ def store_internal_token( ) -> None: """Cache an internal token. + Used primarily for the test suite. + Parameters ---------- token : `gafaelfawr.models.token.Token` @@ -129,6 +214,8 @@ def store_notebook_token( ) -> None: """Cache a notebook token. + Used primarily for the test suite. + Parameters ---------- token : `gafaelfawr.models.token.Token` @@ -139,6 +226,177 @@ def store_notebook_token( key = self._notebook_key(token_data) self._cache[key] = token + async def _acquire_user_lock(self, username: str) -> asyncio.Lock: + """Acquire a per-user cache lock. + + This lock serializes token creation for a specific user so that if a + user without a matching internal or notebook token requests many of + them simultaneously, only one will be created and cached and then + returned by all the other requests. + + Parameters + ---------- + username : `str` + The user for which to acquire a lock. + + Returns + ------- + lock : `asyncio.Lock` + The acquired per-user lock. + """ + async with self._cache_lock: + if username in self._cache_user_lock: + lock = self._cache_user_lock[username] + else: + lock = asyncio.Lock() + self._cache_user_lock[username] = lock + await lock.acquire() + return lock + + async def _create_internal_token( + self, + token_data: TokenData, + service: str, + scopes: List[str], + ip_address: str, + ) -> Token: + """Retrieve or create a new internal token. + + This must be run with the per-user token lock taken so that any other + requests for a token for the same user will wait until this request is + complete. + + Parameters + ---------- + token_data : `gafaelfawr.models.token.TokenData` + The authentication data for the parent token. + service : `str` + The service of the internal token. + scopes : List[`str`] + The scopes the internal token should have. + ip_address : `str` + The IP address from which the request came. + """ + # See if there's already a matching internal token. + key = await self._token_db_store.get_internal_token_key( + token_data, service, scopes, self._minimum_expiration(token_data) + ) + if key: + data = await self._token_redis_store.get_data_by_key(key) + if data: + return data.token + + # There is not, so we need to create a new one. + token = Token() + created = current_datetime() + expires = created + self._config.token_lifetime + if token_data.expires and token_data.expires < expires: + expires = token_data.expires + data = TokenData( + token=token, + username=token_data.username, + token_type=TokenType.internal, + scopes=scopes, + created=created, + expires=expires, + name=token_data.name, + email=token_data.email, + uid=token_data.uid, + groups=token_data.groups, + ) + history_entry = TokenChangeHistoryEntry( + token=token.key, + username=data.username, + token_type=TokenType.internal, + parent=token_data.token.key, + scopes=scopes, + service=service, + expires=expires, + actor=token_data.username, + action=TokenChange.create, + ip_address=ip_address, + event_time=created, + ) + + await self._token_redis_store.store_data(data) + await self._token_db_store.add( + data, service=service, parent=token_data.token.key + ) + await self._token_change_store.add(history_entry) + + self._logger.info( + "Created new internal token", + key=token.key, + service=service, + token_scope=",".join(data.scopes), + ) + + return token + + async def _create_notebook_token( + self, token_data: TokenData, ip_address: str + ) -> Token: + """Retrieve or create a notebook token. + + This must be run with the per-user token lock taken so that any other + requests for a token for the same user will wait until this request is + complete. + + Parameters + ---------- + token_data : `gafaelfawr.models.token.TokenData` + The authentication data for the parent token. + ip_address : `str` + The IP address from which the request came. + """ + # See if there's already a matching notebook token. + key = await self._token_db_store.get_notebook_token_key( + token_data, self._minimum_expiration(token_data) + ) + if key: + data = await self._token_redis_store.get_data_by_key(key) + if data: + return data.token + + # There is not, so we need to create a new one. + token = Token() + created = current_datetime() + expires = created + self._config.token_lifetime + if token_data.expires and token_data.expires < expires: + expires = token_data.expires + data = TokenData( + token=token, + username=token_data.username, + token_type=TokenType.notebook, + scopes=token_data.scopes, + created=created, + expires=expires, + name=token_data.name, + email=token_data.email, + uid=token_data.uid, + groups=token_data.groups, + ) + history_entry = TokenChangeHistoryEntry( + token=token.key, + username=data.username, + token_type=TokenType.notebook, + parent=token_data.token.key, + scopes=data.scopes, + expires=expires, + actor=token_data.username, + action=TokenChange.create, + ip_address=ip_address, + event_time=created, + ) + + await self._token_redis_store.store_data(data) + await self._token_db_store.add(data, parent=token_data.token.key) + await self._token_change_store.add(history_entry) + + # Cache the token and return it. + self._logger.info("Created new notebook token", key=token.key) + return token + async def _get_token( self, key: Tuple[str, ...], scopes: Optional[List[str]] = None ) -> Optional[Token]: @@ -146,7 +404,7 @@ async def _get_token( token = self._cache.get(key) if not token: return None - data = await self._store.get_data(token) + data = await self._token_redis_store.get_data(token) if not data: return None if scopes is not None and not (set(data.scopes) <= set(scopes)): @@ -162,11 +420,34 @@ def _internal_key( self, token_data: TokenData, service: str, scopes: List[str] ) -> Tuple[str, ...]: """Build a cache key for an internal token.""" - scope = ",".join(sorted(scopes)) expires = str(token_data.expires) if token_data.expires else "None" + scope = ",".join(sorted(scopes)) return ("internal", token_data.token.key, expires, service, scope) def _notebook_key(self, token_data: TokenData) -> Tuple[str, ...]: """Build a cache key for a notebook token.""" expires = str(token_data.expires) if token_data.expires else "None" return ("notebook", token_data.token.key, expires) + + def _minimum_expiration(self, token_data: TokenData) -> datetime: + """Determine the minimum expiration for a child token. + + Parameters + ---------- + token_data : `gafaelfawr.models.token.TokenData` + The data for the parent token for which a child token was + requested. + + Returns + ------- + min_expires : `datetime.datetime` + The minimum acceptable expiration time for the child token. If + no child tokens with at least this expiration time exist, a new + child token should be created. + """ + min_expires = current_datetime() + timedelta( + seconds=self._config.token_lifetime.total_seconds() / 2 + ) + if token_data.expires and min_expires > token_data.expires: + min_expires = token_data.expires + return min_expires diff --git a/tests/services/token_test.py b/tests/services/token_test.py index 1783e588a..264b9345a 100644 --- a/tests/services/token_test.py +++ b/tests/services/token_test.py @@ -229,7 +229,7 @@ async def test_notebook_token(config: Config, setup: SetupTest) -> None: assert token == new_token # Try again with the cache cleared to force a database lookup. - token_service._token_cache.clear() + await token_service._token_cache.clear() new_token = await token_service.get_notebook_token( data, ip_address="127.0.0.1" ) @@ -277,7 +277,7 @@ async def test_notebook_token(config: Config, setup: SetupTest) -> None: notebook_token_data, parent=data.token.key ) await setup.session.flush() - token_service._token_cache.clear() + await token_service._token_cache.clear() dup_notebook_token = await token_service.get_notebook_token( data, ip_address="127.0.0.1" ) @@ -363,7 +363,7 @@ async def test_internal_token(config: Config, setup: SetupTest) -> None: assert internal_token == new_internal_token # Try again with the cache cleared to force a database lookup. - token_service._token_cache.clear() + await token_service._token_cache.clear() new_internal_token = await token_service.get_internal_token( data, service="some-service", @@ -417,7 +417,7 @@ async def test_internal_token(config: Config, setup: SetupTest) -> None: internal_token_data, service="some-service", parent=data.token.key ) await setup.session.flush() - token_service._token_cache.clear() + await token_service._token_cache.clear() dup_internal_token = await token_service.get_internal_token( data, service="some-service", diff --git a/tests/token_cache_test.py b/tests/token_cache_test.py index 5708bbd6a..40f0e4ca8 100644 --- a/tests/token_cache_test.py +++ b/tests/token_cache_test.py @@ -30,39 +30,40 @@ async def test_basic(setup: SetupTest) -> None: ) assert internal_token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] + token_data, "some-service", ["read:all"], "127.0.0.1" + ) + assert notebook_token == await token_cache.get_notebook_token( + token_data, "127.0.0.1" ) - assert notebook_token == await token_cache.get_notebook_token(token_data) # Requesting different internal tokens doesn't work. - assert not await token_cache.get_internal_token( - token_data, "other-service", ["read:all"] + assert internal_token != await token_cache.get_internal_token( + token_data, "other-service", ["read:all"], "127.0.0.1" ) - assert not await token_cache.get_internal_token( - token_data, "some-service", [] + assert notebook_token != await token_cache.get_internal_token( + token_data, "some-service", [], "127.0.0.1" ) # A different service token for the same user requesting the same - # information doesn't get anything back. + # information creates a different internal token. new_token_data = await setup.create_session_token(scopes=["read:all"]) - assert not await token_cache.get_internal_token( - new_token_data, "some-service", ["read:all"] + assert internal_token != await token_cache.get_internal_token( + new_token_data, "some-service", ["read:all"], "127.0.0.1" + ) + assert notebook_token != await token_cache.get_notebook_token( + new_token_data, "127.0.0.1" ) - assert not await token_cache.get_notebook_token(new_token_data) # Changing the scope of the parent token doesn't matter as long as the - # internal token is requested with the same scope, as long as the parent - # token has that scope. + # internal token is requested with the same scope. Cases where the parent + # token no longer has that scope are caught one level up by the token + # service and thus aren't tested here. token_data.scopes = ["read:all", "admin:token"] assert internal_token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] - ) - assert not await token_cache.get_internal_token( - token_data, "some-service", ["admin:token"] + token_data, "some-service", ["read:all"], "127.0.0.1" ) - token_data.scopes = [] - assert not await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] + assert internal_token != await token_cache.get_internal_token( + token_data, "some-service", ["admin:token"], "127.0.0.1" ) @@ -79,10 +80,12 @@ async def test_invalid(setup: SetupTest) -> None: ) token_cache.store_notebook_token(notebook_token, token_data) - assert not await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] + assert internal_token != await token_cache.get_internal_token( + token_data, "some-service", ["read:all"], "127.0.0.1" + ) + assert notebook_token != await token_cache.get_notebook_token( + token_data, "127.0.0.1" ) - assert not await token_cache.get_notebook_token(token_data) @pytest.mark.asyncio @@ -115,7 +118,7 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: # The cache should return this token. assert internal_token_data.token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] + token_data, "some-service", ["read:all"], "127.0.0.1" ) # Now change the expiration to be ten seconds earlier, which should make @@ -124,9 +127,9 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: internal_token_data.expires = expires - timedelta(seconds=20) await token_store.store_data(internal_token_data) - # The cache should now decline to return the token. - assert not await token_cache.get_internal_token( - token_data, "some-service", ["read:all"] + # The cache should now decline to return the token and generate a new one. + assert internal_token_data.token != await token_cache.get_internal_token( + token_data, "some-service", ["read:all"], "127.0.0.1" ) # Do the same test with a notebook token. @@ -140,8 +143,11 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: ) await token_store.store_data(notebook_token_data) token_cache.store_notebook_token(notebook_token_data.token, token_data) - token = notebook_token_data.token - assert token == await token_cache.get_notebook_token(token_data) + assert notebook_token_data.token == await token_cache.get_notebook_token( + token_data, "127.0.0.1" + ) notebook_token_data.expires = expires - timedelta(seconds=20) await token_store.store_data(notebook_token_data) - assert not await token_cache.get_notebook_token(token_data) + assert notebook_token_data.token != await token_cache.get_notebook_token( + token_data, "127.0.0.1" + ) From 2946fe14969e82575cb2837c81032724c1f97a82 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Mon, 29 Nov 2021 17:02:25 -0800 Subject: [PATCH 16/24] Make the TokenCache a service Properly speaking, this is part of the service layer. Move it into the appropriate directory and rename it to TokenCacheService accordingly. --- docs/api.rst | 4 ++-- src/gafaelfawr/factory.py | 10 +++++----- src/gafaelfawr/services/token.py | 6 ++++-- src/gafaelfawr/{ => services}/token_cache.py | 4 ++-- tests/{ => services}/token_cache_test.py | 6 +++--- 5 files changed, 16 insertions(+), 14 deletions(-) rename src/gafaelfawr/{ => services}/token_cache.py (99%) rename tests/{ => services}/token_cache_test.py (96%) diff --git a/docs/api.rst b/docs/api.rst index 4f0193a63..4f679dac7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -66,6 +66,8 @@ API reference .. automodapi:: gafaelfawr.services.token +.. automodapi:: gafaelfawr.services.token_cache + .. automodapi:: gafaelfawr.storage.admin .. automodapi:: gafaelfawr.storage.base @@ -81,8 +83,6 @@ API reference .. automodapi:: gafaelfawr.templates :include-all-objects: -.. automodapi:: gafaelfawr.token_cache - .. automodapi:: gafaelfawr.util .. automodapi:: gafaelfawr.verify diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index c4c965cd2..ebf1156c0 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -20,6 +20,7 @@ from gafaelfawr.services.kubernetes import KubernetesService from gafaelfawr.services.oidc import OIDCService from gafaelfawr.services.token import TokenService +from gafaelfawr.services.token_cache import TokenCacheService from gafaelfawr.storage.admin import AdminStore from gafaelfawr.storage.base import RedisStorage from gafaelfawr.storage.history import ( @@ -29,7 +30,6 @@ from gafaelfawr.storage.kubernetes import KubernetesStorage from gafaelfawr.storage.oidc import OIDCAuthorization, OIDCAuthorizationStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore -from gafaelfawr.token_cache import TokenCache from gafaelfawr.verify import TokenVerifier if TYPE_CHECKING: @@ -195,12 +195,12 @@ def create_provider(self) -> Provider: # This should be caught during configuration file parsing. raise NotImplementedError("No authentication provider configured") - def create_token_cache(self) -> TokenCache: + def create_token_cache_service(self) -> TokenCacheService: """Create a token cache. Returns ------- - cache : `gafaelfawr.token_cache.TokenCache` + cache : `gafaelfawr.services.token_cache.TokenCacheService` A new token cache. """ key = self._config.session_secret @@ -208,7 +208,7 @@ def create_token_cache(self) -> TokenCache: token_redis_store = TokenRedisStore(storage, self._logger) token_db_store = TokenDatabaseStore(self.session) token_change_store = TokenChangeHistoryStore(self.session) - return TokenCache( + return TokenCacheService( config=self._config, token_db_store=token_db_store, token_redis_store=token_redis_store, @@ -239,7 +239,7 @@ def create_token_service(self) -> TokenService: storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) token_change_store = TokenChangeHistoryStore(self.session) - token_cache = TokenCache( + token_cache = TokenCacheService( config=self._config, token_db_store=token_db_store, token_redis_store=token_redis_store, diff --git a/src/gafaelfawr/services/token.py b/src/gafaelfawr/services/token.py index ca8fa6292..a73e10f9d 100644 --- a/src/gafaelfawr/services/token.py +++ b/src/gafaelfawr/services/token.py @@ -37,9 +37,9 @@ from gafaelfawr.config import Config from gafaelfawr.models.history import PaginatedHistory from gafaelfawr.models.token import TokenInfo + from gafaelfawr.services.token_cache import TokenCacheService from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore - from gafaelfawr.token_cache import TokenCache __all__ = ["TokenService"] @@ -51,6 +51,8 @@ class TokenService: ---------- config : `gafaelfawr.config.Config` Gafaelfawr configuration. + token_cache : `gafaelfawr.services.token_cache.TokenCacheService` + Cache of internal and notebook tokens. token_db_store : `gafaelfawr.storage.token.TokenDatabaseStore` The database backing store for tokens. token_redis_store : `gafaelfawr.storage.token.TokenRedisStore` @@ -65,7 +67,7 @@ def __init__( self, *, config: Config, - token_cache: TokenCache, + token_cache: TokenCacheService, token_db_store: TokenDatabaseStore, token_redis_store: TokenRedisStore, token_change_store: TokenChangeHistoryStore, diff --git a/src/gafaelfawr/token_cache.py b/src/gafaelfawr/services/token_cache.py similarity index 99% rename from src/gafaelfawr/token_cache.py rename to src/gafaelfawr/services/token_cache.py index 1a872f4b3..beb1e1d2d 100644 --- a/src/gafaelfawr/token_cache.py +++ b/src/gafaelfawr/services/token_cache.py @@ -22,10 +22,10 @@ from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore -__all__ = ["TokenCache"] +__all__ = ["TokenCacheService"] -class TokenCache: +class TokenCacheService: """Cache internal and notebook tokens. To reduce latency and database query load, notebook and internal tokens diff --git a/tests/token_cache_test.py b/tests/services/token_cache_test.py similarity index 96% rename from tests/token_cache_test.py rename to tests/services/token_cache_test.py index 40f0e4ca8..9177018bb 100644 --- a/tests/token_cache_test.py +++ b/tests/services/token_cache_test.py @@ -21,7 +21,7 @@ async def test_basic(setup: SetupTest) -> None: token_data = await setup.create_session_token(scopes=["read:all"]) token_service = setup.factory.create_token_service() - token_cache = setup.factory.create_token_cache() + token_cache = setup.factory.create_token_cache_service() internal_token = await token_service.get_internal_token( token_data, "some-service", ["read:all"], ip_address="127.0.0.1" ) @@ -71,7 +71,7 @@ async def test_basic(setup: SetupTest) -> None: async def test_invalid(setup: SetupTest) -> None: """Invalid tokens should not be returned even if cached.""" token_data = await setup.create_session_token(scopes=["read:all"]) - token_cache = setup.factory.create_token_cache() + token_cache = setup.factory.create_token_cache_service() internal_token = Token() notebook_token = Token() @@ -96,7 +96,7 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: now = current_datetime() storage = RedisStorage(TokenData, config.session_secret, setup.redis) token_store = TokenRedisStore(storage, setup.logger) - token_cache = setup.factory.create_token_cache() + token_cache = setup.factory.create_token_cache_service() # Store a token whose expiration is five seconds more than half the # typical token lifetime in the future and cache that token as an internal From 516360a43c07b0fe2dcd4f51d7133c241a4a7114 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 08:17:43 -0800 Subject: [PATCH 17/24] Shorten name of Kubernetes test Produces nicer test output with 80 columns. --- tests/services/kubernetes_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/services/kubernetes_test.py b/tests/services/kubernetes_test.py index 553586ec1..f7371f550 100644 --- a/tests/services/kubernetes_test.py +++ b/tests/services/kubernetes_test.py @@ -597,7 +597,7 @@ async def test_update_generation( @pytest.mark.asyncio -async def test_update_generation_metadata( +async def test_update_metadata( setup: SetupTest, mock_kubernetes: MockKubernetesApi ) -> None: """Test that Secret metadata is updated even if generation doesn't change. From 33eb0b75dc84bea859b2128bf83dc01baef538de Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 09:40:06 -0800 Subject: [PATCH 18/24] Further refactor token cache Creating the token cache with locks at module import time won't work properly with the test suite when making calls to the underlying app because the locks will be created with a separate loop than the app uses. Instead, add a dependency that manages the token cache data structure with locks, and create it lazily when it's first needed. --- src/gafaelfawr/dependencies/context.py | 10 +++ src/gafaelfawr/dependencies/token_cache.py | 81 ++++++++++++++++++++++ src/gafaelfawr/factory.py | 26 +++++-- src/gafaelfawr/services/token_cache.py | 80 ++++++++++++--------- tests/support/setup.py | 3 + 5 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 src/gafaelfawr/dependencies/token_cache.py diff --git a/src/gafaelfawr/dependencies/context.py b/src/gafaelfawr/dependencies/context.py index b09efb81f..48064e7fa 100644 --- a/src/gafaelfawr/dependencies/context.py +++ b/src/gafaelfawr/dependencies/context.py @@ -21,6 +21,10 @@ from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.db_session import db_session_dependency from gafaelfawr.dependencies.redis import redis_dependency +from gafaelfawr.dependencies.token_cache import ( + TokenCache, + token_cache_dependency, +) from gafaelfawr.factory import ComponentFactory from gafaelfawr.models.state import State @@ -55,6 +59,9 @@ class RequestContext: http_client: AsyncClient """Shared HTTP client.""" + token_cache: TokenCache + """Shared token cache.""" + @property def factory(self) -> ComponentFactory: """A factory for constructing Gafaelfawr components. @@ -67,6 +74,7 @@ def factory(self) -> ComponentFactory: redis=self.redis, session=self.session, http_client=self.http_client, + token_cache=self.token_cache, logger=self.logger, ) @@ -101,6 +109,7 @@ async def context_dependency( redis: Redis = Depends(redis_dependency), session: AsyncSession = Depends(db_session_dependency), http_client: AsyncClient = Depends(http_client_dependency), + token_cache: TokenCache = Depends(token_cache_dependency), ) -> RequestContext: """Provides a RequestContext as a dependency.""" return RequestContext( @@ -110,4 +119,5 @@ async def context_dependency( redis=redis, session=session, http_client=http_client, + token_cache=token_cache, ) diff --git a/src/gafaelfawr/dependencies/token_cache.py b/src/gafaelfawr/dependencies/token_cache.py new file mode 100644 index 000000000..59edb6abe --- /dev/null +++ b/src/gafaelfawr/dependencies/token_cache.py @@ -0,0 +1,81 @@ +"""Shared cache of internal and notebook tokens. + +This is intended to be process-global, but needs to be created after the app +has been created so that the asyncio locks are associated with the correct +event loop. The `TokenCache` object should only be used via +`~gafaelfawr.services.token_cache.TokenCacheService`. +""" + +import asyncio +from dataclasses import dataclass, field +from typing import Dict, Optional, Tuple + +from cachetools import LRUCache + +from gafaelfawr.constants import TOKEN_CACHE_SIZE +from gafaelfawr.models.token import Token + +__all__ = ["TokenCache", "TokenCacheDependency", "token_cache_dependency"] + +LRUTokenCache = LRUCache[Tuple[str, ...], Token] +"""Type for the underlying cache.""" + + +@dataclass +class TokenCache: + """A cache of internal and notebook tokens. + + This contains only the data structure for a token cache. All of the logic + is handled by `~gafaelfawr.services.token_cache.TokenCacheService`. + + Notes + ----- + There is a moderately complex locking structure at play here. When + there's a cache miss for an internal or notebook token for a specific + user, the goal is to block the expensive database lookups or token + creation for that user until the first requester either finds a token in + the database or creates a new one, either way adding it to the cache. + Hopefully then subsequent requests that were blocked on the lock can be + answered from the cache. + + There is therefore a dictionary of per-user locks, but since we don't know + the list of users in advance, we have to populate those locks on the fly. + It shouldn't be necessary to protect the dict of per-user locks with + another lock because we only need to worry about asyncio concurrency, but + since FastAPI does use a thread pool, err on the side of caution and use + the same locking strategy that would be used for multithreaded code. + """ + + cache: LRUTokenCache = field( + default_factory=lambda: LRUCache(TOKEN_CACHE_SIZE) + ) + """Shared cache storage for the tokens.""" + + lock: asyncio.Lock = field(default_factory=asyncio.Lock) + """Lock protecting the dict of per-user locks.""" + + user_lock: Dict[str, asyncio.Lock] = field(default_factory=dict) + """Dict of per-user locks.""" + + +class TokenCacheDependency: + """Manage a single global token cache. + + We have to defer creation of the token cache until application startup, + since the asyncio locks must be created in the same event loop as the rest + of the application. This dependency creates the token cache lazily on + first request and then maintains it as a singleton. + """ + + def __init__(self) -> None: + self._cache: Optional[TokenCache] = None + + async def __call__(self) -> TokenCache: + """Lazily create and return the token cache.""" + if not self._cache: + self._cache = TokenCache() + return self._cache + + +token_cache_dependency = TokenCacheDependency() +"""The dependency that will return the token cache.""" diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index ebf1156c0..5dd127c3b 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -12,6 +12,7 @@ from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency +from gafaelfawr.dependencies.token_cache import TokenCache from gafaelfawr.issuer import TokenIssuer from gafaelfawr.models.token import TokenData from gafaelfawr.providers.github import GitHubProvider @@ -54,6 +55,16 @@ class ComponentFactory: ---------- config : `gafaelfawr.config.Config` Gafaelfawr configuration. + redis : `aioredis.Redis` + Redis client. + session : `sqlalchemy.ext.asyncio.AsyncSession` + SQLAlchemy async session. + http_client : `httpx.AsyncClient` + HTTP async client. + token_cache : `gafaelfawr.dependencies.token_cache.TokenCache` + Shared token cache. + logger : `structlog.stdlib.BoundLogger` + Logger to use for errors. """ @classmethod @@ -67,17 +78,13 @@ async def standalone(cls) -> AsyncIterator[ComponentFactory]: `ComponentFactory`, since they will interfere with each other's Redis pools. - Notes - ----- - This creates a database session directly because fastapi_sqlalchemy - does not work unless an ASGI application has initialized it. - Yields ------ factory : `ComponentFactory` The factory. Must be used as a context manager. """ config = await config_dependency() + token_cache = TokenCache() logger = structlog.get_logger(config.safir.logger_name) assert logger logger.debug("Connecting to Redis") @@ -95,6 +102,7 @@ async def standalone(cls) -> AsyncIterator[ComponentFactory]: redis=redis, session=session, http_client=client, + token_cache=token_cache, logger=logger, ) finally: @@ -108,12 +116,14 @@ def __init__( redis: Redis, session: AsyncSession, http_client: AsyncClient, + token_cache: TokenCache, logger: BoundLogger, ) -> None: self.session = session self._config = config self._redis = redis self._http_client = http_client + self._token_cache = token_cache self._logger = logger def create_admin_service(self) -> AdminService: @@ -209,6 +219,7 @@ def create_token_cache_service(self) -> TokenCacheService: token_db_store = TokenDatabaseStore(self.session) token_change_store = TokenChangeHistoryStore(self.session) return TokenCacheService( + cache=self._token_cache, config=self._config, token_db_store=token_db_store, token_redis_store=token_redis_store, @@ -239,7 +250,8 @@ def create_token_service(self) -> TokenService: storage = RedisStorage(TokenData, key, self._redis) token_redis_store = TokenRedisStore(storage, self._logger) token_change_store = TokenChangeHistoryStore(self.session) - token_cache = TokenCacheService( + token_cache_service = TokenCacheService( + cache=self._token_cache, config=self._config, token_db_store=token_db_store, token_redis_store=token_redis_store, @@ -248,7 +260,7 @@ def create_token_service(self) -> TokenService: ) return TokenService( config=self._config, - token_cache=token_cache, + token_cache=token_cache_service, token_db_store=token_db_store, token_redis_store=token_redis_store, token_change_store=token_change_store, diff --git a/src/gafaelfawr/services/token_cache.py b/src/gafaelfawr/services/token_cache.py index beb1e1d2d..d16bf897f 100644 --- a/src/gafaelfawr/services/token_cache.py +++ b/src/gafaelfawr/services/token_cache.py @@ -14,11 +14,12 @@ from gafaelfawr.util import current_datetime if TYPE_CHECKING: - from typing import Dict, List, Optional, Tuple + from typing import List, Optional, Tuple from structlog.stdlib import BoundLogger from gafaelfawr.config import Config + from gafaelfawr.dependencies.token_cache import TokenCache from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore, TokenRedisStore @@ -26,7 +27,7 @@ class TokenCacheService: - """Cache internal and notebook tokens. + """Manage cache internal and notebook tokens. To reduce latency and database query load, notebook and internal tokens for a given parent token are cached in memory and reused as long as the @@ -35,10 +36,18 @@ class TokenCacheService: Parameters ---------- + cache : `gafaelfawr.dependencies.token_cache.TokenCache` + The underlying cache and locks. config : `gafaelfawr.config.Config` The Gafaelfawr configuration. - redis : `aioredis.Redis` - The Redis client to use to check validity of the cached token. + token_db_store : `gafaelfawr.storage.token.TokenDatabaseStore` + The database backing store for tokens. + token_redis_store : `gafaelfawr.storage.token.TokenRedisStore` + The Redis backing store for tokens. + token_change_store : `gafaelfawr.storage.history.TokenChangeHistoryStore` + The backing store for history of changes to tokens. + logger : `structlog.BoundLogger` + Logger to use. Notes ----- @@ -55,24 +64,17 @@ class TokenCacheService: change by returning a cached token. """ - _cache: LRUCache[Tuple[str, ...], Token] = LRUCache(TOKEN_CACHE_SIZE) - """Shared cache storage for the tokens, global to each process.""" - - _cache_lock = asyncio.Lock() - """Lock around the per-user cache locks.""" - - _cache_user_lock: Dict[str, asyncio.Lock] = {} - """Per-user locks to wait for token issuance for a particular user.""" - def __init__( self, *, + cache: TokenCache, config: Config, token_redis_store: TokenRedisStore, token_db_store: TokenDatabaseStore, token_change_store: TokenChangeHistoryStore, logger: BoundLogger, ) -> None: + self._cache = cache self._config = config self._token_redis_store = token_redis_store self._token_db_store = token_db_store @@ -84,11 +86,11 @@ async def clear(self) -> None: Used primarily for testing. """ - async with self._cache_lock: - self._cache = LRUCache(TOKEN_CACHE_SIZE) - for user, lock in list(self._cache_user_lock.items()): + async with self._cache.lock: + self._cache.cache = LRUCache(TOKEN_CACHE_SIZE) + for user, lock in list(self._cache.user_lock.items()): async with lock: - del self._cache_user_lock[user] + del self._cache.user_lock[user] async def get_internal_token( self, @@ -136,7 +138,7 @@ async def get_internal_token( token = await self._create_internal_token( token_data, service, scopes, ip_address ) - self._cache[key] = token + self._cache.cache[key] = token finally: lock.release() return token @@ -179,7 +181,7 @@ async def get_notebook_token( token = await self._create_notebook_token( token_data, ip_address ) - self._cache[key] = token + self._cache.cache[key] = token finally: lock.release() return token @@ -207,7 +209,7 @@ def store_internal_token( The scopes the internal token should have. """ key = self._internal_key(token_data, service, scopes) - self._cache[key] = token + self._cache.cache[key] = token def store_notebook_token( self, token: Token, token_data: TokenData @@ -224,16 +226,11 @@ def store_notebook_token( The authentication data for the parent token. """ key = self._notebook_key(token_data) - self._cache[key] = token + self._cache.cache[key] = token async def _acquire_user_lock(self, username: str) -> asyncio.Lock: """Acquire a per-user cache lock. - This lock serializes token creation for a specific user so that if a - user without a matching internal or notebook token requests many of - them simultaneously, only one will be created and cached and then - returned by all the other requests. - Parameters ---------- username : `str` @@ -242,14 +239,15 @@ async def _acquire_user_lock(self, username: str) -> asyncio.Lock: Returns ------- lock : `asyncio.Lock` - The acquired per-user lock. + The acquired per-user lock. The caller is responsible for + ensuring the lock is released. """ - async with self._cache_lock: - if username in self._cache_user_lock: - lock = self._cache_user_lock[username] + async with self._cache.lock: + if username in self._cache.user_lock: + lock = self._cache.user_lock[username] else: lock = asyncio.Lock() - self._cache_user_lock[username] = lock + self._cache.user_lock[username] = lock await lock.acquire() return lock @@ -400,8 +398,24 @@ async def _create_notebook_token( async def _get_token( self, key: Tuple[str, ...], scopes: Optional[List[str]] = None ) -> Optional[Token]: - """Retrieve a cached token by key.""" - token = self._cache.get(key) + """Retrieve a cached token by key. + + Parameters + ---------- + key : Tuple[`str`, ...] + The cache key, created by ``_internal_key`` or ``_notebook_key``. + scopes : List[`str`], optional + If provided, ensure that the returned token has scopes that are a + subset of this scope list. This is used to force a cache miss if + an internal token is requested but the requesting token no longer + has the scopes that the internal token provides. + + Returns + ------- + token : `gafaelfawr.models.token.Token` or `None` + The cached token, or `None` on a cache miss. + """ + token = self._cache.cache.get(key) if not token: return None data = await self._token_redis_store.get_data(token) diff --git a/tests/support/setup.py b/tests/support/setup.py index af4826c41..8f3100196 100644 --- a/tests/support/setup.py +++ b/tests/support/setup.py @@ -14,6 +14,7 @@ from gafaelfawr.constants import COOKIE_NAME from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.redis import redis_dependency +from gafaelfawr.dependencies.token_cache import TokenCache from gafaelfawr.factory import ComponentFactory from gafaelfawr.models.state import State from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo @@ -104,6 +105,7 @@ def __init__( self.redis = redis self.session = session self.http_client = http_client + self.token_cache = TokenCache() self.logger = structlog.get_logger(config.safir.logger_name) assert self.logger @@ -124,6 +126,7 @@ def factory(self) -> ComponentFactory: config=config_dependency._config, redis=self.redis, http_client=self.http_client, + token_cache=self.token_cache, session=self.session, logger=self.logger, ) From 099aa37197da6a692360b1dcdc4b35592356cf49 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 09:41:01 -0800 Subject: [PATCH 19/24] Add load test for parallel notebook token creation Add a load test that makes 100 near-simultaneous requests to the /auth endpoint requesting a notebook token that doesn't yet exist. The new caching strategy should avoid PostgreSQL requests for all but the first. --- tests/handlers/auth_load_test.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/handlers/auth_load_test.py diff --git a/tests/handlers/auth_load_test.py b/tests/handlers/auth_load_test.py new file mode 100644 index 000000000..e79b84562 --- /dev/null +++ b/tests/handlers/auth_load_test.py @@ -0,0 +1,40 @@ +"""Tests multiple simultaneous /auth requests. + +These tests are intended to catch problems with excessive database load or +deadlocking when processing numerous requests that require subtokens or some +other potentially expensive or coordinated action. +""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import pytest + +from gafaelfawr.models.token import Token + +if TYPE_CHECKING: + from httpx import AsyncClient + + from tests.support.setup import SetupTest + + +@pytest.mark.asyncio +async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: + data = await setup.create_session_token(scopes=["exec:test", "read:all"]) + await setup.login(client, data.token) + + request_awaits = [] + for _ in range(100): + request_awaits.append( + client.get( + "/auth", params={"scope": "exec:test", "notebook": "true"} + ) + ) + responses = await asyncio.gather(*request_awaits) + assert responses[0].status_code == 200 + token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) + for r in responses: + assert r.status_code == 200 + assert Token.from_str(r.headers["X-Auth-Request-Token"]) == token From 6bb7a98de7e35b2c9fb012a8debf105cb3aca4d4 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 12:03:44 -0800 Subject: [PATCH 20/24] Avoid charset_normalize warning Parsing the error return from the OIDC server when given a bad client ID was for some reason triggering a warning in charset_normalize. This is probably some sort of bug in it or httpx, but the return status is JSON, so work around the problem by parsing it as such. --- tests/handlers/oidc_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/handlers/oidc_test.py b/tests/handlers/oidc_test.py index 29cd3697a..baebd9d81 100644 --- a/tests/handlers/oidc_test.py +++ b/tests/handlers/oidc_test.py @@ -222,7 +222,9 @@ async def test_login_errors( } r = await client.get("/auth/openid/login", params=login_params) assert r.status_code == 400 - assert "Unknown client_id bad-client" in r.text + data = r.json() + assert data["detail"][0]["type"] == "invalid_client" + assert "Unknown client_id bad-client" in data["detail"][0]["msg"] assert parse_log(caplog) == [ { From 7a68ee3fa7d1a32bbd48df988cabc0c7c4f59636 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 12:25:49 -0800 Subject: [PATCH 21/24] Add load test for internal tokens Add a load test for internal tokens similar to that for notebook tokens. This uncovered a requirement to reset the token cache and its locks between instantiations of the app so that the locks aren't shared across asyncio loops. --- src/gafaelfawr/dependencies/token_cache.py | 21 ++++++++++ src/gafaelfawr/main.py | 2 + src/gafaelfawr/services/token_cache.py | 9 +---- tests/handlers/auth_load_test.py | 45 ++++++++++++++++++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/gafaelfawr/dependencies/token_cache.py b/src/gafaelfawr/dependencies/token_cache.py index 59edb6abe..3b70f5afe 100644 --- a/src/gafaelfawr/dependencies/token_cache.py +++ b/src/gafaelfawr/dependencies/token_cache.py @@ -57,6 +57,17 @@ class TokenCache: user_lock: Dict[str, asyncio.Lock] = field(default_factory=dict) """Dict of per-user locks.""" + async def clear(self) -> None: + """Invalidate the cache. + + Used primarily for testing. + """ + async with self.lock: + self.cache = LRUCache(TOKEN_CACHE_SIZE) + for user, lock in list(self.user_lock.items()): + async with lock: + del self.user_lock[user] + class TokenCacheDependency: """Manage a single global token cache. @@ -76,6 +87,16 @@ async def __call__(self) -> TokenCache: self._cache = TokenCache() return self._cache + async def aclose(self) -> None: + """Clear the cache. + + Should be called from a shutdown hook to ensure that cache locks are + not reused across tests when running the test suite. + """ + if self._cache: + await self._cache.clear() + self._cache = None + token_cache_dependency = TokenCacheDependency() """The dependency that will return the token cache.""" diff --git a/src/gafaelfawr/main.py b/src/gafaelfawr/main.py index d141b4a8f..e23fd4cb6 100644 --- a/src/gafaelfawr/main.py +++ b/src/gafaelfawr/main.py @@ -20,6 +20,7 @@ from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.dependencies.db_session import db_session_dependency from gafaelfawr.dependencies.redis import redis_dependency +from gafaelfawr.dependencies.token_cache import token_cache_dependency from gafaelfawr.exceptions import PermissionDeniedError, ValidationError from gafaelfawr.handlers import ( analyze, @@ -120,6 +121,7 @@ async def shutdown_event() -> None: await http_client_dependency.aclose() await db_session_dependency.aclose() await redis_dependency.aclose() + await token_cache_dependency.aclose() @app.exception_handler(PermissionDeniedError) diff --git a/src/gafaelfawr/services/token_cache.py b/src/gafaelfawr/services/token_cache.py index d16bf897f..09149ed24 100644 --- a/src/gafaelfawr/services/token_cache.py +++ b/src/gafaelfawr/services/token_cache.py @@ -6,9 +6,6 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING -from cachetools import LRUCache - -from gafaelfawr.constants import TOKEN_CACHE_SIZE from gafaelfawr.models.history import TokenChange, TokenChangeHistoryEntry from gafaelfawr.models.token import Token, TokenData, TokenType from gafaelfawr.util import current_datetime @@ -86,11 +83,7 @@ async def clear(self) -> None: Used primarily for testing. """ - async with self._cache.lock: - self._cache.cache = LRUCache(TOKEN_CACHE_SIZE) - for user, lock in list(self._cache.user_lock.items()): - async with lock: - del self._cache.user_lock[user] + await self._cache.clear() async def get_internal_token( self, diff --git a/tests/handlers/auth_load_test.py b/tests/handlers/auth_load_test.py index e79b84562..1bb2101f7 100644 --- a/tests/handlers/auth_load_test.py +++ b/tests/handlers/auth_load_test.py @@ -38,3 +38,48 @@ async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: for r in responses: assert r.status_code == 200 assert Token.from_str(r.headers["X-Auth-Request-Token"]) == token + + +@pytest.mark.asyncio +async def test_internal(client: AsyncClient, setup: SetupTest) -> None: + data = await setup.create_session_token(scopes=["exec:test", "read:all"]) + await setup.login(client, data.token) + + request_awaits = [] + for _ in range(100): + request_awaits.append( + client.get( + "/auth", + params={ + "scope": "exec:test", + "delegate_to": "a-service", + "delegate_scope": "read:all", + }, + ) + ) + responses = await asyncio.gather(*request_awaits) + assert responses[0].status_code == 200 + token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) + for r in responses: + assert r.status_code == 200 + assert Token.from_str(r.headers["X-Auth-Request-Token"]) == token + + request_awaits = [] + for _ in range(100): + request_awaits.append( + client.get( + "/auth", + params={ + "scope": "exec:test", + "delegate_to": "a-service", + "delegate_scope": "exec:test", + }, + ) + ) + responses = await asyncio.gather(*request_awaits) + assert responses[0].status_code == 200 + new_token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) + assert new_token != token + for r in responses: + assert r.status_code == 200 + assert Token.from_str(r.headers["X-Auth-Request-Token"]) == new_token From 0c2574eae955f10f010799583971d19709d766fd Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 15:59:24 -0800 Subject: [PATCH 22/24] Finish eliminating SetupTest Move the remaining utility functions into other modules and create a new factory fixture, which replaces the last use of SetupTest. Add a reconfigure method to ComponentFactory to update it with a new configuration, required by many of the tests. Properly wrap database operations in tests with a transaction, now that the code is designed for one transaction per request and doesn't do transaction management inside the services. --- src/gafaelfawr/factory.py | 13 + tests/conftest.py | 33 +- tests/handlers/analyze_test.py | 28 +- tests/handlers/api_admins_test.py | 35 +- tests/handlers/api_history_test.py | 284 ++++---- tests/handlers/api_login_test.py | 13 +- tests/handlers/api_tokens_test.py | 203 +++--- tests/handlers/auth_load_test.py | 24 +- tests/handlers/auth_logging_test.py | 21 +- tests/handlers/auth_test.py | 104 +-- tests/handlers/influxdb_test.py | 19 +- tests/handlers/login_github_test.py | 14 +- tests/handlers/logout_test.py | 18 +- tests/handlers/oidc_test.py | 63 +- tests/issuer_test.py | 10 +- tests/services/admin_test.py | 103 +-- tests/services/kubernetes_test.py | 100 +-- tests/services/oidc_test.py | 35 +- tests/services/token_cache_test.py | 95 +-- tests/services/token_test.py | 971 +++++++++++++++------------- tests/support/cookies.py | 55 ++ tests/support/setup.py | 213 ------ tests/support/tokens.py | 66 +- tests/verify_test.py | 17 +- 24 files changed, 1334 insertions(+), 1203 deletions(-) create mode 100644 tests/support/cookies.py delete mode 100644 tests/support/setup.py diff --git a/src/gafaelfawr/factory.py b/src/gafaelfawr/factory.py index 5dd127c3b..f82089392 100644 --- a/src/gafaelfawr/factory.py +++ b/src/gafaelfawr/factory.py @@ -278,3 +278,16 @@ def create_token_verifier(self) -> TokenVerifier: return TokenVerifier( self._config.verifier, self._http_client, self._logger ) + + def reconfigure(self, config: Config) -> None: + """Change the internal configuration. + + Intended for the test suite, which may have to reconfigure the + component factory after creating it. + + Parameters + ---------- + config : `gafaelfawr.config.Config` + New configuration. + """ + self._config = config diff --git a/tests/conftest.py b/tests/conftest.py index ed106fa9f..2c1fc9caa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ from gafaelfawr.constants import COOKIE_NAME from gafaelfawr.database import initialize_database from gafaelfawr.dependencies.config import config_dependency +from gafaelfawr.factory import ComponentFactory from gafaelfawr.models.state import State from gafaelfawr.models.token import TokenType from tests.pages.tokens import TokensPage @@ -22,7 +23,6 @@ from tests.support.kubernetes import MockKubernetesApi from tests.support.selenium import run_app, selenium_driver from tests.support.settings import build_settings -from tests.support.setup import SetupTest if TYPE_CHECKING: from pathlib import Path @@ -98,6 +98,17 @@ async def empty_database(config: Config) -> None: await initialize_database(config, reset=True) +@pytest.fixture +async def factory(empty_database: None) -> AsyncIterator[ComponentFactory]: + """Return a component factory. + + Note that this creates a separate SQLAlchemy AsyncSession from any that + may be created by the FastAPI app. + """ + async with ComponentFactory.standalone() as factory: + yield factory + + @pytest.fixture def mock_kubernetes() -> Iterator[MockKubernetesApi]: """Replace the Kubernetes API with a mock class. @@ -153,23 +164,3 @@ async def selenium_config( del driver.header_overrides yield config - - -@pytest.fixture -async def setup( - tmp_path: Path, empty_database: None -) -> AsyncIterator[SetupTest]: - """Create a test setup object. - - This encapsulates a lot of the configuration and machinery of setting up - tests, mocking responses, and doing repetitive checks. This fixture must - be referenced even if not used to set up the application properly for - testing. - - Returns - ------- - setup : `tests.support.setup.SetupTest` - The setup object. - """ - async with SetupTest.create(tmp_path) as setup: - yield setup diff --git a/tests/handlers/analyze_test.py b/tests/handlers/analyze_test.py index 1ff146fa3..608abe33a 100644 --- a/tests/handlers/analyze_test.py +++ b/tests/handlers/analyze_test.py @@ -9,16 +9,18 @@ import pytest from gafaelfawr.models.token import Token +from tests.support.cookies import set_session_cookie from tests.support.headers import query_from_url +from tests.support.tokens import create_session_token if TYPE_CHECKING: from httpx import AsyncClient - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_analyze_no_auth(client: AsyncClient, setup: SetupTest) -> None: +async def test_analyze_no_auth(client: AsyncClient) -> None: r = await client.get("/auth/analyze") assert r.status_code == 307 url = urlparse(r.headers["Location"]) @@ -31,13 +33,15 @@ async def test_analyze_no_auth(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_analyze_session(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token( - group_names=["foo", "bar"], scopes=["read:all"] +async def test_analyze_session( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token( + factory, group_names=["foo", "bar"], scopes=["read:all"] ) assert token_data.expires assert token_data.groups - await setup.login(client, token_data.token) + await set_session_cookie(client, token_data.token) r = await client.get("/auth/analyze") assert r.status_code == 200 @@ -64,14 +68,16 @@ async def test_analyze_session(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_token(client: AsyncClient, setup: SetupTest) -> None: +async def test_invalid_token(client: AsyncClient) -> None: r = await client.post("/auth/analyze", data={"token": "some-token"}) assert r.status_code == 200 assert r.json() == {"token": {"errors": [ANY], "valid": False}} @pytest.mark.asyncio -async def test_analyze_token(client: AsyncClient, setup: SetupTest) -> None: +async def test_analyze_token( + client: AsyncClient, factory: ComponentFactory +) -> None: token = Token() # Handle with no session. @@ -83,8 +89,8 @@ async def test_analyze_token(client: AsyncClient, setup: SetupTest) -> None: } # Valid token. - token_data = await setup.create_session_token( - group_names=["foo", "bar"], scopes=["admin:token", "read:all"] + token_data = await create_session_token( + factory, group_names=["foo", "bar"], scopes=["admin:token", "read:all"] ) assert token_data.expires assert token_data.groups @@ -115,7 +121,7 @@ async def test_analyze_token(client: AsyncClient, setup: SetupTest) -> None: token_data.name = None token_data.uid = None token_data.groups = None - token_service = setup.factory.create_token_service() + token_service = factory.create_token_service() user_token = await token_service.create_user_token( token_data, token_data.username, diff --git a/tests/handlers/api_admins_test.py b/tests/handlers/api_admins_test.py index 63843df0e..991b250b6 100644 --- a/tests/handlers/api_admins_test.py +++ b/tests/handlers/api_admins_test.py @@ -6,19 +6,22 @@ import pytest +from tests.support.cookies import set_session_cookie +from tests.support.tokens import create_session_token + if TYPE_CHECKING: from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_admins(client: AsyncClient, setup: SetupTest) -> None: +async def test_admins(client: AsyncClient, factory: ComponentFactory) -> None: r = await client.get("/auth/api/v1/admins") assert r.status_code == 401 - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, @@ -29,7 +32,7 @@ async def test_admins(client: AsyncClient, setup: SetupTest) -> None: "type": "permission_denied", } - token_data = await setup.create_session_token(scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes=["admin:token"]) r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, @@ -37,11 +40,11 @@ async def test_admins(client: AsyncClient, setup: SetupTest) -> None: assert r.status_code == 200 assert r.json() == [{"username": "admin"}] - admin_service = setup.factory.create_admin_service() - await admin_service.add_admin( - "example", actor="admin", ip_address="127.0.0.1" - ) - await setup.session.commit() + admin_service = factory.create_admin_service() + async with factory.session.begin(): + await admin_service.add_admin( + "example", actor="admin", ip_address="127.0.0.1" + ) r = await client.get( "/auth/api/v1/admins", @@ -52,7 +55,9 @@ async def test_admins(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_add_delete(client: AsyncClient, setup: SetupTest) -> None: +async def test_add_delete( + client: AsyncClient, factory: ComponentFactory +) -> None: r = await client.post( "/auth/api/v1/admins", json={"username": "some-user"} ) @@ -60,8 +65,8 @@ async def test_add_delete(client: AsyncClient, setup: SetupTest) -> None: r = await client.delete("/auth/api/v1/admins/admin") assert r.status_code == 401 - token_data = await setup.create_session_token(username="admin") - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory, username="admin") + csrf = await set_session_cookie(client, token_data.token) r = await client.post( "/auth/api/v1/admins", headers={"X-CSRF-Token": csrf}, @@ -81,10 +86,10 @@ async def test_add_delete(client: AsyncClient, setup: SetupTest) -> None: "type": "permission_denied", } - token_data = await setup.create_session_token( - username="admin", scopes=["admin:token"] + token_data = await create_session_token( + factory, username="admin", scopes=["admin:token"] ) - csrf = await setup.login(client, token_data.token) + csrf = await set_session_cookie(client, token_data.token) r = await client.post( "/auth/api/v1/admins", json={"username": "new-admin"} ) diff --git a/tests/handlers/api_history_test.py b/tests/handlers/api_history_test.py index 729eaa36c..01c30ed46 100644 --- a/tests/handlers/api_history_test.py +++ b/tests/handlers/api_history_test.py @@ -22,17 +22,19 @@ from gafaelfawr.schema import TokenChangeHistory from gafaelfawr.util import current_datetime, datetime_to_db from tests.support.constants import TEST_HOSTNAME +from tests.support.cookies import set_session_cookie +from tests.support.tokens import create_session_token if TYPE_CHECKING: from typing import Any, Callable, Dict, List, Optional, Union from httpx import AsyncClient - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory async def build_history( - setup: SetupTest, + factory: ComponentFactory, ) -> List[TokenChangeHistoryEntry]: """Perform a bunch of token manipulations and return the history entries. @@ -40,122 +42,131 @@ async def build_history( since that's tested in other tests. The only point of this function is to build enough history that we can make interesting paginated queries of it. """ - token_service = setup.factory.create_token_service() + token_service = factory.create_token_service() user_info_one = TokenUserInfo(username="one") - token_one = await token_service.create_session_token( - user_info_one, - scopes=["exec:test", "read:all", "user:token"], - ip_address="192.0.2.3", - ) - token_data_one = await token_service.get_data(token_one) - assert token_data_one - await token_service.get_internal_token( - token_data_one, - "foo", - scopes=["exec:test", "read:all"], - ip_address="192.0.2.4", - ) - internal_token_one_bar = await token_service.get_internal_token( - token_data_one, "bar", scopes=["read:all"], ip_address="192.0.2.3" - ) - token_data_internal_one_bar = await token_service.get_data( - internal_token_one_bar - ) - assert token_data_internal_one_bar - await token_service.get_internal_token( - token_data_internal_one_bar, "baz", scopes=[], ip_address="10.10.10.10" - ) - notebook_token_one = await token_service.get_notebook_token( - token_data_one, ip_address="198.51.100.5" - ) - token_data_notebook_one = await token_service.get_data(notebook_token_one) - assert token_data_notebook_one - await token_service.get_internal_token( - token_data_notebook_one, - "foo", - scopes=["exec:test"], - ip_address="10.10.10.20", - ) + async with factory.session.begin(): + token_one = await token_service.create_session_token( + user_info_one, + scopes=["exec:test", "read:all", "user:token"], + ip_address="192.0.2.3", + ) + token_data_one = await token_service.get_data(token_one) + assert token_data_one + await token_service.get_internal_token( + token_data_one, + "foo", + scopes=["exec:test", "read:all"], + ip_address="192.0.2.4", + ) + internal_token_one_bar = await token_service.get_internal_token( + token_data_one, "bar", scopes=["read:all"], ip_address="192.0.2.3" + ) + token_data_internal_one_bar = await token_service.get_data( + internal_token_one_bar + ) + assert token_data_internal_one_bar + await token_service.get_internal_token( + token_data_internal_one_bar, + "baz", + scopes=[], + ip_address="10.10.10.10", + ) + notebook_token_one = await token_service.get_notebook_token( + token_data_one, ip_address="198.51.100.5" + ) + token_data_notebook_one = await token_service.get_data( + notebook_token_one + ) + assert token_data_notebook_one + await token_service.get_internal_token( + token_data_notebook_one, + "foo", + scopes=["exec:test"], + ip_address="10.10.10.20", + ) user_info_two = TokenUserInfo(username="two") - token_two = await token_service.create_session_token( - user_info_two, - scopes=["read:some", "user:token"], - ip_address="192.0.2.20", - ) - token_data_two = await token_service.get_data(token_two) - assert token_data_two - user_token_two = await token_service.create_user_token( - token_data_two, - token_data_two.username, - token_name="some token", - scopes=["read:some", "user:token"], - ip_address="192.0.2.20", - ) - token_data_user_two = await token_service.get_data(user_token_two) - assert token_data_user_two - await token_service.get_internal_token( - token_data_user_two, - "foo", - scopes=["read:some"], - ip_address="10.10.10.10", - ) - assert await token_service.modify_token( - user_token_two.key, - token_data_user_two, - token_data_user_two.username, - ip_address="192.0.2.20", - token_name="happy token", - ) - - request = AdminTokenRequest( - username="service", - token_type=TokenType.service, - scopes=["admin:token"], - ) - service_token = await token_service.create_token_from_admin_request( - request, - TokenData.bootstrap_token(), - ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", - ) - service_token_data = await token_service.get_data(service_token) - assert service_token_data - assert await token_service.modify_token( - user_token_two.key, - service_token_data, - ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", - scopes=["admin:token", "read:all"], - ) - assert await token_service.modify_token( - user_token_two.key, - service_token_data, - ip_address="2001:db8:034a:ea78:4278:4562:6578:af42", - token_name="other name", - expires=current_datetime() + timedelta(days=30), - scopes=["read:all"], - ) - assert await token_service.delete_token( - token_one.key, - service_token_data, - username=token_data_one.username, - ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", - ) + async with factory.session.begin(): + token_two = await token_service.create_session_token( + user_info_two, + scopes=["read:some", "user:token"], + ip_address="192.0.2.20", + ) + token_data_two = await token_service.get_data(token_two) + assert token_data_two + user_token_two = await token_service.create_user_token( + token_data_two, + token_data_two.username, + token_name="some token", + scopes=["read:some", "user:token"], + ip_address="192.0.2.20", + ) + token_data_user_two = await token_service.get_data(user_token_two) + assert token_data_user_two + await token_service.get_internal_token( + token_data_user_two, + "foo", + scopes=["read:some"], + ip_address="10.10.10.10", + ) + assert await token_service.modify_token( + user_token_two.key, + token_data_user_two, + token_data_user_two.username, + ip_address="192.0.2.20", + token_name="happy token", + ) + + async with factory.session.begin(): + request = AdminTokenRequest( + username="service", + token_type=TokenType.service, + scopes=["admin:token"], + ) + service_token = await token_service.create_token_from_admin_request( + request, + TokenData.bootstrap_token(), + ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", + ) + service_token_data = await token_service.get_data(service_token) + assert service_token_data + assert await token_service.modify_token( + user_token_two.key, + service_token_data, + ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", + scopes=["admin:token", "read:all"], + ) + assert await token_service.modify_token( + user_token_two.key, + service_token_data, + ip_address="2001:db8:034a:ea78:4278:4562:6578:af42", + token_name="other name", + expires=current_datetime() + timedelta(days=30), + scopes=["read:all"], + ) + assert await token_service.delete_token( + token_one.key, + service_token_data, + username=token_data_one.username, + ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", + ) # Spread out the timestamps so that we can test date range queries. Every # other entry has the same timestamp as the previous entry to test that # queries handle entries with the same timestamp. - stmt = select(TokenChangeHistory).order_by(TokenChangeHistory.id) - result = await setup.session.execute(stmt) - entries = [e[0] for e in result.all()] - event_time = current_datetime() - timedelta(seconds=len(entries) * 5) - for i, entry in enumerate(entries): - entry.event_time = datetime_to_db(event_time) - if i % 2 != 0: - event_time += timedelta(seconds=5) - await setup.session.commit() - - history = await token_service.get_change_history(service_token_data) + async with factory.session.begin(): + stmt = select(TokenChangeHistory).order_by(TokenChangeHistory.id) + result = await factory.session.execute(stmt) + entries = [e[0] for e in result.all()] + event_time = current_datetime() - timedelta(seconds=len(entries) * 5) + for i, entry in enumerate(entries): + entry.event_time = datetime_to_db(event_time) + if i % 2 != 0: + event_time += timedelta(seconds=5) + + async with factory.session.begin(): + history = await token_service.get_change_history(service_token_data) assert history.count == 20 assert len(history.entries) == 20 return history.entries @@ -265,11 +276,11 @@ async def check_pagination( @pytest.mark.asyncio async def test_admin_change_history( - client: AsyncClient, setup: SetupTest + client: AsyncClient, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token(scopes=["admin:token"]) - await setup.login(client, token_data.token) - history = await build_history(setup) + token_data = await create_session_token(factory, scopes=["admin:token"]) + await set_session_cookie(client, token_data.token) + history = await build_history(factory) r = await client.get("/auth/api/v1/history/token-changes") assert r.status_code == 200 @@ -371,11 +382,11 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: @pytest.mark.asyncio async def test_user_change_history( - client: AsyncClient, setup: SetupTest + client: AsyncClient, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token(username="one") - await setup.login(client, token_data.token) - history = [e for e in await build_history(setup) if e.username == "one"] + token_data = await create_session_token(factory, username="one") + await set_session_cookie(client, token_data.token) + history = [e for e in await build_history(factory) if e.username == "one"] r = await client.get("/auth/api/v1/users/one/token-change-history") assert r.status_code == 200 @@ -466,8 +477,10 @@ def ipv6_filter(e: TokenChangeHistoryEntry) -> bool: @pytest.mark.asyncio -async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() +async def test_auth_required( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) username = token_data.username key = token_data.token.key @@ -484,26 +497,31 @@ async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_admin_required(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() - await setup.login(client, token_data.token) +async def test_admin_required( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) + await set_session_cookie(client, token_data.token) r = await client.get("/auth/api/v1/history/token-changes") assert r.status_code == 403 @pytest.mark.asyncio -async def test_no_scope(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() +async def test_no_scope( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) username = token_data.username - token_service = setup.factory.create_token_service() - token = await token_service.create_user_token( - token_data, - token_data.username, - token_name="user", - scopes=[], - ip_address="127.0.0.1", - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_user_token( + token_data, + token_data.username, + token_name="user", + scopes=[], + ip_address="127.0.0.1", + ) r = await client.get( f"/auth/api/v1/users/{username}/token-change-history", diff --git a/tests/handlers/api_login_test.py b/tests/handlers/api_login_test.py index f778c7698..5fb66cb9c 100644 --- a/tests/handlers/api_login_test.py +++ b/tests/handlers/api_login_test.py @@ -14,20 +14,21 @@ from gafaelfawr.models.token import Token from tests.support.constants import TEST_HOSTNAME from tests.support.headers import assert_unauthorized_is_correct +from tests.support.tokens import create_session_token if TYPE_CHECKING: from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_login( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token( - username="example", scopes=["read:all", "exec:admin"] + token_data = await create_session_token( + factory, username="example", scopes=["read:all", "exec:admin"] ) cookie = await State(token=token_data.token).as_cookie() client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) @@ -53,13 +54,13 @@ async def test_login( @pytest.mark.asyncio async def test_login_no_auth( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: r = await client.get("/auth/api/v1/login") assert_unauthorized_is_correct(r, config) # An Authorization header with a valid token still redirects. - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) r = await client.get( "/auth/api/v1/login", headers={"Authorization": f"bearer {token_data.token}"}, diff --git a/tests/handlers/api_tokens_test.py b/tests/handlers/api_tokens_test.py index 73ef0c5dd..ae9326309 100644 --- a/tests/handlers/api_tokens_test.py +++ b/tests/handlers/api_tokens_test.py @@ -14,19 +14,21 @@ from gafaelfawr.models.token import Token, TokenGroup, TokenUserInfo from gafaelfawr.util import current_datetime from tests.support.constants import TEST_HOSTNAME +from tests.support.cookies import clear_session_cookie, set_session_cookie from tests.support.logging import parse_log +from tests.support.tokens import create_session_token if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_create_delete_modify( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture ) -> None: user_info = TokenUserInfo( username="example", @@ -35,14 +37,14 @@ async def test_create_delete_modify( uid=45613, groups=[TokenGroup(name="foo", id=12313)], ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, - scopes=["read:all", "exec:admin", "user:token"], - ip_address="127.0.0.1", - ) - csrf = await setup.login(client, session_token) - await setup.session.commit() + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "exec:admin", "user:token"], + ip_address="127.0.0.1", + ) + csrf = await set_session_cookie(client, session_token) expires = current_datetime() + timedelta(days=100) r = await client.post( @@ -215,7 +217,7 @@ async def test_create_delete_modify( @pytest.mark.asyncio async def test_token_info( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: user_info = TokenUserInfo( username="example", @@ -224,11 +226,13 @@ async def test_token_info( uid=45613, groups=[TokenGroup(name="foo", id=12313)], ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, scopes=["exec:admin", "user:token"], ip_address="127.0.0.1" - ) - await setup.session.commit() + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["exec:admin", "user:token"], + ip_address="127.0.0.1", + ) r = await client.get( "/auth/api/v1/token-info", @@ -273,15 +277,15 @@ async def test_token_info( # data. expires = now + timedelta(days=100) data = await token_service.get_data(session_token) - user_token = await token_service.create_user_token( - data, - data.username, - token_name="some-token", - scopes=["exec:admin"], - expires=expires, - ip_address="127.0.0.1", - ) - await setup.session.commit() + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + data.username, + token_name="some-token", + scopes=["exec:admin"], + expires=expires, + ip_address="127.0.0.1", + ) r = await client.get( "/auth/api/v1/token-info", @@ -315,14 +319,16 @@ async def test_token_info( @pytest.mark.asyncio -async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() +async def test_auth_required( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) token = token_data.token - csrf = await setup.login(client, token) + csrf = await set_session_cookie(client, token) # Replace the cookie with one containing the CSRF token but not the # authentication token. - setup.logout(client) + clear_session_cookie(client) client.cookies[COOKIE_NAME] = await State(csrf=csrf).as_cookie() r = await client.post( @@ -365,17 +371,20 @@ async def test_auth_required(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_csrf_required(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token(scopes=["admin:token"]) - csrf = await setup.login(client, token_data.token) - token_service = setup.factory.create_token_service() - user_token = await token_service.create_user_token( - token_data, - token_data.username, - token_name="foo", - scopes=[], - ip_address="127.0.0.1", - ) +async def test_csrf_required( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory, scopes=["admin:token"]) + csrf = await set_session_cookie(client, token_data.token) + token_service = factory.create_token_service() + async with factory.session.begin(): + user_token = await token_service.create_user_token( + token_data, + token_data.username, + token_name="foo", + scopes=[], + ip_address="127.0.0.1", + ) r = await client.post( "/auth/api/v1/tokens", @@ -428,9 +437,9 @@ async def test_csrf_required(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_no_bootstrap( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) token = token_data.token bootstrap_token = str(config.bootstrap_token) @@ -468,17 +477,19 @@ async def test_no_bootstrap( @pytest.mark.asyncio -async def test_no_scope(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() - token_service = setup.factory.create_token_service() - token = await token_service.create_user_token( - token_data, - token_data.username, - token_name="user", - scopes=[], - ip_address="127.0.0.1", - ) - await setup.session.commit() +async def test_no_scope( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_user_token( + token_data, + token_data.username, + token_name="user", + scopes=[], + ip_address="127.0.0.1", + ) r = await client.get( f"/auth/api/v1/users/{token_data.username}/tokens", @@ -514,10 +525,12 @@ async def test_no_scope(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_modify_nonuser(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() +async def test_modify_nonuser( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) token = token_data.token - csrf = await setup.login(client, token) + csrf = await set_session_cookie(client, token) r = await client.patch( f"/auth/api/v1/users/{token_data.username}/tokens/{token.key}", @@ -529,26 +542,29 @@ async def test_modify_nonuser(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_wrong_user(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() - csrf = await setup.login(client, token_data.token) - token_service = setup.factory.create_token_service() +async def test_wrong_user( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) + csrf = await set_session_cookie(client, token_data.token) + token_service = factory.create_token_service() user_info = TokenUserInfo( username="other-person", name="Some Other Person", uid=137123 ) - other_session_token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" - ) + async with factory.session.begin(): + other_session_token = await token_service.create_session_token( + user_info, scopes=["user:token"], ip_address="127.0.0.1" + ) other_session_data = await token_service.get_data(other_session_token) assert other_session_data - other_token = await token_service.create_user_token( - other_session_data, - "other-person", - token_name="foo", - scopes=[], - ip_address="127.0.0.1", - ) - await setup.session.commit() + async with factory.session.begin(): + other_token = await token_service.create_user_token( + other_session_data, + "other-person", + token_name="foo", + scopes=[], + ip_address="127.0.0.1", + ) # Get a token list. r = await client.get("/auth/api/v1/users/other-person/tokens") @@ -622,10 +638,12 @@ async def test_wrong_user(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_no_expires(client: AsyncClient, setup: SetupTest) -> None: +async def test_no_expires( + client: AsyncClient, factory: ComponentFactory +) -> None: """Test creating a user token that doesn't expire.""" - token_data = await setup.create_session_token() - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory) + csrf = await set_session_cookie(client, token_data.token) r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", @@ -651,7 +669,7 @@ async def test_no_expires(client: AsyncClient, setup: SetupTest) -> None: ) assert r.status_code == 201 user_token = Token.from_str(r.json()["token"]) - token_service = setup.factory.create_token_service() + token_service = factory.create_token_service() user_token_data = await token_service.get_data(user_token) assert user_token_data and user_token_data.expires == expires token_url = r.headers["Location"] @@ -668,18 +686,17 @@ async def test_no_expires(client: AsyncClient, setup: SetupTest) -> None: assert "expires" not in r.json() # Check that the expiration was also changed in Redis. - token_service = setup.factory.create_token_service() user_token_data = await token_service.get_data(user_token) assert user_token_data and user_token_data.expires is None @pytest.mark.asyncio async def test_duplicate_token_name( - client: AsyncClient, setup: SetupTest + client: AsyncClient, factory: ComponentFactory ) -> None: """Test duplicate token names.""" - token_data = await setup.create_session_token() - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory) + csrf = await set_session_cookie(client, token_data.token) r = await client.post( f"/auth/api/v1/users/{token_data.username}/tokens", @@ -714,10 +731,12 @@ async def test_duplicate_token_name( @pytest.mark.asyncio -async def test_bad_expires(client: AsyncClient, setup: SetupTest) -> None: +async def test_bad_expires( + client: AsyncClient, factory: ComponentFactory +) -> None: """Test creating or modifying a token with bogus expirations.""" - token_data = await setup.create_session_token() - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory) + csrf = await set_session_cookie(client, token_data.token) now = int(time.time()) bad_expires = [-now, -1, 0, now, now + (5 * 60) - 1] @@ -756,15 +775,15 @@ async def test_bad_expires(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio async def test_bad_scopes( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: """Test creating or modifying a token with bogus scopes.""" known_scopes = list(config.known_scopes.keys()) assert len(known_scopes) > 4 - token_data = await setup.create_session_token( - scopes=known_scopes[1:3] + ["other:scope", "user:token"] + token_data = await create_session_token( + factory, scopes=known_scopes[1:3] + ["other:scope", "user:token"] ) - csrf = await setup.login(client, token_data.token) + csrf = await set_session_cookie(client, token_data.token) # Check that we reject both an unknown scope and a scope that's present on # the session but isn't valid in the configuration. @@ -803,11 +822,11 @@ async def test_bad_scopes( @pytest.mark.asyncio async def test_create_admin( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: """Test creating a token through the admin interface.""" - token_data = await setup.create_session_token(scopes=["exec:admin"]) - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory, scopes=["exec:admin"]) + csrf = await set_session_cookie(client, token_data.token) r = await client.post( "/auth/api/v1/tokens", @@ -816,8 +835,8 @@ async def test_create_admin( ) assert r.status_code == 403 - token_data = await setup.create_session_token(scopes=["admin:token"]) - csrf = await setup.login(client, token_data.token) + token_data = await create_session_token(factory, scopes=["admin:token"]) + csrf = await set_session_cookie(client, token_data.token) now = datetime.now(tz=timezone.utc) expires = int((now + timedelta(days=2)).timestamp()) @@ -841,7 +860,7 @@ async def test_create_admin( token_url = f"/auth/api/v1/users/a-service/tokens/{service_token.key}" assert r.headers["Location"] == token_url - setup.logout(client) + clear_session_cookie(client) r = await client.get( "/auth/api/v1/token-info", headers={"Authorization": f"bearer {str(service_token)}"}, diff --git a/tests/handlers/auth_load_test.py b/tests/handlers/auth_load_test.py index 1bb2101f7..3fd498c46 100644 --- a/tests/handlers/auth_load_test.py +++ b/tests/handlers/auth_load_test.py @@ -13,17 +13,23 @@ import pytest from gafaelfawr.models.token import Token +from tests.support.cookies import set_session_cookie +from tests.support.tokens import create_session_token if TYPE_CHECKING: from httpx import AsyncClient - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: - data = await setup.create_session_token(scopes=["exec:test", "read:all"]) - await setup.login(client, data.token) +async def test_notebook( + client: AsyncClient, factory: ComponentFactory +) -> None: + data = await create_session_token( + factory, scopes=["exec:test", "read:all"] + ) + await set_session_cookie(client, data.token) request_awaits = [] for _ in range(100): @@ -41,9 +47,13 @@ async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_internal(client: AsyncClient, setup: SetupTest) -> None: - data = await setup.create_session_token(scopes=["exec:test", "read:all"]) - await setup.login(client, data.token) +async def test_internal( + client: AsyncClient, factory: ComponentFactory +) -> None: + data = await create_session_token( + factory, scopes=["exec:test", "read:all"] + ) + await set_session_cookie(client, data.token) request_awaits = [] for _ in range(100): diff --git a/tests/handlers/auth_logging_test.py b/tests/handlers/auth_logging_test.py index 2517ff24d..a35e0769c 100644 --- a/tests/handlers/auth_logging_test.py +++ b/tests/handlers/auth_logging_test.py @@ -8,19 +8,20 @@ import pytest from tests.support.logging import parse_log +from tests.support.tokens import create_session_token if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture from httpx import AsyncClient - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_success( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture ) -> None: - token_data = await setup.create_session_token(scopes=["exec:admin"]) + token_data = await create_session_token(factory, scopes=["exec:admin"]) # Successful request with X-Forwarded-For and a bearer token. caplog.clear() @@ -87,9 +88,9 @@ async def test_success( @pytest.mark.asyncio async def test_authorization_failed( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture ) -> None: - token_data = await setup.create_session_token(scopes=["exec:admin"]) + token_data = await create_session_token(factory, scopes=["exec:admin"]) caplog.clear() r = await client.get( @@ -123,9 +124,9 @@ async def test_authorization_failed( @pytest.mark.asyncio async def test_original_url( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture ) -> None: - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) caplog.clear() r = await client.get( @@ -173,9 +174,9 @@ async def test_original_url( @pytest.mark.asyncio async def test_chained_x_forwarded( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture ) -> None: - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) caplog.clear() r = await client.get( @@ -211,7 +212,7 @@ async def test_chained_x_forwarded( @pytest.mark.asyncio async def test_invalid_token( - client: AsyncClient, setup: SetupTest, caplog: LogCaptureFixture + client: AsyncClient, caplog: LogCaptureFixture ) -> None: caplog.clear() r = await client.get( diff --git a/tests/handlers/auth_test.py b/tests/handlers/auth_test.py index eca68c03b..5f910c718 100644 --- a/tests/handlers/auth_test.py +++ b/tests/handlers/auth_test.py @@ -14,18 +14,17 @@ assert_unauthorized_is_correct, parse_www_authenticate, ) +from tests.support.tokens import create_session_token if TYPE_CHECKING: from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_no_auth( - client: AsyncClient, config: Config, setup: SetupTest -) -> None: +async def test_no_auth(client: AsyncClient, config: Config) -> None: r = await client.get("/auth", params={"scope": "exec:admin"}) assert_unauthorized_is_correct(r, config) @@ -46,8 +45,9 @@ async def test_no_auth( @pytest.mark.asyncio -async def test_invalid(client: AsyncClient, setup: SetupTest) -> None: - token = await setup.create_session_token() +async def test_invalid(client: AsyncClient, factory: ComponentFactory) -> None: + token = await create_session_token(factory) + r = await client.get( "/auth", headers={"Authorization": f"bearer {token.token}"} ) @@ -72,9 +72,7 @@ async def test_invalid(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_invalid_auth( - client: AsyncClient, config: Config, setup: SetupTest -) -> None: +async def test_invalid_auth(client: AsyncClient, config: Config) -> None: r = await client.get( "/auth", params={"scope": "exec:admin"}, @@ -130,9 +128,9 @@ async def test_invalid_auth( @pytest.mark.asyncio async def test_access_denied( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) r = await client.get( "/auth", @@ -151,9 +149,7 @@ async def test_access_denied( @pytest.mark.asyncio -async def test_auth_forbidden( - client: AsyncClient, config: Config, setup: SetupTest -) -> None: +async def test_auth_forbidden(client: AsyncClient, config: Config) -> None: r = await client.get( "/auth/forbidden", params=[("scope", "exec:test"), ("scope", "exec:admin")], @@ -182,9 +178,9 @@ async def test_auth_forbidden( @pytest.mark.asyncio async def test_satisfy_all( - client: AsyncClient, config: Config, setup: SetupTest + client: AsyncClient, config: Config, factory: ComponentFactory ) -> None: - token_data = await setup.create_session_token(scopes=["exec:test"]) + token_data = await create_session_token(factory, scopes=["exec:test"]) r = await client.get( "/auth", @@ -203,9 +199,9 @@ async def test_satisfy_all( @pytest.mark.asyncio -async def test_success(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token( - group_names=["admin"], scopes=["exec:admin", "read:all"] +async def test_success(client: AsyncClient, factory: ComponentFactory) -> None: + token_data = await create_session_token( + factory, group_names=["admin"], scopes=["exec:admin", "read:all"] ) r = await client.get( @@ -228,12 +224,15 @@ async def test_success(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success_minimal(client: AsyncClient, setup: SetupTest) -> None: +async def test_success_minimal( + client: AsyncClient, factory: ComponentFactory +) -> None: user_info = TokenUserInfo(username="user", uid=1234) - token_service = setup.factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=["read:all"], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, scopes=["read:all"], ip_address="127.0.0.1" + ) r = await client.get( "/auth", @@ -251,9 +250,11 @@ async def test_success_minimal(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token( - group_names=["admin"], scopes=["exec:admin", "read:all"] +async def test_notebook( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token( + factory, group_names=["admin"], scopes=["exec:admin", "read:all"] ) assert token_data.expires @@ -292,9 +293,13 @@ async def test_notebook(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_internal(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token( - group_names=["admin"], scopes=["exec:admin", "read:all", "read:some"] +async def test_internal( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token( + factory, + group_names=["admin"], + scopes=["exec:admin", "read:all", "read:some"], ) assert token_data.expires @@ -342,8 +347,10 @@ async def test_internal(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_internal_errors(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token(scopes=["read:some"]) +async def test_internal_errors( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory, scopes=["read:some"]) # Delegating a token with a scope the original doesn't have will fail. r = await client.get( @@ -377,15 +384,17 @@ async def test_internal_errors(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_success_any(client: AsyncClient, setup: SetupTest) -> None: +async def test_success_any( + client: AsyncClient, factory: ComponentFactory +) -> None: """Test ``satisfy=any`` as an ``/auth`` parameter. Ask for either ``exec:admin`` or ``exec:test`` and pass in credentials with only ``exec:test``. Ensure they are accepted but also the headers don't claim the client has ``exec:admin``. """ - token_data = await setup.create_session_token( - group_names=["test"], scopes=["exec:test"] + token_data = await create_session_token( + factory, group_names=["test"], scopes=["exec:test"] ) r = await client.get( @@ -406,9 +415,9 @@ async def test_success_any(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_basic(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token( - group_names=["test"], scopes=["exec:admin"] +async def test_basic(client: AsyncClient, factory: ComponentFactory) -> None: + token_data = await create_session_token( + factory, group_names=["test"], scopes=["exec:admin"] ) basic = f"{token_data.token}:x-oauth-basic".encode() @@ -445,9 +454,7 @@ async def test_basic(client: AsyncClient, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_basic_failure( - client: AsyncClient, config: Config, setup: SetupTest -) -> None: +async def test_basic_failure(client: AsyncClient, config: Config) -> None: basic_b64 = base64.b64encode(b"bogus-string").decode() r = await client.get( "/auth", @@ -472,9 +479,7 @@ async def test_basic_failure( @pytest.mark.asyncio -async def test_ajax_unauthorized( - client: AsyncClient, config: Config, setup: SetupTest -) -> None: +async def test_ajax_unauthorized(client: AsyncClient, config: Config) -> None: """Test that AJAX requests without auth get 403, not 401.""" r = await client.get( "/auth", @@ -490,13 +495,14 @@ async def test_ajax_unauthorized( @pytest.mark.asyncio async def test_success_unicode_name( - client: AsyncClient, setup: SetupTest + client: AsyncClient, factory: ComponentFactory ) -> None: user_info = TokenUserInfo(username="user", uid=1234, name="名字") - token_service = setup.factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=["read:all"], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, scopes=["read:all"], ip_address="127.0.0.1" + ) r = await client.get( "/auth", diff --git a/tests/handlers/influxdb_test.py b/tests/handlers/influxdb_test.py index 8440eaf41..74884796e 100644 --- a/tests/handlers/influxdb_test.py +++ b/tests/handlers/influxdb_test.py @@ -11,6 +11,7 @@ from tests.support.headers import assert_unauthorized_is_correct from tests.support.logging import parse_log from tests.support.settings import configure +from tests.support.tokens import create_session_token if TYPE_CHECKING: from pathlib import Path @@ -19,17 +20,17 @@ from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_influxdb( client: AsyncClient, config: Config, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) assert token_data.expires influxdb_secret = config.issuer.influxdb_secret assert influxdb_secret @@ -80,11 +81,12 @@ async def test_no_auth(client: AsyncClient, config: Config) -> None: async def test_not_configured( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: - await configure(tmp_path, "oidc") - token_data = await setup.create_session_token() + config = await configure(tmp_path, "oidc") + factory.reconfigure(config) + token_data = await create_session_token(factory) caplog.clear() r = await client.get( @@ -115,11 +117,12 @@ async def test_not_configured( async def test_influxdb_force_username( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: config = await configure(tmp_path, "influxdb-username") - token_data = await setup.create_session_token() + factory.reconfigure(config) + token_data = await create_session_token(factory) assert token_data.expires influxdb_secret = config.issuer.influxdb_secret assert influxdb_secret diff --git a/tests/handlers/login_github_test.py b/tests/handlers/login_github_test.py index 2ba23bf39..84f5d2a81 100644 --- a/tests/handlers/login_github_test.py +++ b/tests/handlers/login_github_test.py @@ -24,7 +24,7 @@ from _pytest.logging import LogCaptureFixture from httpx import AsyncClient, Response - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory async def simulate_github_login( @@ -329,14 +329,14 @@ async def test_github_uppercase( @pytest.mark.asyncio async def test_github_admin( - client: AsyncClient, respx_mock: respx.Router, setup: SetupTest + client: AsyncClient, respx_mock: respx.Router, factory: ComponentFactory ) -> None: """Test that a token administrator gets the admin:token scope.""" - admin_service = setup.factory.create_admin_service() - await admin_service.add_admin( - "someuser", actor="admin", ip_address="127.0.0.1" - ) - await setup.session.commit() + admin_service = factory.create_admin_service() + async with factory.session.begin(): + await admin_service.add_admin( + "someuser", actor="admin", ip_address="127.0.0.1" + ) user_info = GitHubUserInfo( name="A User", username="someuser", diff --git a/tests/handlers/logout_test.py b/tests/handlers/logout_test.py index 9646e327d..3ab43ed2e 100644 --- a/tests/handlers/logout_test.py +++ b/tests/handlers/logout_test.py @@ -7,9 +7,11 @@ import pytest from gafaelfawr.providers.github import GitHubTeam, GitHubUserInfo +from tests.support.cookies import set_session_cookie from tests.support.github import mock_github from tests.support.headers import query_from_url from tests.support.logging import parse_log +from tests.support.tokens import create_session_token if TYPE_CHECKING: import respx @@ -17,18 +19,18 @@ from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_logout( client: AsyncClient, config: Config, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: - token_data = await setup.create_session_token(scopes=["read:all"]) - await setup.login(client, token_data.token) + token_data = await create_session_token(factory, scopes=["read:all"]) + await set_session_cookie(client, token_data.token) # Confirm that we're logged in. r = await client.get("/auth", params={"scope": "read:all"}) @@ -57,9 +59,11 @@ async def test_logout( @pytest.mark.asyncio -async def test_logout_with_url(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token(scopes=["read:all"]) - await setup.login(client, token_data.token) +async def test_logout_with_url( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory, scopes=["read:all"]) + await set_session_cookie(client, token_data.token) # Confirm that we're logged in. r = await client.get("/auth", params={"scope": "read:all"}) diff --git a/tests/handlers/oidc_test.py b/tests/handlers/oidc_test.py index baebd9d81..7a35e18f5 100644 --- a/tests/handlers/oidc_test.py +++ b/tests/handlers/oidc_test.py @@ -13,9 +13,11 @@ from gafaelfawr.auth import AuthError, AuthErrorChallenge, AuthType from gafaelfawr.config import OIDCClient from gafaelfawr.constants import ALGORITHM +from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.models.oidc import OIDCAuthorizationCode, OIDCToken from gafaelfawr.util import number_to_base64 from tests.support.constants import TEST_HOSTNAME +from tests.support.cookies import set_session_cookie from tests.support.headers import ( assert_unauthorized_is_correct, parse_www_authenticate, @@ -23,6 +25,7 @@ ) from tests.support.logging import parse_log from tests.support.settings import configure +from tests.support.tokens import create_session_token if TYPE_CHECKING: from pathlib import Path @@ -32,20 +35,21 @@ from httpx import AsyncClient from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio async def test_login( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] config = await configure(tmp_path, "github", oidc_clients=clients) - token_data = await setup.create_session_token() - await setup.login(client, token_data.token) + factory.reconfigure(config) + token_data = await create_session_token(factory) + await set_session_cookie(client, token_data.token) return_url = f"https://{TEST_HOSTNAME}:4444/foo?a=bar&b=baz" # Log in @@ -117,7 +121,7 @@ async def test_login( assert exp_seconds - 5 <= data["expires_in"] <= exp_seconds assert data["access_token"] == data["id_token"] - verifier = setup.factory.create_token_verifier() + verifier = factory.create_token_verifier() token = verifier.verify_internal_token(OIDCToken(encoded=data["id_token"])) assert token.claims == { "aud": config.issuer.aud, @@ -153,10 +157,7 @@ async def test_login( @pytest.mark.asyncio async def test_unauthenticated( - tmp_path: Path, - client: AsyncClient, - setup: SetupTest, - caplog: LogCaptureFixture, + tmp_path: Path, client: AsyncClient, caplog: LogCaptureFixture ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] await configure(tmp_path, "github", oidc_clients=clients) @@ -197,13 +198,14 @@ async def test_unauthenticated( async def test_login_errors( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] - await configure(tmp_path, "github", oidc_clients=clients) - token_data = await setup.create_session_token() - await setup.login(client, token_data.token) + config = await configure(tmp_path, "github", oidc_clients=clients) + factory.reconfigure(config) + token_data = await create_session_token(factory) + await set_session_cookie(client, token_data.token) # No parameters at all. r = await client.get("/auth/openid/login") @@ -313,17 +315,18 @@ async def test_login_errors( async def test_token_errors( tmp_path: Path, client: AsyncClient, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: clients = [ OIDCClient(client_id="some-id", client_secret="some-secret"), OIDCClient(client_id="other-id", client_secret="other-secret"), ] - await configure(tmp_path, "github", oidc_clients=clients) - token_data = await setup.create_session_token() + config = await configure(tmp_path, "github", oidc_clients=clients) + factory.reconfigure(config) + token_data = await create_session_token(factory) token = token_data.token - oidc_service = setup.factory.create_oidc_service() + oidc_service = factory.create_oidc_service() redirect_uri = f"https://{TEST_HOSTNAME}/app" code = await oidc_service.issue_code("some-id", redirect_uri, token) @@ -438,7 +441,8 @@ async def test_token_errors( assert log["error"] == f"Unknown authorization code {bogus_code.key}" # Corrupt stored data. - await setup.redis.set(bogus_code.key, "XXXXXXX") + redis = await redis_dependency() + await redis.set(bogus_code.key, "XXXXXXX") r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { @@ -466,10 +470,11 @@ async def test_token_errors( } # Delete the underlying token. - token_service = setup.factory.create_token_service() - await token_service.delete_token( - token.key, token_data, token_data.username, ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + await token_service.delete_token( + token.key, token_data, token_data.username, ip_address="127.0.0.1" + ) request["redirect_uri"] = redirect_uri r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 @@ -480,9 +485,11 @@ async def test_token_errors( @pytest.mark.asyncio -async def test_userinfo(client: AsyncClient, setup: SetupTest) -> None: - token_data = await setup.create_session_token() - issuer = setup.factory.create_token_issuer() +async def test_userinfo( + client: AsyncClient, factory: ComponentFactory +) -> None: + token_data = await create_session_token(factory) + issuer = factory.create_token_issuer() oidc_token = issuer.issue_token(token_data, jti="some-jti") r = await client.get( @@ -504,11 +511,11 @@ async def test_no_auth(client: AsyncClient, config: Config) -> None: async def test_invalid( client: AsyncClient, config: Config, - setup: SetupTest, + factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: - token_data = await setup.create_session_token() - issuer = setup.factory.create_token_issuer() + token_data = await create_session_token(factory) + issuer = factory.create_token_issuer() oidc_token = issuer.issue_token(token_data, jti="some-jti") caplog.clear() diff --git a/tests/issuer_test.py b/tests/issuer_test.py index 8bc923d3f..6902f1655 100644 --- a/tests/issuer_test.py +++ b/tests/issuer_test.py @@ -9,19 +9,21 @@ import pytest from tests.support.settings import configure +from tests.support.tokens import create_session_token if TYPE_CHECKING: from pathlib import Path - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_issue_token(tmp_path: Path, setup: SetupTest) -> None: +async def test_issue_token(tmp_path: Path, factory: ComponentFactory) -> None: config = await configure(tmp_path, "oidc") - issuer = setup.factory.create_token_issuer() + factory.reconfigure(config) + issuer = factory.create_token_issuer() - token_data = await setup.create_session_token() + token_data = await create_session_token(factory) oidc_token = issuer.issue_token(token_data, jti="new-jti", scope="openid") assert oidc_token.claims == { diff --git a/tests/services/admin_test.py b/tests/services/admin_test.py index 87f6d5c26..ae532cbfd 100644 --- a/tests/services/admin_test.py +++ b/tests/services/admin_test.py @@ -10,63 +10,80 @@ from gafaelfawr.models.admin import Admin if TYPE_CHECKING: - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_add(setup: SetupTest) -> None: - admin_service = setup.factory.create_admin_service() +async def test_add(factory: ComponentFactory) -> None: + admin_service = factory.create_admin_service() - assert await admin_service.get_admins() == [Admin(username="admin")] - - await admin_service.add_admin( - "example", actor="admin", ip_address="192.168.0.1" - ) - - assert await admin_service.get_admins() == [ - Admin(username="admin"), - Admin(username="example"), - ] - assert await admin_service.is_admin("example") - assert not await admin_service.is_admin("foo") + async with factory.session.begin(): + assert await admin_service.get_admins() == [Admin(username="admin")] + await admin_service.add_admin( + "example", actor="admin", ip_address="192.168.0.1" + ) - with pytest.raises(PermissionDeniedError): + async with factory.session.begin(): + assert await admin_service.get_admins() == [ + Admin(username="admin"), + Admin(username="example"), + ] + assert await admin_service.is_admin("example") + assert not await admin_service.is_admin("foo") + + async with factory.session.begin(): + with pytest.raises(PermissionDeniedError): + await admin_service.add_admin( + "foo", actor="bar", ip_address="127.0.0.1" + ) + + async with factory.session.begin(): await admin_service.add_admin( - "foo", actor="bar", ip_address="127.0.0.1" + "foo", actor="", ip_address="127.0.0.1" ) - await admin_service.add_admin( - "foo", actor="", ip_address="127.0.0.1" - ) - assert await admin_service.is_admin("foo") - assert not await admin_service.is_admin("") + async with factory.session.begin(): + assert await admin_service.is_admin("foo") + assert not await admin_service.is_admin("") @pytest.mark.asyncio -async def test_delete(setup: SetupTest) -> None: - admin_service = setup.factory.create_admin_service() +async def test_delete(factory: ComponentFactory) -> None: + admin_service = factory.create_admin_service() - assert await admin_service.get_admins() == [Admin(username="admin")] + async with factory.session.begin(): + assert await admin_service.get_admins() == [Admin(username="admin")] + + async with factory.session.begin(): + with pytest.raises(PermissionDeniedError): + await admin_service.delete_admin( + "admin", actor="admin", ip_address="127.0.0.1" + ) + + async with factory.session.begin(): + await admin_service.add_admin( + "example", actor="admin", ip_address="127.0.0.1" + ) - with pytest.raises(PermissionDeniedError): + async with factory.session.begin(): await admin_service.delete_admin( "admin", actor="admin", ip_address="127.0.0.1" ) - await admin_service.add_admin( - "example", actor="admin", ip_address="127.0.0.1" - ) - await admin_service.delete_admin( - "admin", actor="admin", ip_address="127.0.0.1" - ) - assert await admin_service.is_admin("example") - assert not await admin_service.is_admin("admin") - assert await admin_service.get_admins() == [Admin(username="example")] - - await admin_service.add_admin( - "other", actor="example", ip_address="127.0.0.1" - ) - await admin_service.delete_admin( - "other", actor="", ip_address="127.0.0.1" - ) - assert await admin_service.get_admins() == [Admin(username="example")] + async with factory.session.begin(): + assert await admin_service.is_admin("example") + assert not await admin_service.is_admin("admin") + assert await admin_service.get_admins() == [Admin(username="example")] + + async with factory.session.begin(): + await admin_service.add_admin( + "other", actor="example", ip_address="127.0.0.1" + ) + + async with factory.session.begin(): + await admin_service.delete_admin( + "other", actor="", ip_address="127.0.0.1" + ) + + async with factory.session.begin(): + assert await admin_service.get_admins() == [Admin(username="example")] diff --git a/tests/services/kubernetes_test.py b/tests/services/kubernetes_test.py index f7371f550..ec8cf8c94 100644 --- a/tests/services/kubernetes_test.py +++ b/tests/services/kubernetes_test.py @@ -39,8 +39,8 @@ from _pytest.logging import LogCaptureFixture + from gafaelfawr.factory import ComponentFactory from gafaelfawr.services.token import TokenService - from tests.support.setup import SetupTest TEST_SERVICE_TOKENS: List[Dict[str, Any]] = [ { @@ -106,9 +106,9 @@ async def token_data_from_secret( async def assert_kubernetes_secrets_are_correct( - setup: SetupTest, mock: MockKubernetesApi, is_fresh: bool = True + factory: ComponentFactory, mock: MockKubernetesApi, is_fresh: bool = True ) -> None: - token_service = setup.factory.create_token_service() + token_service = factory.create_token_service() # Get all of the GafaelfawrServiceToken custom objects. service_tokens = mock.get_all_objects_for_test("GafaelfawrServiceToken") @@ -165,14 +165,14 @@ async def assert_kubernetes_secrets_are_correct( @pytest.mark.asyncio async def test_create( - setup: SetupTest, + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi, caplog: LogCaptureFixture, ) -> None: create_test_service_tokens(mock_kubernetes) - kubernetes_service = setup.factory.create_kubernetes_service() + kubernetes_service = factory.create_kubernetes_service() await kubernetes_service.update_service_tokens() - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) service_token = mock_kubernetes.get_namespaced_custom_object( "gafaelfawr.lsst.io", @@ -248,13 +248,13 @@ async def test_create( @pytest.mark.asyncio async def test_modify( - setup: SetupTest, + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi, caplog: LogCaptureFixture, ) -> None: create_test_service_tokens(mock_kubernetes) - kubernetes_service = setup.factory.create_kubernetes_service() - token_service = setup.factory.create_token_service() + kubernetes_service = factory.create_kubernetes_service() + token_service = factory.create_token_service() # Valid secret but with a bogus token. secret = V1Secret( @@ -289,7 +289,7 @@ async def test_modify( # Update the secrets. This should replace both with fresh secrets. await kubernetes_service.update_service_tokens() - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) # Check the logging. assert parse_log(caplog) == [ @@ -322,15 +322,16 @@ async def test_modify( ] # Replace one secret with a valid token for the wrong service. - token = await token_service.create_token_from_admin_request( - AdminTokenRequest( - username="some-other-service", - token_type=TokenType.service, - scopes=["admin:token"], - ), - TokenData.internal_token(), - ip_address=None, - ) + async with factory.session.begin(): + token = await token_service.create_token_from_admin_request( + AdminTokenRequest( + username="some-other-service", + token_type=TokenType.service, + scopes=["admin:token"], + ), + TokenData.internal_token(), + ip_address=None, + ) secret = V1Secret( api_version="v1", kind="Secret", @@ -343,15 +344,16 @@ async def test_modify( ) # Replace the other token with a valid token with the wrong scopes. - token = await token_service.create_token_from_admin_request( - AdminTokenRequest( - username="nublado-hub", - token_type=TokenType.service, - scopes=["read:all"], - ), - TokenData.internal_token(), - ip_address=None, - ) + async with factory.session.begin(): + token = await token_service.create_token_from_admin_request( + AdminTokenRequest( + username="nublado-hub", + token_type=TokenType.service, + scopes=["read:all"], + ), + TokenData.internal_token(), + ip_address=None, + ) secret = V1Secret( api_version="v1", kind="Secret", @@ -360,11 +362,10 @@ async def test_modify( type="Opaque", ) mock_kubernetes.replace_namespaced_secret("gafaelfawr", "nublado2", secret) - await setup.session.commit() # Update the secrets. This should create new tokens for both. await kubernetes_service.update_service_tokens() - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) nublado_secret = mock_kubernetes.read_namespaced_secret( "gafaelfawr", "nublado2" ) @@ -382,10 +383,9 @@ async def test_modify( # Update the secrets. This should create a new token for the first secret # but not for the second. - await setup.session.commit() await kubernetes_service.update_service_tokens() await assert_kubernetes_secrets_are_correct( - setup, mock_kubernetes, is_fresh=False + factory, mock_kubernetes, is_fresh=False ) assert nublado_secret == mock_kubernetes.read_namespaced_secret( "gafaelfawr", "nublado2" @@ -394,7 +394,7 @@ async def test_modify( @pytest.mark.asyncio async def test_update_from_queue( - setup: SetupTest, mock_kubernetes: MockKubernetesApi + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi ) -> None: service_token: Dict[str, Any] = { "apiVersion": "gafaelfawr.lsst.io/v1alpha1", @@ -416,7 +416,7 @@ async def test_update_from_queue( "gafaelfawrservicetokens", service_token, ) - kubernetes_service = setup.factory.create_kubernetes_service() + kubernetes_service = factory.create_kubernetes_service() queue: Queue[WatchEvent] = Queue() queue.put( WatchEvent( @@ -429,7 +429,7 @@ async def test_update_from_queue( await kubernetes_service.update_service_tokens_from_queue( queue, exit_on_empty=True ) - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) assert queue.empty() service_token = mock_kubernetes.get_namespaced_custom_object( @@ -460,7 +460,7 @@ async def test_update_from_queue( await kubernetes_service.update_service_tokens_from_queue( queue, exit_on_empty=True ) - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) assert queue.empty() # Deletion does nothing, but shouldn't prompt an error. @@ -475,13 +475,13 @@ async def test_update_from_queue( await kubernetes_service.update_service_tokens_from_queue( queue, exit_on_empty=True ) - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) assert queue.empty() @pytest.mark.asyncio async def test_update_generation( - setup: SetupTest, mock_kubernetes: MockKubernetesApi + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi ) -> None: """Test that GafaelfawrServiceToken status changes don't trigger updates. @@ -514,7 +514,7 @@ async def test_update_generation( "gafaelfawrservicetokens", service_token, ) - kubernetes_service = setup.factory.create_kubernetes_service() + kubernetes_service = factory.create_kubernetes_service() queue: Queue[WatchEvent] = Queue() queue.put( WatchEvent( @@ -527,7 +527,7 @@ async def test_update_generation( await kubernetes_service.update_service_tokens_from_queue( queue, exit_on_empty=True ) - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) assert queue.empty() secret = mock_kubernetes.read_namespaced_secret( "gafaelfawr-secret", "mobu" @@ -593,12 +593,12 @@ async def test_update_generation( assert secret != mock_kubernetes.read_namespaced_secret( "gafaelfawr-secret", "mobu" ) - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) @pytest.mark.asyncio async def test_update_metadata( - setup: SetupTest, mock_kubernetes: MockKubernetesApi + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi ) -> None: """Test that Secret metadata is updated even if generation doesn't change. @@ -626,9 +626,9 @@ async def test_update_metadata( "gafaelfawrservicetokens", service_token, ) - kubernetes_service = setup.factory.create_kubernetes_service() + kubernetes_service = factory.create_kubernetes_service() await kubernetes_service.update_service_tokens() - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) secret = mock_kubernetes.read_namespaced_secret( "gafaelfawr-secret", "mobu" ) @@ -682,16 +682,16 @@ async def test_update_metadata( queue, exit_on_empty=True ) assert queue.empty() - await assert_kubernetes_secrets_are_correct(setup, mock_kubernetes) + await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes) @pytest.mark.asyncio async def test_errors_replace_read( - setup: SetupTest, mock_kubernetes: MockKubernetesApi + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi ) -> None: create_test_service_tokens(mock_kubernetes) - kubernetes_service = setup.factory.create_kubernetes_service() - token_service = setup.factory.create_token_service() + kubernetes_service = factory.create_kubernetes_service() + token_service = factory.create_token_service() # Create a secret that should exist but has an invalid token. secret = V1Secret( @@ -755,7 +755,7 @@ def error_callback_read(method: str, *args: Any) -> None: @pytest.mark.asyncio async def test_errors_scope( - setup: SetupTest, mock_kubernetes: MockKubernetesApi + factory: ComponentFactory, mock_kubernetes: MockKubernetesApi ) -> None: mock_kubernetes.create_namespaced_custom_object( "gafaelfawr.lsst.io", @@ -776,7 +776,7 @@ async def test_errors_scope( }, }, ) - kubernetes_service = setup.factory.create_kubernetes_service() + kubernetes_service = factory.create_kubernetes_service() await kubernetes_service.update_service_tokens() with pytest.raises(ApiException): diff --git a/tests/services/oidc_test.py b/tests/services/oidc_test.py index cf140a262..c0e3ff4ef 100644 --- a/tests/services/oidc_test.py +++ b/tests/services/oidc_test.py @@ -11,6 +11,7 @@ from cryptography.fernet import Fernet from gafaelfawr.config import OIDCClient +from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.exceptions import ( InvalidClientError, InvalidGrantError, @@ -18,19 +19,21 @@ ) from gafaelfawr.models.oidc import OIDCAuthorizationCode from tests.support.settings import configure +from tests.support.tokens import create_session_token if TYPE_CHECKING: from pathlib import Path - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_issue_code(tmp_path: Path, setup: SetupTest) -> None: +async def test_issue_code(tmp_path: Path, factory: ComponentFactory) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] config = await configure(tmp_path, "github", oidc_clients=clients) - oidc_service = setup.factory.create_oidc_service() - token_data = await setup.create_session_token() + factory.reconfigure(config) + oidc_service = factory.create_oidc_service() + token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" @@ -41,7 +44,8 @@ async def test_issue_code(tmp_path: Path, setup: SetupTest) -> None: await oidc_service.issue_code("unknown-client", redirect_uri, token) code = await oidc_service.issue_code("some-id", redirect_uri, token) - encrypted_code = await setup.redis.get(f"oidc:{code.key}") + redis = await redis_dependency() + encrypted_code = await redis.get(f"oidc:{code.key}") assert encrypted_code fernet = Fernet(config.session_secret.encode()) serialized_code = json.loads(fernet.decrypt(encrypted_code)) @@ -63,14 +67,15 @@ async def test_issue_code(tmp_path: Path, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_redeem_code(tmp_path: Path, setup: SetupTest) -> None: +async def test_redeem_code(tmp_path: Path, factory: ComponentFactory) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] config = await configure(tmp_path, "github", oidc_clients=clients) - oidc_service = setup.factory.create_oidc_service() - token_data = await setup.create_session_token() + factory.reconfigure(config) + oidc_service = factory.create_oidc_service() + token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" code = await oidc_service.issue_code("client-2", redirect_uri, token) @@ -92,18 +97,22 @@ async def test_redeem_code(tmp_path: Path, setup: SetupTest) -> None: config.issuer.uid_claim: token_data.uid, } - assert not await setup.redis.get(f"oidc:{code.key}") + redis = await redis_dependency() + assert not await redis.get(f"oidc:{code.key}") @pytest.mark.asyncio -async def test_redeem_code_errors(tmp_path: Path, setup: SetupTest) -> None: +async def test_redeem_code_errors( + tmp_path: Path, factory: ComponentFactory +) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] - await configure(tmp_path, "github", oidc_clients=clients) - oidc_service = setup.factory.create_oidc_service() - token_data = await setup.create_session_token() + config = await configure(tmp_path, "github", oidc_clients=clients) + factory.reconfigure(config) + oidc_service = factory.create_oidc_service() + token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" code = await oidc_service.issue_code("client-2", redirect_uri, token) diff --git a/tests/services/token_cache_test.py b/tests/services/token_cache_test.py index 9177018bb..211a2c756 100644 --- a/tests/services/token_cache_test.py +++ b/tests/services/token_cache_test.py @@ -6,28 +6,32 @@ from typing import TYPE_CHECKING import pytest +import structlog +from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.models.token import Token, TokenData, TokenType from gafaelfawr.storage.base import RedisStorage from gafaelfawr.storage.token import TokenRedisStore from gafaelfawr.util import current_datetime +from tests.support.tokens import create_session_token if TYPE_CHECKING: from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_basic(setup: SetupTest) -> None: - token_data = await setup.create_session_token(scopes=["read:all"]) - token_service = setup.factory.create_token_service() - token_cache = setup.factory.create_token_cache_service() - internal_token = await token_service.get_internal_token( - token_data, "some-service", ["read:all"], ip_address="127.0.0.1" - ) - notebook_token = await token_service.get_notebook_token( - token_data, ip_address="127.0.0.1" - ) +async def test_basic(factory: ComponentFactory) -> None: + token_data = await create_session_token(factory, scopes=["read:all"]) + token_service = factory.create_token_service() + token_cache = factory.create_token_cache_service() + async with factory.session.begin(): + internal_token = await token_service.get_internal_token( + token_data, "some-service", ["read:all"], ip_address="127.0.0.1" + ) + notebook_token = await token_service.get_notebook_token( + token_data, ip_address="127.0.0.1" + ) assert internal_token == await token_cache.get_internal_token( token_data, "some-service", ["read:all"], "127.0.0.1" @@ -37,22 +41,24 @@ async def test_basic(setup: SetupTest) -> None: ) # Requesting different internal tokens doesn't work. - assert internal_token != await token_cache.get_internal_token( - token_data, "other-service", ["read:all"], "127.0.0.1" - ) - assert notebook_token != await token_cache.get_internal_token( - token_data, "some-service", [], "127.0.0.1" - ) + async with factory.session.begin(): + assert internal_token != await token_cache.get_internal_token( + token_data, "other-service", ["read:all"], "127.0.0.1" + ) + assert notebook_token != await token_cache.get_internal_token( + token_data, "some-service", [], "127.0.0.1" + ) # A different service token for the same user requesting the same # information creates a different internal token. - new_token_data = await setup.create_session_token(scopes=["read:all"]) - assert internal_token != await token_cache.get_internal_token( - new_token_data, "some-service", ["read:all"], "127.0.0.1" - ) - assert notebook_token != await token_cache.get_notebook_token( - new_token_data, "127.0.0.1" - ) + new_token_data = await create_session_token(factory, scopes=["read:all"]) + async with factory.session.begin(): + assert internal_token != await token_cache.get_internal_token( + new_token_data, "some-service", ["read:all"], "127.0.0.1" + ) + assert notebook_token != await token_cache.get_notebook_token( + new_token_data, "127.0.0.1" + ) # Changing the scope of the parent token doesn't matter as long as the # internal token is requested with the same scope. Cases where the parent @@ -62,16 +68,17 @@ async def test_basic(setup: SetupTest) -> None: assert internal_token == await token_cache.get_internal_token( token_data, "some-service", ["read:all"], "127.0.0.1" ) - assert internal_token != await token_cache.get_internal_token( - token_data, "some-service", ["admin:token"], "127.0.0.1" - ) + async with factory.session.begin(): + assert internal_token != await token_cache.get_internal_token( + token_data, "some-service", ["admin:token"], "127.0.0.1" + ) @pytest.mark.asyncio -async def test_invalid(setup: SetupTest) -> None: +async def test_invalid(factory: ComponentFactory) -> None: """Invalid tokens should not be returned even if cached.""" - token_data = await setup.create_session_token(scopes=["read:all"]) - token_cache = setup.factory.create_token_cache_service() + token_data = await create_session_token(factory, scopes=["read:all"]) + token_cache = factory.create_token_cache_service() internal_token = Token() notebook_token = Token() @@ -89,14 +96,16 @@ async def test_invalid(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_expiration(config: Config, setup: SetupTest) -> None: +async def test_expiration(config: Config, factory: ComponentFactory) -> None: """The cache is valid until half the lifetime of the child token.""" - token_data = await setup.create_session_token(scopes=["read:all"]) + token_data = await create_session_token(factory, scopes=["read:all"]) lifetime = config.token_lifetime now = current_datetime() - storage = RedisStorage(TokenData, config.session_secret, setup.redis) - token_store = TokenRedisStore(storage, setup.logger) - token_cache = setup.factory.create_token_cache_service() + redis = await redis_dependency() + logger = structlog.get_logger(config.safir.logger_name) + storage = RedisStorage(TokenData, config.session_secret, redis) + token_store = TokenRedisStore(storage, logger) + token_cache = factory.create_token_cache_service() # Store a token whose expiration is five seconds more than half the # typical token lifetime in the future and cache that token as an internal @@ -128,9 +137,11 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: await token_store.store_data(internal_token_data) # The cache should now decline to return the token and generate a new one. - assert internal_token_data.token != await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" - ) + old_token = internal_token_data.token + async with factory.session.begin(): + assert old_token != await token_cache.get_internal_token( + token_data, "some-service", ["read:all"], "127.0.0.1" + ) # Do the same test with a notebook token. notebook_token_data = TokenData( @@ -148,6 +159,8 @@ async def test_expiration(config: Config, setup: SetupTest) -> None: ) notebook_token_data.expires = expires - timedelta(seconds=20) await token_store.store_data(notebook_token_data) - assert notebook_token_data.token != await token_cache.get_notebook_token( - token_data, "127.0.0.1" - ) + old_token = notebook_token_data.token + async with factory.session.begin(): + assert old_token != await token_cache.get_notebook_token( + token_data, "127.0.0.1" + ) diff --git a/tests/services/token_test.py b/tests/services/token_test.py index 264b9345a..a1919be46 100644 --- a/tests/services/token_test.py +++ b/tests/services/token_test.py @@ -10,6 +10,7 @@ from cryptography.fernet import Fernet from pydantic import ValidationError +from gafaelfawr.dependencies.redis import redis_dependency from gafaelfawr.exceptions import ( InvalidExpiresError, InvalidScopesError, @@ -26,16 +27,19 @@ TokenUserInfo, ) from gafaelfawr.util import current_datetime +from tests.support.tokens import create_session_token from tests.support.util import assert_is_now if TYPE_CHECKING: from gafaelfawr.config import Config - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory @pytest.mark.asyncio -async def test_session_token(config: Config, setup: SetupTest) -> None: - token_service = setup.factory.create_token_service() +async def test_session_token( + config: Config, factory: ComponentFactory +) -> None: + token_service = factory.create_token_service() user_info = TokenUserInfo( username="example", name="Example Person", @@ -46,9 +50,10 @@ async def test_session_token(config: Config, setup: SetupTest) -> None: ], ) - token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" - ) + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, scopes=["user:token"], ip_address="127.0.0.1" + ) data = await token_service.get_data(token) assert data and data == TokenData( token=token, @@ -68,7 +73,8 @@ async def test_session_token(config: Config, setup: SetupTest) -> None: expires = data.created + timedelta(minutes=config.issuer.exp_minutes) assert data.expires == expires - info = await token_service.get_token_info_unchecked(token.key) + async with factory.session.begin(): + info = await token_service.get_token_info_unchecked(token.key) assert info and info == TokenInfo( token=token.key, username=user_info.username, @@ -82,9 +88,10 @@ async def test_session_token(config: Config, setup: SetupTest) -> None: ) assert await token_service.get_user_info(token) == user_info - history = await token_service.get_change_history( - data, token=token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, @@ -100,42 +107,47 @@ async def test_session_token(config: Config, setup: SetupTest) -> None: ] # Test a session token with scopes. - token = await token_service.create_session_token( - user_info, scopes=["read:all", "exec:admin"], ip_address="127.0.0.1" - ) - data = await token_service.get_data(token) - assert data and data.scopes == ["exec:admin", "read:all"] - info = await token_service.get_token_info_unchecked(token.key) - assert info and info.scopes == ["exec:admin", "read:all"] + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, + scopes=["read:all", "exec:admin"], + ip_address="127.0.0.1", + ) + data = await token_service.get_data(token) + assert data and data.scopes == ["exec:admin", "read:all"] + info = await token_service.get_token_info_unchecked(token.key) + assert info and info.scopes == ["exec:admin", "read:all"] @pytest.mark.asyncio -async def test_user_token(setup: SetupTest) -> None: +async def test_user_token(factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137 ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, - scopes=["read:all", "exec:admin", "user:token"], - ip_address="127.0.0.1", - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "exec:admin", "user:token"], + ip_address="127.0.0.1", + ) data = await token_service.get_data(session_token) assert data expires = current_datetime() + timedelta(days=2) # Scopes are provided not in sorted order to ensure they're sorted when # creating the token. - user_token = await token_service.create_user_token( - data, - "example", - token_name="some-token", - scopes=["read:all", "exec:admin"], - expires=expires, - ip_address="192.168.0.1", - ) - assert await token_service.get_user_info(user_token) == user_info - info = await token_service.get_token_info_unchecked(user_token.key) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + "example", + token_name="some-token", + scopes=["read:all", "exec:admin"], + expires=expires, + ip_address="192.168.0.1", + ) + assert await token_service.get_user_info(user_token) == user_info + info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username=user_info.username, @@ -159,9 +171,10 @@ async def test_user_token(setup: SetupTest) -> None: uid=user_info.uid, ) - history = await token_service.get_change_history( - data, token=user_token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=user_token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=user_token.key, @@ -179,25 +192,31 @@ async def test_user_token(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_notebook_token(config: Config, setup: SetupTest) -> None: +async def test_notebook_token( + config: Config, factory: ComponentFactory +) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, - scopes=["read:all", "exec:admin", "user:token"], - ip_address="127.0.0.1", - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "exec:admin", "user:token"], + ip_address="127.0.0.1", + ) data = await token_service.get_data(session_token) assert data - token = await token_service.get_notebook_token(data, ip_address="1.0.0.1") - assert await token_service.get_user_info(token) == user_info - info = await token_service.get_token_info_unchecked(token.key) + async with factory.session.begin(): + token = await token_service.get_notebook_token( + data, ip_address="1.0.0.1" + ) + assert await token_service.get_user_info(token) == user_info + info = await token_service.get_token_info_unchecked(token.key) assert info and info == TokenInfo( token=token.key, username=user_info.username, @@ -230,14 +249,16 @@ async def test_notebook_token(config: Config, setup: SetupTest) -> None: # Try again with the cache cleared to force a database lookup. await token_service._token_cache.clear() - new_token = await token_service.get_notebook_token( - data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + new_token = await token_service.get_notebook_token( + data, ip_address="127.0.0.1" + ) assert token == new_token - history = await token_service.get_change_history( - data, token=token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, @@ -273,63 +294,70 @@ async def test_notebook_token(config: Config, setup: SetupTest) -> None: groups=data.groups, ) await token_service._token_redis_store.store_data(notebook_token_data) - await token_service._token_db_store.add( - notebook_token_data, parent=data.token.key - ) - await setup.session.flush() + async with factory.session.begin(): + await token_service._token_db_store.add( + notebook_token_data, parent=data.token.key + ) await token_service._token_cache.clear() - dup_notebook_token = await token_service.get_notebook_token( - data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + dup_notebook_token = await token_service.get_notebook_token( + data, ip_address="127.0.0.1" + ) assert dup_notebook_token in (token, second_token) # Check that the expiration time is capped by creating a user token that # doesn't expire and then creating a notebook token from it. - user_token = await token_service.create_user_token( - data, - data.username, - token_name="some token", - scopes=[], - expires=None, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + data.username, + token_name="some token", + scopes=[], + expires=None, + ip_address="127.0.0.1", + ) data = await token_service.get_data(user_token) assert data - new_token = await token_service.get_notebook_token( - data, ip_address="127.0.0.1" - ) - assert new_token != token - info = await token_service.get_token_info_unchecked(new_token.key) + async with factory.session.begin(): + new_token = await token_service.get_notebook_token( + data, ip_address="127.0.0.1" + ) + assert new_token != token + info = await token_service.get_token_info_unchecked(new_token.key) assert info expires = info.created + timedelta(minutes=config.issuer.exp_minutes) assert info.expires == expires @pytest.mark.asyncio -async def test_internal_token(config: Config, setup: SetupTest) -> None: +async def test_internal_token( + config: Config, factory: ComponentFactory +) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, - scopes=["read:all", "exec:admin", "user:token"], - ip_address="127.0.0.1", - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "exec:admin", "user:token"], + ip_address="127.0.0.1", + ) data = await token_service.get_data(session_token) assert data - internal_token = await token_service.get_internal_token( - data, - service="some-service", - scopes=["read:all"], - ip_address="2001:db8::45", - ) - assert await token_service.get_user_info(internal_token) == user_info - info = await token_service.get_token_info_unchecked(internal_token.key) + async with factory.session.begin(): + internal_token = await token_service.get_internal_token( + data, + service="some-service", + scopes=["read:all"], + ip_address="2001:db8::45", + ) + assert await token_service.get_user_info(internal_token) == user_info + info = await token_service.get_token_info_unchecked(internal_token.key) assert info and info == TokenInfo( token=internal_token.key, username=user_info.username, @@ -364,17 +392,19 @@ async def test_internal_token(config: Config, setup: SetupTest) -> None: # Try again with the cache cleared to force a database lookup. await token_service._token_cache.clear() - new_internal_token = await token_service.get_internal_token( - data, - service="some-service", - scopes=["read:all"], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + new_internal_token = await token_service.get_internal_token( + data, + service="some-service", + scopes=["read:all"], + ip_address="127.0.0.1", + ) assert internal_token == new_internal_token - history = await token_service.get_change_history( - data, token=internal_token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=internal_token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=internal_token.key, @@ -413,63 +443,72 @@ async def test_internal_token(config: Config, setup: SetupTest) -> None: groups=data.groups, ) await token_service._token_redis_store.store_data(internal_token_data) - await token_service._token_db_store.add( - internal_token_data, service="some-service", parent=data.token.key - ) - await setup.session.flush() + async with factory.session.begin(): + await token_service._token_db_store.add( + internal_token_data, service="some-service", parent=data.token.key + ) await token_service._token_cache.clear() - dup_internal_token = await token_service.get_internal_token( - data, - service="some-service", - scopes=["read:all"], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + dup_internal_token = await token_service.get_internal_token( + data, + service="some-service", + scopes=["read:all"], + ip_address="127.0.0.1", + ) assert dup_internal_token in (internal_token, second_internal_token) # A different scope or a different service results in a new token. - new_internal_token = await token_service.get_internal_token( - data, - service="some-service", - scopes=["exec:admin"], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + new_internal_token = await token_service.get_internal_token( + data, + service="some-service", + scopes=["exec:admin"], + ip_address="127.0.0.1", + ) assert internal_token != new_internal_token - new_internal_token = await token_service.get_internal_token( - data, - service="another-service", - scopes=["read:all"], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + new_internal_token = await token_service.get_internal_token( + data, + service="another-service", + scopes=["read:all"], + ip_address="127.0.0.1", + ) assert internal_token != new_internal_token # Check that the expiration time is capped by creating a user token that # doesn't expire and then creating a notebook token from it. Use this to # test a token with empty scopes. - user_token = await token_service.create_user_token( - data, - data.username, - token_name="some token", - scopes=["exec:admin"], - expires=None, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + data.username, + token_name="some token", + scopes=["exec:admin"], + expires=None, + ip_address="127.0.0.1", + ) data = await token_service.get_data(user_token) assert data - new_internal_token = await token_service.get_internal_token( - data, service="some-service", scopes=[], ip_address="127.0.0.1" - ) - assert new_internal_token != internal_token - info = await token_service.get_token_info_unchecked(new_internal_token.key) + async with factory.session.begin(): + new_internal_token = await token_service.get_internal_token( + data, service="some-service", scopes=[], ip_address="127.0.0.1" + ) + assert new_internal_token != internal_token + info = await token_service.get_token_info_unchecked( + new_internal_token.key + ) assert info and info.scopes == [] expires = info.created + timedelta(minutes=config.issuer.exp_minutes) assert info.expires == expires @pytest.mark.asyncio -async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: +async def test_child_token_lifetime( + config: Config, factory: ComponentFactory +) -> None: """Test that a new internal token is generated at half its lifetime.""" - session_token_data = await setup.create_session_token() - token_service = setup.factory.create_token_service() + session_token_data = await create_session_token(factory) + token_service = factory.create_token_service() # Generate a user token with a lifetime less than half of the default # lifetime for an internal token. This will get us a short-lived internal @@ -477,21 +516,23 @@ async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: # doesn't expire. delta = timedelta(minutes=(config.issuer.exp_minutes / 2) - 5) expires = current_datetime() + delta - user_token = await token_service.create_user_token( - session_token_data, - session_token_data.username, - token_name="n", - expires=expires, - scopes=[], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + session_token_data, + session_token_data.username, + token_name="n", + expires=expires, + scopes=[], + ip_address="127.0.0.1", + ) user_token_data = await token_service.get_data(user_token) assert user_token_data # Get an internal token and ensure we get the same one when we ask again. - internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" - ) + async with factory.session.begin(): + internal_token = await token_service.get_internal_token( + user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + ) internal_token_data = await token_service.get_data(internal_token) assert internal_token_data assert internal_token_data.expires == user_token_data.expires @@ -501,9 +542,10 @@ async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: assert new_internal_token == internal_token # Do the same thing with a notebook token. - notebook_token = await token_service.get_notebook_token( - user_token_data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + notebook_token = await token_service.get_notebook_token( + user_token_data, ip_address="127.0.0.1" + ) notebook_token_data = await token_service.get_data(notebook_token) assert notebook_token_data assert notebook_token_data.expires == user_token_data.expires @@ -516,30 +558,33 @@ async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: # internal token lifetime. new_delta = timedelta(minutes=config.issuer.exp_minutes * 2) expires = current_datetime() + new_delta - assert await token_service.modify_token( - user_token.key, - session_token_data, - session_token_data.username, - ip_address="127.0.0.1", - expires=expires, - ) + async with factory.session.begin(): + assert await token_service.modify_token( + user_token.key, + session_token_data, + session_token_data.username, + ip_address="127.0.0.1", + expires=expires, + ) user_token_data = await token_service.get_data(user_token) assert user_token_data # Now, request an internal and notebook token. We should get different # ones with a longer expiration. - new_internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" - ) + async with factory.session.begin(): + new_internal_token = await token_service.get_internal_token( + user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + ) assert new_internal_token != internal_token internal_token = new_internal_token internal_token_data = await token_service.get_data(internal_token) assert internal_token_data delta = timedelta(minutes=config.issuer.exp_minutes) assert internal_token_data.expires == internal_token_data.created + delta - new_notebook_token = await token_service.get_notebook_token( - user_token_data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + new_notebook_token = await token_service.get_notebook_token( + user_token_data, ip_address="127.0.0.1" + ) assert new_notebook_token != notebook_token notebook_token = new_notebook_token notebook_token_data = await token_service.get_data(notebook_token) @@ -547,14 +592,15 @@ async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: assert notebook_token_data.expires == notebook_token_data.created + delta # Change the expiration of the user token to no longer expire. - assert await token_service.modify_token( - user_token.key, - session_token_data, - session_token_data.username, - ip_address="127.0.0.1", - expires=None, - no_expire=True, - ) + async with factory.session.begin(): + assert await token_service.modify_token( + user_token.key, + session_token_data, + session_token_data.username, + ip_address="127.0.0.1", + expires=None, + no_expire=True, + ) user_token_data = await token_service.get_data(user_token) assert user_token_data @@ -571,14 +617,15 @@ async def test_child_token_lifetime(config: Config, setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_token_from_admin_request(setup: SetupTest) -> None: +async def test_token_from_admin_request(factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137 ) - token_service = setup.factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=[], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, scopes=[], ip_address="127.0.0.1" + ) data = await token_service.get_data(token) assert data expires = current_datetime() + timedelta(days=2) @@ -601,9 +648,10 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: ) # Get a token with an appropriate scope. - session_token = await token_service.create_session_token( - user_info, scopes=["admin:token"], ip_address="127.0.0.1" - ) + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, scopes=["admin:token"], ip_address="127.0.0.1" + ) admin_data = await token_service.get_data(session_token) assert admin_data @@ -622,18 +670,20 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: request.expires = expires # Try a successful request. - token = await token_service.create_token_from_admin_request( - request, admin_data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + token = await token_service.create_token_from_admin_request( + request, admin_data, ip_address="127.0.0.1" + ) user_data = await token_service.get_data(token) assert user_data and user_data == TokenData( token=token, created=user_data.created, **request.dict() ) assert_is_now(user_data.created) - history = await token_service.get_change_history( - admin_data, token=token.key, username=request.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + admin_data, token=token.key, username=request.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, @@ -659,18 +709,20 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: request = AdminTokenRequest( username="service", token_type=TokenType.service ) - token = await token_service.create_token_from_admin_request( - request, admin_data, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + token = await token_service.create_token_from_admin_request( + request, admin_data, ip_address="127.0.0.1" + ) service_data = await token_service.get_data(token) assert service_data and service_data == TokenData( token=token, created=service_data.created, **request.dict() ) assert_is_now(service_data.created) - history = await token_service.get_change_history( - admin_data, token=token.key, username=request.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + admin_data, token=token.key, username=request.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, @@ -687,57 +739,60 @@ async def test_token_from_admin_request(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_list(setup: SetupTest) -> None: +async def test_list(factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137 ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, scopes=["user:token"], ip_address="127.0.0.1" + ) data = await token_service.get_data(session_token) assert data - user_token = await token_service.create_user_token( - data, - data.username, - token_name="some-token", - scopes=[], - ip_address="127.0.0.1", - ) - other_user_info = TokenUserInfo( - username="other", name="Other Person", uid=1313 - ) - other_session_token = await token_service.create_session_token( - other_user_info, scopes=["admin:token"], ip_address="1.1.1.1" - ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + data.username, + token_name="some-token", + scopes=[], + ip_address="127.0.0.1", + ) + other_user_info = TokenUserInfo( + username="other", name="Other Person", uid=1313 + ) + other_session_token = await token_service.create_session_token( + other_user_info, scopes=["admin:token"], ip_address="1.1.1.1" + ) admin_data = await token_service.get_data(other_session_token) assert admin_data - session_info = await token_service.get_token_info_unchecked( - session_token.key - ) - assert session_info - user_token_info = await token_service.get_token_info_unchecked( - user_token.key - ) - assert user_token_info - other_session_info = await token_service.get_token_info_unchecked( - other_session_token.key - ) - assert other_session_info - assert await token_service.list_tokens(data, "example") == sorted( - sorted((session_info, user_token_info), key=lambda t: t.token), - key=lambda t: t.created, - reverse=True, - ) - assert await token_service.list_tokens(admin_data) == sorted( - sorted( - (session_info, other_session_info, user_token_info), - key=lambda t: t.token, - ), - key=lambda t: t.created, - reverse=True, - ) + async with factory.session.begin(): + session_info = await token_service.get_token_info_unchecked( + session_token.key + ) + assert session_info + user_token_info = await token_service.get_token_info_unchecked( + user_token.key + ) + assert user_token_info + other_session_info = await token_service.get_token_info_unchecked( + other_session_token.key + ) + assert other_session_info + assert await token_service.list_tokens(data, "example") == sorted( + sorted((session_info, user_token_info), key=lambda t: t.token), + key=lambda t: t.created, + reverse=True, + ) + assert await token_service.list_tokens(admin_data) == sorted( + sorted( + (session_info, other_session_info, user_token_info), + key=lambda t: t.token, + ), + key=lambda t: t.created, + reverse=True, + ) # Regular users can't retrieve all tokens. with pytest.raises(PermissionDeniedError): @@ -745,36 +800,44 @@ async def test_list(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_modify(setup: SetupTest) -> None: +async def test_modify(factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="example", name="Example Person", uid=4137 ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, scopes=["read:all", "user:token"], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "user:token"], + ip_address="127.0.0.1", + ) data = await token_service.get_data(session_token) assert data - user_token = await token_service.create_user_token( - data, - data.username, - token_name="some-token", - scopes=[], - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + data, + data.username, + token_name="some-token", + scopes=[], + ip_address="127.0.0.1", + ) expires = current_datetime() + timedelta(days=50) - await token_service.modify_token( - user_token.key, data, token_name="happy token", ip_address="127.0.0.1" - ) - await token_service.modify_token( - user_token.key, - data, - scopes=["read:all"], - expires=expires, - ip_address="192.168.0.4", - ) - info = await token_service.get_token_info_unchecked(user_token.key) + async with factory.session.begin(): + await token_service.modify_token( + user_token.key, + data, + token_name="happy token", + ip_address="127.0.0.1", + ) + await token_service.modify_token( + user_token.key, + data, + scopes=["read:all"], + expires=expires, + ip_address="192.168.0.4", + ) + info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username="example", @@ -786,17 +849,19 @@ async def test_modify(setup: SetupTest) -> None: last_used=None, parent=None, ) - await token_service.modify_token( - user_token.key, - data, - expires=None, - no_expire=True, - ip_address="127.0.4.5", - ) + async with factory.session.begin(): + await token_service.modify_token( + user_token.key, + data, + expires=None, + no_expire=True, + ip_address="127.0.4.5", + ) - history = await token_service.get_change_history( - data, token=user_token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=user_token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=user_token.key, @@ -854,32 +919,37 @@ async def test_modify(setup: SetupTest) -> None: @pytest.mark.asyncio -async def test_delete(setup: SetupTest) -> None: - data = await setup.create_session_token() - token_service = setup.factory.create_token_service() - token = await token_service.create_user_token( - data, - data.username, - token_name="some token", - scopes=[], - ip_address="127.0.0.1", - ) +async def test_delete(factory: ComponentFactory) -> None: + data = await create_session_token(factory) + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_user_token( + data, + data.username, + token_name="some token", + scopes=[], + ip_address="127.0.0.1", + ) - assert await token_service.delete_token( - token.key, data, data.username, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + assert await token_service.delete_token( + token.key, data, data.username, ip_address="127.0.0.1" + ) assert await token_service.get_data(token) is None - assert await token_service.get_token_info_unchecked(token.key) is None - assert await token_service.get_user_info(token) is None + async with factory.session.begin(): + assert await token_service.get_token_info_unchecked(token.key) is None + assert await token_service.get_user_info(token) is None - assert not await token_service.delete_token( - token.key, data, data.username, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + assert not await token_service.delete_token( + token.key, data, data.username, ip_address="127.0.0.1" + ) - history = await token_service.get_change_history( - data, token=token.key, username=data.username - ) + async with factory.session.begin(): + history = await token_service.get_change_history( + data, token=token.key, username=data.username + ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, @@ -908,112 +978,130 @@ async def test_delete(setup: SetupTest) -> None: ] # Cannot delete someone else's token. - token = await token_service.create_user_token( - data, - data.username, - token_name="some token", - scopes=[], - ip_address="127.0.0.1", - ) - other_data = await setup.create_session_token(username="other") - with pytest.raises(PermissionDeniedError): - await token_service.delete_token( - token.key, other_data, data.username, ip_address="127.0.0.1" + async with factory.session.begin(): + token = await token_service.create_user_token( + data, + data.username, + token_name="some token", + scopes=[], + ip_address="127.0.0.1", ) + other_data = await create_session_token(factory, username="other") + async with factory.session.begin(): + with pytest.raises(PermissionDeniedError): + await token_service.delete_token( + token.key, other_data, data.username, ip_address="127.0.0.1" + ) # Admins can delete soemone else's token. - admin_data = await setup.create_session_token( - username="admin", scopes=["admin:token"] + admin_data = await create_session_token( + factory, username="admin", scopes=["admin:token"] ) assert await token_service.get_data(token) - assert await token_service.delete_token( - token.key, admin_data, data.username, ip_address="127.0.0.1" - ) + async with factory.session.begin(): + assert await token_service.delete_token( + token.key, admin_data, data.username, ip_address="127.0.0.1" + ) assert await token_service.get_data(token) is None @pytest.mark.asyncio -async def test_delete_cascade(setup: SetupTest) -> None: +async def test_delete_cascade(factory: ComponentFactory) -> None: """Test that deleting a token cascades to child tokens.""" - token_service = setup.factory.create_token_service() - session_token_data = await setup.create_session_token( - scopes=["admin:token", "read:all", "user:token"] - ) - user_token = await token_service.create_user_token( - session_token_data, - session_token_data.username, - token_name="user-token", - scopes=["user:token"], - ip_address="127.0.0.1", - ) - user_token_data = await token_service.get_data(user_token) - assert user_token_data - admin_request = AdminTokenRequest( - username="service", - token_type=TokenType.service, - scopes=["read:all", "user:token"], - name="Some Service", - ) - service_token = await token_service.create_token_from_admin_request( - admin_request, session_token_data, ip_address="127.0.0.1" - ) - service_token_data = await token_service.get_data(service_token) - assert service_token_data + token_service = factory.create_token_service() + session_token_data = await create_session_token( + factory, scopes=["admin:token", "read:all", "user:token"] + ) + async with factory.session.begin(): + user_token = await token_service.create_user_token( + session_token_data, + session_token_data.username, + token_name="user-token", + scopes=["user:token"], + ip_address="127.0.0.1", + ) + user_token_data = await token_service.get_data(user_token) + assert user_token_data + admin_request = AdminTokenRequest( + username="service", + token_type=TokenType.service, + scopes=["read:all", "user:token"], + name="Some Service", + ) + service_token = await token_service.create_token_from_admin_request( + admin_request, session_token_data, ip_address="127.0.0.1" + ) + service_token_data = await token_service.get_data(service_token) + assert service_token_data # Build a tree of tokens hung off of the session token. - notebook_token = await token_service.get_notebook_token( - session_token_data, ip_address="127.0.0.1" - ) - notebook_token_data = await token_service.get_data(notebook_token) - assert notebook_token_data - session_children = [ - notebook_token, - await token_service.get_internal_token( - session_token_data, "service-a", scopes=[], ip_address="127.0.0.1" - ), - await token_service.get_internal_token( - notebook_token_data, "service-b", scopes=[], ip_address="127.0.0.1" - ), - await token_service.get_internal_token( - notebook_token_data, - "service-a", - scopes=["read:all"], - ip_address="127.0.0.1", - ), - ] - internal_token_data = await token_service.get_data(session_children[-1]) - assert internal_token_data - session_children.append( - await token_service.get_internal_token( - internal_token_data, - "service-b", - scopes=["read:all"], - ip_address="127.0.0.1", + async with factory.session.begin(): + notebook_token = await token_service.get_notebook_token( + session_token_data, ip_address="127.0.0.1" + ) + notebook_token_data = await token_service.get_data(notebook_token) + assert notebook_token_data + session_children = [ + notebook_token, + await token_service.get_internal_token( + session_token_data, + "service-a", + scopes=[], + ip_address="127.0.0.1", + ), + await token_service.get_internal_token( + notebook_token_data, + "service-b", + scopes=[], + ip_address="127.0.0.1", + ), + await token_service.get_internal_token( + notebook_token_data, + "service-a", + scopes=["read:all"], + ip_address="127.0.0.1", + ), + ] + internal_token_data = await token_service.get_data( + session_children[-1] + ) + assert internal_token_data + session_children.append( + await token_service.get_internal_token( + internal_token_data, + "service-b", + scopes=["read:all"], + ip_address="127.0.0.1", + ) ) - ) # Shorter trees of tokens from the user and service tokens. - user_children = [ - await token_service.get_internal_token( - user_token_data, "service-c", scopes=[], ip_address="127.0.0.1" - ), - await token_service.get_notebook_token( - user_token_data, ip_address="127.0.0.1" - ), - ] - service_children = [ - await token_service.get_internal_token( - service_token_data, "service-a", scopes=[], ip_address="127.0.0.1" - ) - ] + async with factory.session.begin(): + user_children = [ + await token_service.get_internal_token( + user_token_data, "service-c", scopes=[], ip_address="127.0.0.1" + ), + await token_service.get_notebook_token( + user_token_data, ip_address="127.0.0.1" + ), + ] + service_children = [ + await token_service.get_internal_token( + service_token_data, + "service-a", + scopes=[], + ip_address="127.0.0.1", + ) + ] # Deleting the session token should invalidate all of its children. - assert await token_service.delete_token( - session_token_data.token.key, - session_token_data, - session_token_data.username, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + assert await token_service.delete_token( + session_token_data.token.key, + session_token_data, + session_token_data.username, + ip_address="127.0.0.1", + ) for token in session_children: assert await token_service.get_data(token) is None @@ -1023,58 +1111,63 @@ async def test_delete_cascade(setup: SetupTest) -> None: assert await token_service.get_data(service_token_data.token) # Deleting those tokens should cascade to their children. - assert await token_service.delete_token( - user_token_data.token.key, - user_token_data, - user_token_data.username, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + assert await token_service.delete_token( + user_token_data.token.key, + user_token_data, + user_token_data.username, + ip_address="127.0.0.1", + ) for token in user_children: assert await token_service.get_data(token) is None - assert await token_service.delete_token( - service_token_data.token.key, - service_token_data, - service_token_data.username, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + assert await token_service.delete_token( + service_token_data.token.key, + service_token_data, + service_token_data.username, + ip_address="127.0.0.1", + ) for token in service_children: assert await token_service.get_data(token) is None @pytest.mark.asyncio -async def test_modify_expires(config: Config, setup: SetupTest) -> None: +async def test_modify_expires( + config: Config, factory: ComponentFactory +) -> None: """Test that expiration changes cascade to subtokens.""" - token_service = setup.factory.create_token_service() - session_token_data = await setup.create_session_token( - scopes=["user:token"] + token_service = factory.create_token_service() + session_token_data = await create_session_token( + factory, scopes=["user:token"] ) # Create a user token with no expiration and some additional tokens # chained off of it. - user_token = await token_service.create_user_token( - session_token_data, - session_token_data.username, - token_name="user-token", - scopes=["user:token"], - ip_address="127.0.0.1", - ) - user_token_data = await token_service.get_data(user_token) - assert user_token_data - notebook_token = await token_service.get_notebook_token( - user_token_data, ip_address="127.0.0.1" - ) - notebook_token_data = await token_service.get_data(notebook_token) - assert notebook_token_data - internal_token = await token_service.get_internal_token( - user_token_data, "service-a", scopes=[], ip_address="127.0.0.1" - ) - internal_token_data = await token_service.get_data(internal_token) - assert internal_token_data - nested_token = await token_service.get_internal_token( - notebook_token_data, "service-b", scopes=[], ip_address="127.0.0.1" - ) - nested_token_data = await token_service.get_data(nested_token) - assert nested_token_data + async with factory.session.begin(): + user_token = await token_service.create_user_token( + session_token_data, + session_token_data.username, + token_name="user-token", + scopes=["user:token"], + ip_address="127.0.0.1", + ) + user_token_data = await token_service.get_data(user_token) + assert user_token_data + notebook_token = await token_service.get_notebook_token( + user_token_data, ip_address="127.0.0.1" + ) + notebook_token_data = await token_service.get_data(notebook_token) + assert notebook_token_data + internal_token = await token_service.get_internal_token( + user_token_data, "service-a", scopes=[], ip_address="127.0.0.1" + ) + internal_token_data = await token_service.get_data(internal_token) + assert internal_token_data + nested_token = await token_service.get_internal_token( + notebook_token_data, "service-b", scopes=[], ip_address="127.0.0.1" + ) + nested_token_data = await token_service.get_data(nested_token) + assert nested_token_data # Check the expiration of all of those tokens matches the default # expiration for generated tokens. @@ -1084,19 +1177,21 @@ async def test_modify_expires(config: Config, setup: SetupTest) -> None: assert nested_token_data.expires == notebook_token_data.expires # Check that Redis also has an appropriate TTL. + redis = await redis_dependency() ttl = delta.total_seconds() for token in (notebook_token, internal_token, nested_token): - assert ttl - 5 <= await setup.redis.ttl(f"token:{token.key}") <= ttl + assert ttl - 5 <= await redis.ttl(f"token:{token.key}") <= ttl # Change the expiration of the user token. new_delta = timedelta(minutes=config.issuer.exp_minutes / 2) new_expires = user_token_data.created + new_delta - await token_service.modify_token( - user_token.key, - user_token_data, - expires=new_expires, - ip_address="127.0.0.1", - ) + async with factory.session.begin(): + await token_service.modify_token( + user_token.key, + user_token_data, + expires=new_expires, + ip_address="127.0.0.1", + ) # Check that all of the tokens have been updated. notebook_token_data = await token_service.get_data(notebook_token) @@ -1112,12 +1207,13 @@ async def test_modify_expires(config: Config, setup: SetupTest) -> None: # Check that the Redis TTL has also been updated. ttl = new_delta.total_seconds() for token in (notebook_token, internal_token, nested_token): - assert ttl - 5 <= await setup.redis.ttl(f"token:{token.key}") <= ttl + assert ttl - 5 <= await redis.ttl(f"token:{token.key}") <= ttl @pytest.mark.asyncio -async def test_invalid(config: Config, setup: SetupTest) -> None: - token_service = setup.factory.create_token_service() +async def test_invalid(config: Config, factory: ComponentFactory) -> None: + redis = await redis_dependency() + token_service = factory.create_token_service() expires = int(timedelta(days=1).total_seconds()) # No such key. @@ -1125,13 +1221,13 @@ async def test_invalid(config: Config, setup: SetupTest) -> None: assert await token_service.get_data(token) is None # Invalid encrypted blob. - await setup.redis.set(f"token:{token.key}", "foo", ex=expires) + await redis.set(f"token:{token.key}", "foo", ex=expires) assert await token_service.get_data(token) is None # Malformed session. fernet = Fernet(config.session_secret.encode()) raw_data = fernet.encrypt(b"malformed json") - await setup.redis.set(f"token:{token.key}", raw_data, ex=expires) + await redis.set(f"token:{token.key}", raw_data, ex=expires) assert await token_service.get_data(token) is None # Mismatched token. @@ -1145,7 +1241,7 @@ async def test_invalid(config: Config, setup: SetupTest) -> None: uid=12345, ) session = fernet.encrypt(data.json().encode()) - await setup.redis.set(f"token:{token.key}", session, ex=expires) + await redis.set(f"token:{token.key}", session, ex=expires) assert await token_service.get_data(token) is None # Missing required fields. @@ -1160,30 +1256,33 @@ async def test_invalid(config: Config, setup: SetupTest) -> None: "name": "Some User", } raw_data = fernet.encrypt(json.dumps(json_data).encode()) - await setup.redis.set(f"token:{token.key}", raw_data, ex=expires) + await redis.set(f"token:{token.key}", raw_data, ex=expires) assert await token_service.get_data(token) is None # Fix the session store and confirm we can retrieve the manually-stored # session. json_data["username"] = "example" raw_data = fernet.encrypt(json.dumps(json_data).encode()) - await setup.redis.set(f"token:{token.key}", raw_data, ex=expires) + await redis.set(f"token:{token.key}", raw_data, ex=expires) new_data = await token_service.get_data(token) assert new_data == TokenData.parse_obj(json_data) @pytest.mark.asyncio -async def test_invalid_username(setup: SetupTest) -> None: +async def test_invalid_username(factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="ex-am-pl-e", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) - token_service = setup.factory.create_token_service() - session_token = await token_service.create_session_token( - user_info, scopes=["read:all", "admin:token"], ip_address="127.0.0.1" - ) + token_service = factory.create_token_service() + async with factory.session.begin(): + session_token = await token_service.create_session_token( + user_info, + scopes=["read:all", "admin:token"], + ip_address="127.0.0.1", + ) data = await token_service.get_data(session_token) assert data diff --git a/tests/support/cookies.py b/tests/support/cookies.py new file mode 100644 index 000000000..088be0c3f --- /dev/null +++ b/tests/support/cookies.py @@ -0,0 +1,55 @@ +"""Helper functions for managing test cookies.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from gafaelfawr.constants import COOKIE_NAME +from gafaelfawr.models.state import State +from tests.support.constants import TEST_HOSTNAME + +if TYPE_CHECKING: + from httpx import AsyncClient + + from gafaelfawr.models.token import Token + +__all__ = [ + "clear_session_cookie", + "set_session_cookie", +] + + +async def set_session_cookie(client: AsyncClient, token: Token) -> str: + """Create a valid Gafaelfawr session. + + Add a valid Gafaelfawr session cookie to the ``httpx.AsyncClient``, use + the login URL, and return the resulting CSRF token. + + Parameters + ---------- + client : ``httpx.AsyncClient`` + The client to add the session cookie to. + token : `gafaelfawr.models.token.Token` + The token for the client identity to use. + + Returns + ------- + csrf : `str` + The CSRF token to use in subsequent API requests. + """ + cookie = await State(token=token).as_cookie() + client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) + r = await client.get("/auth/api/v1/login") + assert r.status_code == 200 + return r.json()["csrf"] + + +def clear_session_cookie(client: AsyncClient) -> None: + """Delete the Gafaelfawr session token. + + Parameters + ---------- + client : `httpx.AsyncClient` + The client from which to remove the session cookie. + """ + del client.cookies[COOKIE_NAME] diff --git a/tests/support/setup.py b/tests/support/setup.py deleted file mode 100644 index 8f3100196..000000000 --- a/tests/support/setup.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Set up the test suite.""" - -from __future__ import annotations - -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING - -import structlog -from httpx import AsyncClient -from safir.dependencies.http_client import http_client_dependency -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy.orm import sessionmaker - -from gafaelfawr.constants import COOKIE_NAME -from gafaelfawr.dependencies.config import config_dependency -from gafaelfawr.dependencies.redis import redis_dependency -from gafaelfawr.dependencies.token_cache import TokenCache -from gafaelfawr.factory import ComponentFactory -from gafaelfawr.models.state import State -from gafaelfawr.models.token import Token, TokenData, TokenGroup, TokenUserInfo -from tests.support.constants import TEST_HOSTNAME - -if TYPE_CHECKING: - from pathlib import Path - from typing import AsyncIterator, List, Optional - - from aioredis import Redis - - from gafaelfawr.config import Config - - -class SetupTest: - """Utility class for test setup. - - This class wraps creating a test FastAPI application, creating a factory - for building the components, and accessing configuration settings. - - This object should always be created via the :py:meth:`create` method. - The constructor should be considered private. - - Notes - ----- - This class is named SetupTest instead of TestSetup because pytest thinks - the latter is a test case and tries to execute it. - """ - - @classmethod - @asynccontextmanager - async def create(cls, tmp_path: Path) -> AsyncIterator[SetupTest]: - """Create a new `SetupTest` instance. - - This is the only supported way to set up the test environment and - should be called instead of calling the constructor directly. It - initializes and starts the application and configures an - `httpx.AsyncClient` to talk to it. - - Parameters - ---------- - tmp_path : `pathlib.Path` - The path for temporary files. - """ - config = await config_dependency() - redis = await redis_dependency(config) - - # Create the database session that will be used by SetupTest and by - # the factory it contains. The application will use a separate - # session handled by its middleware. - engine = create_async_engine(config.database_url, future=True) - session_factory = sessionmaker( - engine, expire_on_commit=False, class_=AsyncSession - ) - - # Build the SetupTest object inside all of the contexts required by - # its components and handle clean shutdown. We have to build two - # separate AsyncClients here, one which will be used to make requests - # to the application under test and the other of which will be used in - # the factory for components that require a client. They have to be - # separate or requests for routes that are also served by the app will - # bypass the mock and call the app instead, causing tests to fail. - try: - async with AsyncClient() as http_client: - async with session_factory() as session: - yield cls( - tmp_path=tmp_path, - config=config, - redis=redis, - session=session, - http_client=http_client, - ) - finally: - await http_client_dependency.aclose() - await redis_dependency.aclose() - await engine.dispose() - - def __init__( - self, - *, - tmp_path: Path, - config: Config, - redis: Redis, - session: AsyncSession, - http_client: AsyncClient, - ) -> None: - self.tmp_path = tmp_path - self.redis = redis - self.session = session - self.http_client = http_client - self.token_cache = TokenCache() - self.logger = structlog.get_logger(config.safir.logger_name) - assert self.logger - - @property - def factory(self) -> ComponentFactory: - """Return a `~gafaelfawr.factory.ComponentFactory`. - - Build a new one each time to ensure that it picks up the current - configuration information. - - Returns - ------- - factory : `gafaelfawr.factory.ComponentFactory` - Newly-created factory. - """ - assert config_dependency._config - return ComponentFactory( - config=config_dependency._config, - redis=self.redis, - http_client=self.http_client, - token_cache=self.token_cache, - session=self.session, - logger=self.logger, - ) - - async def create_session_token( - self, - *, - username: Optional[str] = None, - group_names: Optional[List[str]] = None, - scopes: Optional[List[str]] = None, - ) -> TokenData: - """Create a session token. - - Parameters - ---------- - username : `str`, optional - Override the username of the generated token. - group_namess : List[`str`], optional - Group memberships the generated token should have. - scopes : List[`str`], optional - Scope for the generated token. - - Returns - ------- - data : `gafaelfawr.models.token.TokenData` - The data for the generated token. - """ - if not username: - username = "some-user" - if group_names: - groups = [TokenGroup(name=g, id=1000) for g in group_names] - else: - groups = [] - user_info = TokenUserInfo( - username=username, - name="Some User", - email="someuser@example.com", - uid=1000, - groups=groups, - ) - if not scopes: - scopes = ["user:token"] - token_service = self.factory.create_token_service() - token = await token_service.create_session_token( - user_info, scopes=scopes, ip_address="127.0.0.1" - ) - data = await token_service.get_data(token) - assert data - await self.session.commit() - return data - - async def login(self, client: AsyncClient, token: Token) -> str: - """Create a valid Gafaelfawr session. - - Add a valid Gafaelfawr session cookie to the `httpx.AsyncClient`, use - the login URL, and return the resulting CSRF token. - - Parameters - ---------- - client : `httpx.AsyncClient` - The client to add the session cookie to. - token : `gafaelfawr.models.token.Token` - The token for the client identity to use. - - Returns - ------- - csrf : `str` - The CSRF token to use in subsequent API requests. - """ - cookie = await State(token=token).as_cookie() - client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) - r = await client.get("/auth/api/v1/login") - assert r.status_code == 200 - return r.json()["csrf"] - - def logout(self, client: AsyncClient) -> None: - """Delete the Gafaelfawr session token. - - Parameters - ---------- - client : `httpx.AsyncClient` - The client from which to remove the session cookie. - """ - del client.cookies[COOKIE_NAME] diff --git a/tests/support/tokens.py b/tests/support/tokens.py index 0ac3f7408..b7fb9cb2f 100644 --- a/tests/support/tokens.py +++ b/tests/support/tokens.py @@ -11,7 +11,13 @@ from gafaelfawr.dependencies.config import config_dependency from gafaelfawr.models.history import TokenChange, TokenChangeHistoryEntry from gafaelfawr.models.oidc import OIDCVerifiedToken -from gafaelfawr.models.token import Token, TokenData, TokenType, TokenUserInfo +from gafaelfawr.models.token import ( + Token, + TokenData, + TokenGroup, + TokenType, + TokenUserInfo, +) from gafaelfawr.storage.history import TokenChangeHistoryStore from gafaelfawr.storage.token import TokenDatabaseStore from gafaelfawr.util import current_datetime @@ -22,8 +28,14 @@ from sqlalchemy.ext.asyncio import AsyncSession from gafaelfawr.config import Config + from gafaelfawr.factory import ComponentFactory -__all__ = ["create_test_token", "create_upstream_oidc_token"] +__all__ = [ + "add_expired_session_token", + "create_session_token", + "create_test_token", + "create_upstream_oidc_token", +] async def add_expired_session_token( @@ -84,6 +96,56 @@ async def add_expired_session_token( await token_change_store.add(history_entry) +async def create_session_token( + factory: ComponentFactory, + *, + username: Optional[str] = None, + group_names: Optional[List[str]] = None, + scopes: Optional[List[str]] = None, +) -> TokenData: + """Create a session token. + + Parameters + ---------- + factory : `gafaelfawr.factory.ComponentFactory` + Factory used to create services to add the token. + username : `str`, optional + Override the username of the generated token. + group_namess : List[`str`], optional + Group memberships the generated token should have. + scopes : List[`str`], optional + Scope for the generated token. + + Returns + ------- + data : `gafaelfawr.models.token.TokenData` + The data for the generated token. + """ + if not username: + username = "some-user" + if group_names: + groups = [TokenGroup(name=g, id=1000) for g in group_names] + else: + groups = [] + user_info = TokenUserInfo( + username=username, + name="Some User", + email="someuser@example.com", + uid=1000, + groups=groups, + ) + if not scopes: + scopes = ["user:token"] + token_service = factory.create_token_service() + async with factory.session.begin(): + token = await token_service.create_session_token( + user_info, scopes=scopes, ip_address="127.0.0.1" + ) + data = await token_service.get_data(token) + assert data + return data + + def create_test_token( config: Config, groups: Optional[List[str]] = None, diff --git a/tests/verify_test.py b/tests/verify_test.py index 5064b854b..ae0e4f5bc 100644 --- a/tests/verify_test.py +++ b/tests/verify_test.py @@ -30,7 +30,7 @@ import respx from _pytest._code import ExceptionInfo - from tests.support.setup import SetupTest + from gafaelfawr.factory import ComponentFactory def encode_token( @@ -51,10 +51,11 @@ def encode_token( @pytest.mark.asyncio async def test_verify_oidc( - tmp_path: Path, respx_mock: respx.Router, setup: SetupTest + tmp_path: Path, respx_mock: respx.Router, factory: ComponentFactory ) -> None: config = await configure(tmp_path, "oidc") - verifier = setup.factory.create_token_verifier() + factory.reconfigure(config) + verifier = factory.create_token_verifier() now = datetime.now(timezone.utc) exp = now + timedelta(days=24) @@ -114,11 +115,12 @@ async def test_verify_oidc( @pytest.mark.asyncio async def test_verify_oidc_no_kids( - tmp_path: Path, respx_mock: respx.Router, setup: SetupTest + tmp_path: Path, respx_mock: respx.Router, factory: ComponentFactory ) -> None: config = await configure(tmp_path, "oidc-no-kids") + factory.reconfigure(config) keypair = config.issuer.keypair - verifier = setup.factory.create_token_verifier() + verifier = factory.create_token_verifier() await mock_oidc_provider_config(respx_mock, keypair, "kid") now = datetime.now(timezone.utc) @@ -138,11 +140,12 @@ async def test_verify_oidc_no_kids( @pytest.mark.asyncio async def test_key_retrieval( - tmp_path: Path, respx_mock: respx.Router, setup: SetupTest + tmp_path: Path, respx_mock: respx.Router, factory: ComponentFactory ) -> None: config = await configure(tmp_path, "oidc-no-kids") + factory.reconfigure(config) assert config.oidc - verifier = setup.factory.create_token_verifier() + verifier = factory.create_token_verifier() # Initial working JWKS configuration. jwks = config.issuer.keypair.public_key_as_jwks("some-kid") From 76198b3dd2141f435586c3cb19f9c0b616cfc568 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 30 Nov 2021 16:49:19 -0800 Subject: [PATCH 23/24] Add changelog for the upcoming 3.4.0 release --- CHANGELOG.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ca0573b96..9a5453864 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,16 @@ Change log ########## +3.4.0 (unreleased) +================== + +- Gafaelfawr now uses async SQLAlchemy for all database calls, which avoids latency affecting the whole process when a request requires database queries or writes. +- Internal and notebook tokens are now acquired, when needed, while holding a per-user cache lock. + This means that when a flood of requests that all require a delegated token come in at the same time, a given Gafaelfawr process allows only the first request to proceed and blocks the rest until it completes. + All the other requests are then served from the cache. + This fixes a deadlock observed in previous versions of Gafaelfawr under heavy load from a single user who does not have a cached delegated token. +- Update dependencies. + 3.3.0 (2021-11-11) ================== From d0a07cb611f4ba267673df2f7deaebd3b48101d5 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 2 Dec 2021 13:52:31 -0800 Subject: [PATCH 24/24] Add date for 3.4.0 release --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a5453864..d14e4d394 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Change log ########## -3.4.0 (unreleased) +3.4.0 (2021-12-02) ================== - Gafaelfawr now uses async SQLAlchemy for all database calls, which avoids latency affecting the whole process when a request requires database queries or writes.