commit 68d521677a27013fd8d1ff3f6c579bb449cc2a4a Author: bernard-ng Date: Sun Oct 5 13:55:28 2025 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d6cb196 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,33 @@ +# Unified repository-wide Git attributes +# Mark vendored/non-source directories so GitHub Linguist doesn’t skew language stats +projects/backend/public/** linguist-vendored +projects/backend/assets/** linguist-vendored +projects/crawler/notebooks/** linguist-vendored +# Do not count certain generated or data-heavy formats in language stats +*.css linguist-detectable=false +*.scss linguist-detectable=false +*.ipynb linguist-detectable=false +# Enforce LF newlines for text files commonly used in this monorepo +*.css text eol=lf +*.html text eol=lf +*.js text eol=lf +*.json text eol=lf +*.md text eol=lf +*.py text eol=lf +*.scss text eol=lf +*.svg text eol=lf +*.txt text eol=lf +*.xml text eol=lf +*.xml.* text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.php text eol=lf +*.twig text eol=lf +*.sh text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.py text eol=lf +# Binary formats +*.map binary +*.mmdb filter=lfs diff=lfs merge=lfs -text +*.sqlite filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/backend/audit.yaml b/.github/workflows/backend/audit.yaml new file mode 100644 index 0000000..37eb764 --- /dev/null +++ b/.github/workflows/backend/audit.yaml @@ -0,0 +1,51 @@ +name: audit +on: + workflow_call: + push: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" + pull_request: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" +defaults: + run: + working-directory: projects/backend +jobs: + audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + tools: composer:v2 + + - name: Setup cache + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v4 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php8.3-composer-${{ hashFiles('projects/backend/composer.lock') }} + restore-keys: | + php8.3-composer-latest- + - name: Update composer + run: composer self-update + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Security Audit with composer + run: composer audit + continue-on-error: true diff --git a/.github/workflows/backend/deploy.yaml b/.github/workflows/backend/deploy.yaml new file mode 100644 index 0000000..51e7e00 --- /dev/null +++ b/.github/workflows/backend/deploy.yaml @@ -0,0 +1,40 @@ +name: deploy +on: + push: + branches: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" +jobs: + audit: + uses: ./.github/workflows/backend/audit.yaml + quality: + uses: ./.github/workflows/backend/quality.yaml + tests: + uses: ./.github/workflows/backend/tests.yaml + needs: [audit, quality] + + deploy: + name: Deploy + needs: [tests] + runs-on: ubuntu-latest + steps: + - name: execute ssh command + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + script: | + cd /var/www/html/news.devscast.tech + git pull origin main --rebase + make deploy + curl -X POST "https://api.telegram.org/bot${{ secrets.DEVY_TOKEN }}/sendMessage" \ + -H "Content-Type: application/json" \ + -d '{ + "chat_id": "${{ secrets.DEVY_CHAT_ID }}", + "text": "news.devscast.tech : `'"$(git rev-parse --short HEAD)"'` has been deployed! 🎉", + "parse_mode": "Markdown" + }' diff --git a/.github/workflows/backend/quality.yaml b/.github/workflows/backend/quality.yaml new file mode 100644 index 0000000..d637f02 --- /dev/null +++ b/.github/workflows/backend/quality.yaml @@ -0,0 +1,51 @@ +name: quality +on: + workflow_call: + push: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" + pull_request: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" + +defaults: + run: + working-directory: projects/backend +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + tools: composer:v2 + + - name: Setup cache + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v4 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php8.3-composer-${{ hashFiles('projects/backend/composer.lock') }} + restore-keys: | + php8.3-composer-latest- + - name: Update composer + run: composer self-update + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run code quality analysis + run: composer app:cs diff --git a/.github/workflows/backend/tests.yaml b/.github/workflows/backend/tests.yaml new file mode 100644 index 0000000..04c3990 --- /dev/null +++ b/.github/workflows/backend/tests.yaml @@ -0,0 +1,67 @@ +name: tests +on: + workflow_call: + push: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" + pull_request: + branches-ignore: + - main + paths: + - "projects/backend/**" + - ".github/workflows/backend/**" +jobs: + functional: + name: Functional Tests + runs-on: ubuntu-latest +# services: +# mysql: +# image: mariadb:10.11.11 +# env: +# MYSQL_ALLOW_EMPTY_PASSWORD: false +# MYSQL_ROOT_PASSWORD: root +# MYSQL_DATABASE: root +# ports: +# - 3306/tcp +# options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + matrix: + php: [8.4] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + - name: Setup cache + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v4 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer-latest- + - name: Update composer + run: composer self-update + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + +# - name: Setup mysql +# run: sudo systemctl start mysql + + - name: Run functional tests + run: composer app:test + env: + APP_ENV: test +# DATABASE_URL: mysql://root:root@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/app_test diff --git a/.github/workflows/crawler/audit.yml b/.github/workflows/crawler/audit.yml new file mode 100644 index 0000000..6bf503c --- /dev/null +++ b/.github/workflows/crawler/audit.yml @@ -0,0 +1,40 @@ +name: audit + +on: + push: + branches: + - main + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + pull_request: + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + +jobs: + bandit: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Sync dependencies (with dev tools) + run: uv sync --dev + + - name: Run Bandit (security linter) + run: uv run bandit -r . -c pyproject.toml || true diff --git a/.github/workflows/crawler/quality.yml b/.github/workflows/crawler/quality.yml new file mode 100644 index 0000000..0dca0c3 --- /dev/null +++ b/.github/workflows/crawler/quality.yml @@ -0,0 +1,49 @@ +name: quality + +on: + push: + branches: + - main + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + pull_request: + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + +defaults: + run: + working-directory: projects/crawler + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + projects/crawler/.venv + key: ${{ runner.os }}-uv-${{ hashFiles('projects/crawler/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Sync dependencies (with dev tools) + run: uv sync --dev + + - name: Run Ruff (lint + format checks) + run: | + uv run ruff check . + uv run ruff format --check . + + - name: Run Pyright (type checks) + run: uv run pyright diff --git a/.github/workflows/crawler/tests.yml b/.github/workflows/crawler/tests.yml new file mode 100644 index 0000000..99c4ade --- /dev/null +++ b/.github/workflows/crawler/tests.yml @@ -0,0 +1,44 @@ +name: tests + +on: + push: + branches: + - main + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + pull_request: + paths: + - "projects/crawler/**" + - ".github/workflows/crawler/**" + +defaults: + run: + working-directory: projects/crawler + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + projects/crawler/.venv + key: ${{ runner.os }}-uv-${{ hashFiles('projects/crawler/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Sync dependencies (with dev tools) + run: uv sync --dev + + - name: Run Pytest + run: uv run pytest diff --git a/.github/workflows/mobile/quality.yaml b/.github/workflows/mobile/quality.yaml new file mode 100644 index 0000000..387b52e --- /dev/null +++ b/.github/workflows/mobile/quality.yaml @@ -0,0 +1,51 @@ +name: quality + +on: + push: + paths: + - "projects/mobile/**" + - ".github/workflows/mobile/**" + pull_request: + paths: + - "projects/mobile/**" + - ".github/workflows/mobile/**" + +defaults: + run: + working-directory: projects/mobile + +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Cache Bun Dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('projects/mobile/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install Dependencies + run: bun install --frozen-lockfile + + - name: Run Code Quality Checks + run: | + bun run check-types + bun run check + bun run lint:check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..379e67a --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +.idea +/data/ +.deptrac.cache +.nohup.out +.*.out +.DS_Store +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> phpstan/phpstan ### +phpstan.neon +###< phpstan/phpstan ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### + +.idea/ +.vscode/ +.ipynb_checkpoints/ +*.pyc +.env.local +.env.*.local +var/ +var/volumes/ +var/volums/ +.DS_Store + +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +data/ + +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.idea +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +.env.local diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..42f5934 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,175 @@ +## creative commons + +# Attribution-NonCommercial-ShareAlike 4.0 International + +Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +### Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). + +* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). + +## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +### Section 1 – Definitions. + +a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. + +d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. + +h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. + +k. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. + +l. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +m. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +n. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +### Section 2 – Scope. + +a. ___License grant.___ + +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and + + B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. + +2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + +3. __Term.__ The term of this Public License is specified in Section 6(a). + +4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + +5. __Downstream recipients.__ + + A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + + C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + +6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. ___Other rights.___ + +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. + +### Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. ___Attribution.___ + +1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + +3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + +b. ___ShareAlike.___ + +In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. + +1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. + +2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. + +3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. + +### Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and + +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +### Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ + +b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ + +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +### Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + +2. upon express reinstatement by the Licensor. + +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +### Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +### Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. +> +> Creative Commons may be contacted at creativecommons.org diff --git a/README.md b/README.md new file mode 100644 index 0000000..e20784e --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Basango : Towards a scalable and intelligent system for Congolese News curation + +| Scope | Link | +|-------------------|-------------------------------------------| +| Crawler | [README.md](./projects/crawler/README.md) | +| Backend | [README.md](./projects/backend/README.md) | +| Mobile | [README.md](./projects/mobile/README.md) | + +--- +### Introduction + +The **"Basango"** is a structured and scalable system of news articles sourced from major media outlets covering diverse aspects of the Democratic Republic of Congo (DRC). Designed for efficiency, this system enables the automated collection, processing, and organization of news stories spanning politics, economy, society, culture, environment, and international affairs. + +This system is built to support large-scale text analysis, making it a valuable resource for researchers, journalists, policymakers, and data scientists. It facilitates tasks such as sentiment analysis, trend detection, entity recognition, and language modeling, providing deep insights into the evolving socio-political and economic landscape of the DRC. + +To ensure quality and reliability, the dataset prioritizes reputable news sources while maintaining an adaptable framework for continuous expansion. However, users are encouraged to critically assess the content, as journalistic standards and perspectives may vary. + +### Sources + +| Source | Articles | Link | +|----------------------|----------|--------------------------------------| +| radiookapi.net | +100k | https://www.radiookapi.net/actualite | +| mediacongo.cd | +100k | https://www.mediacongo.net/ | +| beto.cd | +30k | https://www.beto.cd/ | +| actualite.cd | +57k | https://actualite.cd/ | +| 7sur7.cd | +50k | https://7sur7.cd | +| newscd.net | +5k | https://newscd.net | +| congoindependant.com | +10k | https://www.congoindependant.com/ | +| congoactu.net | +10k | https://www.congoactu.net/ | + +### Acknowledgment: +The compilation and curation of the "Basango" were conducted by Bernard Ngandu with the primary objective of facilitating research and analysis related to the Democratic Republic of Congo. +I do not own the content of the articles, and all rights belong to the respective publishers. The dataset is intended for non-commercial research purposes only. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..fa7a95c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,123 @@ +services: + mariadb: + image: mariadb:10.11.11 + environment: + MARIADB_USER: root + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: app + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'no' + volumes: + - ./var/volumes/mariadb:/var/lib/mysql:rw + - ./var/volumes/backend-var:/var/www/var + networks: + - basango_network + + postgres: + image: pgvector/pgvector:pg16 + environment: + POSTGRES_DB: app + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - ./var/volumes/postgres:/var/lib/postgresql/data + networks: + - basango_network + + redis: + image: redis:7-alpine + command: ["redis-server", "--appendonly", "yes"] + ports: + - "6379:6379" + volumes: + - ./var/volumes/redis:/data + networks: + - basango_network + + redis-commander: + image: rediscommander/redis-commander:latest + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8081:8081" + depends_on: + - redis + networks: + - basango_network + + nginx: + build: ./docker/nginx + ports: + - "8000:80" + volumes: + - ./projects/backend/public:/var/www/public:delegated + depends_on: + - php + networks: + - basango_network + + php: + user: '${USER_ID:-1000}:${GROUP_ID:-1000}' + build: ./docker/php + volumes: + - ./projects/backend:/var/www:delegated + depends_on: + - mariadb + - postgres + - redis + networks: + - basango_network + + adminer: + image: adminer:latest + depends_on: + - mariadb + environment: + APP_ENV: dev + ADMINER_DESIGN: pepa-linha + ADMINER_DEFAULT_SERVER: mariadb + ports: + - "8082:8080" + networks: + - basango_network + + mailer: + image: axllent/mailpit + ports: + - "1025:1025" + - "8025:8025" + environment: + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 + networks: + - basango_network + +networks: + basango_network: + +volumes: + database_data: + driver: local + driver_opts: + type: none + o: bind + device: ./var/volumes/mariadb + backend_var: + driver: local + driver_opts: + type: none + o: bind + device: ./var/volumes/backend-var + redis_data: + driver: local + driver_opts: + type: none + o: bind + device: ./var/volumes/redis + postgres_data: + driver: local + driver_opts: + type: none + o: bind + device: ./var/volumes/postgres diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..4520133 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.27.1-alpine + +COPY default.conf /etc/nginx/conf.d/default.conf + diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..933ce34 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + root /var/www/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + index index.html index.htm index.php; + + charset utf-8; + + location / { + root /var/www/; + try_files /public/$uri /public/$uri /assets/$uri /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451 500 501 502 503 504 505 506 507 508 510 511 /error.html; + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} + diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..7167e53 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,19 @@ +FROM php:8.4-fpm-alpine + +# Install dependencies +RUN apk --no-cache add curl git wget bash dpkg + +# Add PHP extensions +ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ +RUN chmod +x /usr/local/bin/install-php-extensions + +RUN install-php-extensions opcache iconv soap +RUN install-php-extensions zip intl fileinfo +RUN install-php-extensions pdo redis mysqli pdo_mysql +RUN install-php-extensions gd + +# Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer + +WORKDIR /var/www + diff --git a/projects/backend/.env b/projects/backend/.env new file mode 100644 index 0000000..63d97e0 --- /dev/null +++ b/projects/backend/.env @@ -0,0 +1,70 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=f51bc23b4d778b25fcb9804bb9dfaf39 +###< symfony/framework-bundle ### + +CRAWLING_NOTIFICATION_EMAIL= +TIMEZONE=Africa/Lubumbashi +GOOGLE_CUSTOM_SEARCH_API_KEY= +NEWS_DATA_API_KEY= + +###> Devy ### +DEVY_TOKEN= +DEVY_CHANNEL= +DEVY_TOPIC=4919 +###< Devy ### + +###> symfony/mailer ### +MAILER_DSN=smtp://mailer:1025?encryption=null&auth_mode=null +###< symfony/mailer ### + +###> symfony/messenger ### +# Choose one of the transports below +# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages +# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages +MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 +###< symfony/messenger ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +DATABASE_URL="mysql://root:root@database:3306/app?serverVersion=Mariadb-10.11.11&charset=utf8mb4" +#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +###< doctrine/doctrine-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=fa85ee5fac0b5ab29b4bd41927d12fd56420d28994be1738bb7e52dcf9dc163a +JWT_TTL=84600 +###< lexik/jwt-authentication-bundle ### + +###> sentry/sentry-symfony ### +SENTRY_DSN= +###< sentry/sentry-symfony ### + +###> blackfilre/blackfire ### +BLACKFIRE_SERVER_ID= +BLACKFIRE_SERVER_TOKEN= +BLACKFIRE_CLIENT_ID= +BLACKFIRE_CLIENT_TOKEN= +###< blackfire/blackfire ### diff --git a/projects/backend/.env.test b/projects/backend/.env.test new file mode 100644 index 0000000..6846a72 --- /dev/null +++ b/projects/backend/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\SharedKernel\Infrastructure\Framework\Symfony\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/projects/backend/.gitignore b/projects/backend/.gitignore new file mode 100644 index 0000000..747d44e --- /dev/null +++ b/projects/backend/.gitignore @@ -0,0 +1,28 @@ +.idea +/data/ +.deptrac.cache +.nohup.out +.*.out +.DS_Store +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> phpstan/phpstan ### +phpstan.neon +###< phpstan/phpstan ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### diff --git a/projects/backend/.htaccess b/projects/backend/.htaccess new file mode 100644 index 0000000..4ce02d0 --- /dev/null +++ b/projects/backend/.htaccess @@ -0,0 +1,68 @@ + + + Header set Access-Control-Allow-Origin "*" + + + + + RewriteEngine on + RewriteOptions inherit + + # SSL and let's encrypt + RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/.+$ + RewriteCond %{REQUEST_URI} !^/\.well-known/pki-validation/[A-F0-9]{32}\.txt(?:\ Comodo\ DCV)?$ + RewriteRule ^.well-known/acme-challenge - [L] + + # redirect to no-www + RewriteBase / + RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] + RewriteRule ^(.*)$ http://%1/$1 [R=301,L] + + # https redirect + RewriteCond %{HTTPS} !=on + RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + + # redirect all requests to public directory + RewriteCond %{REQUEST_URI} !public/ + RewriteRule (.*) /public/$1 [L] + + +# Enable Gzip Compression for page speed + + mod_gzip_on Yes + mod_gzip_dechunk Yes + mod_gzip_item_include file .(html?|txt|css|js|php|pl)$ + mod_gzip_item_include handler ^cgi-script$ + mod_gzip_item_include mime ^text/.* + mod_gzip_item_include mime ^application/x-javascript.* + mod_gzip_item_exclude mime ^image/.* + mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.* + + +# Enable Deflate Comporession for page speed + + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/xml + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE application/xhtml+xml + AddOutputFilterByType DEFLATE application/rss+xml + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/x-javascript + AddOutputFilterByType DEFLATE application/vnd.ms-fontobject + AddOutputFilterByType DEFLATE application/x-font + AddOutputFilterByType DEFLATE application/x-font-opentype + AddOutputFilterByType DEFLATE application/x-font-otf + AddOutputFilterByType DEFLATE application/x-font-truetype + AddOutputFilterByType DEFLATE image/jpeg + AddOutputFilterByType DEFLATE image/png + AddOutputFilterByType DEFLATE image/gif + AddOutputFilterByType DEFLATE image/bmp + AddOutputFilterByType DEFLATE image/jpeg, + AddOutputFilterByType DEFLATE image/svg+xml + AddOutputFilterByType DEFLATE image/x-icon + AddOutputFilterByType DEFLATE audio/mpeg + AddOutputFilterByType DEFLATE audio/* + AddOutputFilterByType DEFLATE video/mp4 + diff --git a/projects/backend/CHANGELOG.md b/projects/backend/CHANGELOG.md new file mode 100644 index 0000000..e6fff49 --- /dev/null +++ b/projects/backend/CHANGELOG.md @@ -0,0 +1,24 @@ +# CHANGELOG + +This changelog references the relevant changes (bug and security fixes) done + +## [Unreleased] +- Added: `app:stats' command to get the number of articles in the database +- Modified: use `hash` instead of `link` field as index in `articles` table +- Added: support for wp-json plugin via `WordPressJson` class +- Added: `getPagination` method to `Source` abstract class +- Added: `--parallel` option to `app:crawl` command to crawl multiple pages in parallel +- Added: `app:update` command to update the database with the latest articles +- Added: `$sep` parameter to `DateRange::from` method +- Added: support for mysql database +- Added: export to csv feature +- Removed: `--filename` option from `app:crawl` command + + +### 1.2.1 +- Added: '--page' option to 'app:crawl' command +- Added: '--date' option to 'app:crawl' command +- Added: notification by email when the crawl is finished + +### 1.0.0 +- Initial release diff --git a/projects/backend/CITATION.cff b/projects/backend/CITATION.cff new file mode 100644 index 0000000..25fa00b --- /dev/null +++ b/projects/backend/CITATION.cff @@ -0,0 +1,38 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: DRC News Corpus +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Bernard + name-particle: Tshabu + family-names: Ngandu + email: bernard@devscast.tech + affiliation: Devscast Community + orcid: 'https://orcid.org/0009-0003-9777-6349' +repository-code: 'https://github.com/bernard-ng/drc-news-corpus' +repository: >- + https://www.huggingface.c0/datasets/bernard-ng/drc-news-corpus +abstract: >- + The "DRC News Corpus" is a curated collection of news + articles sourced from major media outlets covering a wide + spectrum of topics related to the Democratic Republic of + Congo (DRC). This dataset encompasses a diverse range of + news stories, including but not limited to politics, + economy, social issues, culture, environment, and + international relations, providing comprehensive coverage + of events and developments within the country. +keywords: + - news + - datasets + - DRC + - politics + - NLP +license: CC-BY-NC-SA-4.0 +commit: b1d386986b196ae0ab637ec1a50fd992cf829d34 +version: 1.2.1 +date-released: '2024-03-31' diff --git a/projects/backend/LICENSE.md b/projects/backend/LICENSE.md new file mode 100644 index 0000000..42f5934 --- /dev/null +++ b/projects/backend/LICENSE.md @@ -0,0 +1,175 @@ +## creative commons + +# Attribution-NonCommercial-ShareAlike 4.0 International + +Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +### Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). + +* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). + +## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +### Section 1 – Definitions. + +a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. + +d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. + +h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. + +k. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. + +l. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +m. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +n. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +### Section 2 – Scope. + +a. ___License grant.___ + +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and + + B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. + +2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + +3. __Term.__ The term of this Public License is specified in Section 6(a). + +4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + +5. __Downstream recipients.__ + + A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + + C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + +6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. ___Other rights.___ + +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. + +### Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. ___Attribution.___ + +1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + +3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + +b. ___ShareAlike.___ + +In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. + +1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. + +2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. + +3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. + +### Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and + +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +### Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ + +b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ + +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +### Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + +2. upon express reinstatement by the Licensor. + +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +### Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +### Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. +> +> Creative Commons may be contacted at creativecommons.org diff --git a/projects/backend/Makefile b/projects/backend/Makefile new file mode 100644 index 0000000..bbbe024 --- /dev/null +++ b/projects/backend/Makefile @@ -0,0 +1,97 @@ +.PHONY: default +default: help + +# ----------------------------------- +# Variables +# ----------------------------------- +user := $(shell id -u) +group := $(shell id -g) +dc := USER_ID=$(user) GROUP_ID=$(group) docker compose -f ../../compose.yaml + +.PHONY: help +help: + @echo Tasks: + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +# ----------------------------------- +# Docker Compose +# ----------------------------------- +FORCE: +.PHONY: build +build: ## Build & start docker containers for development + $(dc) build && \ + $(dc) up -d + $(dc) run --rm php composer install && \ + $(dc) run --rm php bin/console c:c + +.PHONY: ssh +ssh: ## SSH into container + $(dc) exec php bash + +.PHONY: start +start: ## Start docker containers + $(dc) --env-file .env.local up -d + +.PHONY: restart +restart: ## Restart docker containers + $(dc) restart + +.PHONY: stop +stop: ## Stop docker containers + $(dc) stop + +.PHONY: logs +logs: ## Show logs of docker containers + @$(dc) logs --tail=0 --follow + +.PHONY: destroy +destroy: ## Destroy docker containers + $(dc) kill && \ + $(dc) rm -f + +# ----------------------------------- +# CI / CD +# ----------------------------------- +.PHONY: lint +lint: ## code quality analysis + $(dc) run --rm php composer app:cs + +.PHONY: test +test: ## unit and functional tests + $(dc) run --rm php composer app:test + #$(dc) run --rm php composer app:behat + +.PHONY: audit +audit: ## security audit + $(dc) run --rm php composer audit + +.PHONY: deploy +deploy: ## Deployment tasks + /usr/bin/php ~/composer.phar install --optimize-autoloader + /usr/bin/php bin/console doctrine:database:create --if-not-exists + /usr/bin/php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration --all-or-nothing + /usr/bin/php bin/console cache:clear --env=prod + /usr/bin/php bin/console secrets:decrypt-to-local --env=prod + /usr/bin/php ~/composer.phar symfony:dump-env prod + +# ----------------------------------- +# Symfony +# ----------------------------------- +.PHONY: migrate +migrate: ## Run migrations + $(dc) run --rm php bin/console doctrine:databases:create --if-not-exists + $(dc) run --rm php bin/console doctrine:migrations:migrate --no-interaction + +.PHONY: cache +cache: ## Clear cache + $(dc) run --rm php bin/console cache:clear + $(dc) run --rm php bin/console cache:warmup + +# ----------------------------------- +# Dependencies +# ----------------------------------- +.PHONY: deps +deps: ## Install dependencies + $(dc) run --rm php composer install + $(dc) run --rm node yarn install --force + $(dc) run --rm node yarn build diff --git a/projects/backend/README.md b/projects/backend/README.md new file mode 100644 index 0000000..535fc4b --- /dev/null +++ b/projects/backend/README.md @@ -0,0 +1,108 @@ +# Core and Backend + +![Deployed](https://github.com/bernard-ng/drc-news-corpus/actions/workflows/deploy.yaml/badge.svg) +![Coding Standard](https://github.com/bernard-ng/drc-news-corpus/actions/workflows/quality.yaml/badge.svg) +![Tests](https://github.com/bernard-ng/drc-news-corpus/actions/workflows/tests.yaml/badge.svg) +![Security](https://github.com/bernard-ng/drc-news-corpus/actions/workflows/audit.yaml/badge.svg) + +| Scope | Link | +|-------------------|------------------------------------------------------------| +| core and backend | https://github.com/bernard-ng/drc-news-corpus | +| ML models | https://github.com/bernard-ng/drc-news-ml | +| Mobile App | https://github.com/bernard-ng/drc-news-app | +| Dataset (partial) | https://huggingface.co/datasets/bernard-ng/drc-news-corpus | + +--- + +## DRC News Corpus : Towards a scalable and intelligent system for Congolese News curation + +### Introduction + +The **"DRC News Corpus"** is a structured and scalable dataset of news articles sourced from major media outlets covering diverse aspects of the Democratic Republic of Congo (DRC). Designed for efficiency, this system enables the automated collection, processing, and organization of news stories spanning politics, economy, society, culture, environment, and international affairs. + +### Scalability and Use Cases: + +This dataset is built to support large-scale text analysis, making it a valuable resource for researchers, journalists, policymakers, and data scientists. It facilitates tasks such as sentiment analysis, trend detection, entity recognition, and language modeling, providing deep insights into the evolving socio-political and economic landscape of the DRC. + +To ensure quality and reliability, the dataset prioritizes reputable news sources while maintaining an adaptable framework for continuous expansion. However, users are encouraged to critically assess the content, as journalistic standards and perspectives may vary. + +### Sources + +| Source | Articles | Link | +|----------------------|----------|--------------------------------------| +| radiookapi.net | +100k | https://www.radiookapi.net/actualite | +| mediacongo.cd | +100k | https://www.mediacongo.net/ | +| beto.cd | +30k | https://www.beto.cd/ | +| actualite.cd | +57k | https://actualite.cd/ | +| 7sur7.cd | +50k | https://7sur7.cd | +| newscd.net | +5k | https://newscd.net | +| congoindependant.com | +10k | https://www.congoindependant.com/ | +| congoactu.net | +10k | https://www.congoactu.net/ | + + +### Build the dataset +If you want to rebuild the dataset follow the steps bellow : + +#### Installation +```bash +git clone https://github.com/bernard-ng/drc-news-corpus.git && cd drc-news-corpus +make build +make start +``` + +#### Usage +See supported sources above. you can also add your own source by extending the `App/Aggregator/Infrastructure/Crawler/Source/Source` abstract class. +if you want to crawl `radiookapi.net` you can run the following command: + +##### 1. **Crawling** +```bash +php bin/console app:crawl radiookapi.net + +# You can specify a date range to crawl articles. +php bin/console app:crawl beto.cd --date="2022-01-01:2022-12-31" + +# You can specify a page range to crawl articles. +php bin/console app:crawl mediacongo.net --page="0:6" + +# You can specify both date and page range. +php bin/console app:crawl actualite.cd --date="2022-01-01:2022-12-31" --page="0:6" + +# some sources require a category to crawl articles. +php bin/console app:crawl 7sur7.cd --category=politique + +# You can crawl multiple pages in parallel (WIP - not stable). +php bin/console app:crawl radiookapi.net --parallel=20 +``` + +##### 2. **Updating** +```bash +# Update the database with the latest articles. +php bin/console app:update radiookapi.net +``` + +Notice that this can take a while depending on the number of articles you want to crawl and will store the articles in the database. +running this command in the background is recommended. by default no output is generated, you can add the `-v` option to see the progress. + +```bash +nohup php bin/console app:crawl radiookapi.net -v > crawling.log +``` + +##### 3. **Statistics** +```bash +# Get the number of articles in the database. +php bin/console app:stats +``` + +### Export the dataset +You can export the dataset to a CSV file by running the following command: + +```bash +php bin/console app:export radiookapi.net +``` + +a CSV file will be generated in the `data` directory. + + +### Acknowledgment: +The compilation and curation of the "DRC News Corpus" were conducted by Tshabu Ngandu Bernard with the primary objective of facilitating research and analysis related to the Democratic Republic of Congo. +I do not own the content of the articles, and all rights belong to the respective publishers. The dataset is intended for non-commercial research purposes only. diff --git a/projects/backend/api/bruno.json b/projects/backend/api/bruno.json new file mode 100644 index 0000000..d43c349 --- /dev/null +++ b/projects/backend/api/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "drc-news-corpus", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/projects/backend/api/environments/dev.bru b/projects/backend/api/environments/dev.bru new file mode 100644 index 0000000..d6339c8 --- /dev/null +++ b/projects/backend/api/environments/dev.bru @@ -0,0 +1,7 @@ +vars { + baseUrl: http://localhost:8000/api +} +vars:secret [ + token, + refreshToken +] diff --git a/projects/backend/api/environments/prod.bru b/projects/backend/api/environments/prod.bru new file mode 100644 index 0000000..e326012 --- /dev/null +++ b/projects/backend/api/environments/prod.bru @@ -0,0 +1,7 @@ +vars { + baseUrl: https://devscast.org/api +} +vars:secret [ + refreshToken, + token +] diff --git a/projects/backend/api/feed-management/article/add-comment-to-article.bru b/projects/backend/api/feed-management/article/add-comment-to-article.bru new file mode 100644 index 0000000..acf1376 --- /dev/null +++ b/projects/backend/api/feed-management/article/add-comment-to-article.bru @@ -0,0 +1,25 @@ +meta { + name: add-comment-to-article + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/feed/articles/:articleId/comments + body: json + auth: bearer +} + +params:path { + articleId: 019589b9-7137-7156-9aeb-1e3f0f138a15 +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "content": "this is a comment !" + } +} diff --git a/projects/backend/api/feed-management/article/article-comment-list.bru b/projects/backend/api/feed-management/article/article-comment-list.bru new file mode 100644 index 0000000..0b566a5 --- /dev/null +++ b/projects/backend/api/feed-management/article/article-comment-list.bru @@ -0,0 +1,19 @@ +meta { + name: article-comment-list + type: http + seq: 5 +} + +get { + url: {{baseUrl}}/feed/articles/:articleId/comments + body: none + auth: bearer +} + +params:path { + articleId: 019589b9-7137-7156-9aeb-1e3f0f138a15 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/article/article-details.bru b/projects/backend/api/feed-management/article/article-details.bru new file mode 100644 index 0000000..ef18e1a --- /dev/null +++ b/projects/backend/api/feed-management/article/article-details.bru @@ -0,0 +1,19 @@ +meta { + name: article-details + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/feed/articles/:articleId + body: none + auth: bearer +} + +params:path { + articleId: 019589b9-7137-7156-9aeb-1e3f0f138a15 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/article/article-overview-list.bru b/projects/backend/api/feed-management/article/article-overview-list.bru new file mode 100644 index 0000000..f5d8883 --- /dev/null +++ b/projects/backend/api/feed-management/article/article-overview-list.bru @@ -0,0 +1,24 @@ +meta { + name: article-overview-list + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/feed/articles + body: none + auth: bearer +} + +params:query { + ~lastId: 019589b9-7137-7af1-96b3-9ff7427218fb + ~dateRange[start]: 1740614400 + ~dateRange[end]: 1740700800 + ~page: 22 + ~limit: 100 + ~search: Tshisekedi +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/article/folder.bru b/projects/backend/api/feed-management/article/folder.bru new file mode 100644 index 0000000..7281727 --- /dev/null +++ b/projects/backend/api/feed-management/article/folder.bru @@ -0,0 +1,4 @@ +meta { + name: article + seq: 3 +} diff --git a/projects/backend/api/feed-management/article/remove-comment-from-article.bru b/projects/backend/api/feed-management/article/remove-comment-from-article.bru new file mode 100644 index 0000000..b095418 --- /dev/null +++ b/projects/backend/api/feed-management/article/remove-comment-from-article.bru @@ -0,0 +1,20 @@ +meta { + name: remove-comment-from-article + type: http + seq: 4 +} + +delete { + url: {{baseUrl}}/feed/articles/:articleId/comments/:commentId + body: none + auth: bearer +} + +params:path { + commentId: 01971449-6f1b-724f-bb43-2bc0af698c5f + articleId: 019589b9-7137-7156-9aeb-1e3f0f138a15 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/add-article-to-bookmark.bru b/projects/backend/api/feed-management/bookmark/add-article-to-bookmark.bru new file mode 100644 index 0000000..b522b38 --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/add-article-to-bookmark.bru @@ -0,0 +1,20 @@ +meta { + name: add-article-to-bookmark + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/feed/bookmarks/:bookmarkId/articles/:articleId + body: none + auth: bearer +} + +params:path { + articleId: 01957834-0d68-7a45-9f77-39e66fd92b4f + bookmarkId: 0196d6e6-dd3b-7f46-8097-a8b260dcd2de +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/bookmark-list.bru b/projects/backend/api/feed-management/bookmark/bookmark-list.bru new file mode 100644 index 0000000..6705ab1 --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/bookmark-list.bru @@ -0,0 +1,15 @@ +meta { + name: bookmark-list + type: http + seq: 6 +} + +get { + url: {{baseUrl}}/feed/bookmarks + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/bookmarked-articles-list.bru b/projects/backend/api/feed-management/bookmark/bookmarked-articles-list.bru new file mode 100644 index 0000000..17bff3b --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/bookmarked-articles-list.bru @@ -0,0 +1,19 @@ +meta { + name: bookmarked-articles-list + type: http + seq: 7 +} + +get { + url: {{baseUrl}}/feed/bookmarks/:bookmarkId/articles + body: none + auth: bearer +} + +params:path { + bookmarkId: 0196d6e6-dd3b-7f46-8097-a8b260dcd2de +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/create-bookmark.bru b/projects/backend/api/feed-management/bookmark/create-bookmark.bru new file mode 100644 index 0000000..d4a7096 --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/create-bookmark.bru @@ -0,0 +1,24 @@ +meta { + name: create-bookmark + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/feed/bookmarks + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "name": "read later", + "description": null, + "isPublic": false + } + +} diff --git a/projects/backend/api/feed-management/bookmark/delete-bookmark.bru b/projects/backend/api/feed-management/bookmark/delete-bookmark.bru new file mode 100644 index 0000000..0dc0dfa --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/delete-bookmark.bru @@ -0,0 +1,19 @@ +meta { + name: delete-bookmark + type: http + seq: 2 +} + +delete { + url: {{baseUrl}}/feed/bookmarks/:bookmarkId + body: none + auth: bearer +} + +params:path { + bookmarkId: 0196d1dc-eb76-7481-8ba5-90c73f838411 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/folder.bru b/projects/backend/api/feed-management/bookmark/folder.bru new file mode 100644 index 0000000..f8c4c91 --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/folder.bru @@ -0,0 +1,3 @@ +meta { + name: bookmark +} diff --git a/projects/backend/api/feed-management/bookmark/remove-article-from-bookmark.bru b/projects/backend/api/feed-management/bookmark/remove-article-from-bookmark.bru new file mode 100644 index 0000000..784ec1b --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/remove-article-from-bookmark.bru @@ -0,0 +1,20 @@ +meta { + name: remove-article-from-bookmark + type: http + seq: 4 +} + +delete { + url: {{baseUrl}}/feed/bookmarks/:bookmarkId/articles/:articleId + body: none + auth: bearer +} + +params:path { + articleId: 019549f9-13d8-725b-81f6-3c7e75aa5a26 + bookmarkId: 0196d1dc-eb76-7481-8ba5-90c73f838411 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/bookmark/update-bookmark.bru b/projects/backend/api/feed-management/bookmark/update-bookmark.bru new file mode 100644 index 0000000..2656c66 --- /dev/null +++ b/projects/backend/api/feed-management/bookmark/update-bookmark.bru @@ -0,0 +1,27 @@ +meta { + name: update-bookmark + type: http + seq: 5 +} + +put { + url: {{baseUrl}}/feed/bookmarks/:bookmarkId + body: json + auth: bearer +} + +params:path { + bookmarkId: 0196d1dc-eb76-7481-8ba5-90c73f838411 +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "name": "updated name", + "description": "some description", + "isPublic": true + } +} diff --git a/projects/backend/api/feed-management/folder.bru b/projects/backend/api/feed-management/folder.bru new file mode 100644 index 0000000..9d9c68e --- /dev/null +++ b/projects/backend/api/feed-management/folder.bru @@ -0,0 +1,3 @@ +meta { + name: feed-management +} diff --git a/projects/backend/api/feed-management/source/folder.bru b/projects/backend/api/feed-management/source/folder.bru new file mode 100644 index 0000000..a0c5603 --- /dev/null +++ b/projects/backend/api/feed-management/source/folder.bru @@ -0,0 +1,4 @@ +meta { + name: source + seq: 2 +} diff --git a/projects/backend/api/feed-management/source/follow-source.bru b/projects/backend/api/feed-management/source/follow-source.bru new file mode 100644 index 0000000..11860c7 --- /dev/null +++ b/projects/backend/api/feed-management/source/follow-source.bru @@ -0,0 +1,19 @@ +meta { + name: follow-source + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/feed/sources/:sourceId/follow + body: none + auth: bearer +} + +params:path { + sourceId: 01970f05-a945-7ef0-bfe3-4583491d58d2 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/source/source-article-overview-list.bru b/projects/backend/api/feed-management/source/source-article-overview-list.bru new file mode 100644 index 0000000..c6a44a4 --- /dev/null +++ b/projects/backend/api/feed-management/source/source-article-overview-list.bru @@ -0,0 +1,28 @@ +meta { + name: source-article-overview-list + type: http + seq: 4 +} + +get { + url: {{baseUrl}}/feed/sources/:sourceId/articles + body: none + auth: bearer +} + +params:query { + ~lastId: 019549f9-0962-7fb5-9197-29b1754d13a5 + ~dateRange[start]: 1740614400 + ~dateRange[end]: 1740700800 + ~page: 22 + ~limit: 100 + ~search: Lubumbashi +} + +params:path { + sourceId: 01970f05-a945-7ef0-bfe3-4583491d58d2 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/source/source-details.bru b/projects/backend/api/feed-management/source/source-details.bru new file mode 100644 index 0000000..1d11ab1 --- /dev/null +++ b/projects/backend/api/feed-management/source/source-details.bru @@ -0,0 +1,24 @@ +meta { + name: source-details + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/feed/sources/:sourceId + body: none + auth: bearer +} + +params:path { + sourceId: 01970f05-a945-7ef0-bfe3-4583491d58d2 +} + +headers { + accept: application/json + : +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/source/sources-overview-list.bru b/projects/backend/api/feed-management/source/sources-overview-list.bru new file mode 100644 index 0000000..6d52801 --- /dev/null +++ b/projects/backend/api/feed-management/source/sources-overview-list.bru @@ -0,0 +1,26 @@ +meta { + name: sources-overview-list + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/feed/sources + body: none + auth: bearer +} + +params:query { + ~lastId: 01970f05-a945-7ef0-bfe3-45834b6bc40e + ~limit: 10 + ~page: 1 +} + +headers { + accept: application/json + : +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/feed-management/source/unfollow-source.bru b/projects/backend/api/feed-management/source/unfollow-source.bru new file mode 100644 index 0000000..1c12ad9 --- /dev/null +++ b/projects/backend/api/feed-management/source/unfollow-source.bru @@ -0,0 +1,19 @@ +meta { + name: unfollow-source + type: http + seq: 5 +} + +delete { + url: {{baseUrl}}/feed/sources/:sourceId/unfollow + body: none + auth: bearer +} + +params:path { + sourceId: 01970f05-a945-7ef0-bfe3-4583491d58d2 +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/api/identity-and-access/account-confirm.bru b/projects/backend/api/identity-and-access/account-confirm.bru new file mode 100644 index 0000000..c037dad --- /dev/null +++ b/projects/backend/api/identity-and-access/account-confirm.bru @@ -0,0 +1,15 @@ +meta { + name: account-confirm + type: http + seq: 8 +} + +get { + url: {{baseUrl}}/account/confirm/:token + body: none + auth: none +} + +params:path { + token: WNg8QWMENL77hbCXXkrqyCtYLn5MV7ngEbSJledP9DB6V701LwDPfMJZdkn2 +} diff --git a/projects/backend/api/identity-and-access/account-unlock.bru b/projects/backend/api/identity-and-access/account-unlock.bru new file mode 100644 index 0000000..b49b4fc --- /dev/null +++ b/projects/backend/api/identity-and-access/account-unlock.bru @@ -0,0 +1,15 @@ +meta { + name: account-unlock + type: http + seq: 7 +} + +get { + url: {{baseUrl}}/account/unlock/:token + body: none + auth: none +} + +params:path { + token: KFgBaXF4dxX4PtOMlrpjOoO6g1bkm6zAuvm8ocxC41LwJ27XQOHMn1J7V3kI +} diff --git a/projects/backend/api/identity-and-access/login.bru b/projects/backend/api/identity-and-access/login.bru new file mode 100644 index 0000000..3daae81 --- /dev/null +++ b/projects/backend/api/identity-and-access/login.bru @@ -0,0 +1,18 @@ +meta { + name: login + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/login_check + body: json + auth: none +} + +body:json { + { + "username": "bernard@devscast.tech", + "password": "#New--123pass@" + } +} diff --git a/projects/backend/api/identity-and-access/logout.bru b/projects/backend/api/identity-and-access/logout.bru new file mode 100644 index 0000000..86400c6 --- /dev/null +++ b/projects/backend/api/identity-and-access/logout.bru @@ -0,0 +1,15 @@ +meta { + name: logout + type: http + seq: 10 +} + +post { + url: {{baseUrl}}/token/invalidate + body: none + auth: bearer +} + +auth:bearer { + token: +} diff --git a/projects/backend/api/identity-and-access/password-request.bru b/projects/backend/api/identity-and-access/password-request.bru new file mode 100644 index 0000000..f81c66a --- /dev/null +++ b/projects/backend/api/identity-and-access/password-request.bru @@ -0,0 +1,17 @@ +meta { + name: password-request + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/password/request + body: json + auth: none +} + +body:json { + { + "email": "bernard@devscast.tech" + } +} diff --git a/projects/backend/api/identity-and-access/password-reset.bru b/projects/backend/api/identity-and-access/password-reset.bru new file mode 100644 index 0000000..7a89ad7 --- /dev/null +++ b/projects/backend/api/identity-and-access/password-reset.bru @@ -0,0 +1,22 @@ +meta { + name: password-reset + type: http + seq: 4 +} + +post { + url: {{baseUrl}}/password/reset/:token + body: json + auth: none +} + +params:path { + token: qCdtZkciu7C82LVlnZhjpogfYfxUbApkHdSQmJuFQhqaINHjU2bro5uMzuY3 +} + +body:json { + { + "password": "#New--123pass@", + "confirm": "#New--123pass@" + } +} diff --git a/projects/backend/api/identity-and-access/password-update.bru b/projects/backend/api/identity-and-access/password-update.bru new file mode 100644 index 0000000..a371ca6 --- /dev/null +++ b/projects/backend/api/identity-and-access/password-update.bru @@ -0,0 +1,23 @@ +meta { + name: password-update + type: http + seq: 6 +} + +post { + url: {{baseUrl}}/password/update + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "current": "#1231AZuu*---23213", + "password": "#New--123pass@", + "confirm": "#New--123pass@" + } +} diff --git a/projects/backend/api/identity-and-access/refresh-token.bru b/projects/backend/api/identity-and-access/refresh-token.bru new file mode 100644 index 0000000..a7864b3 --- /dev/null +++ b/projects/backend/api/identity-and-access/refresh-token.bru @@ -0,0 +1,17 @@ +meta { + name: refresh-token + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/token/refresh + body: json + auth: none +} + +body:json { + { + "refresh_token": "{{refreshToken}}" + } +} diff --git a/projects/backend/api/identity-and-access/register.bru b/projects/backend/api/identity-and-access/register.bru new file mode 100644 index 0000000..e50df27 --- /dev/null +++ b/projects/backend/api/identity-and-access/register.bru @@ -0,0 +1,19 @@ +meta { + name: register + type: http + seq: 5 +} + +post { + url: {{baseUrl}}/register + body: json + auth: none +} + +body:json { + { + "name": "bernard", + "email": "bernard@devscast.org", + "password": "#New--123pass@" + } +} diff --git a/projects/backend/api/identity-and-access/user-profile.bru b/projects/backend/api/identity-and-access/user-profile.bru new file mode 100644 index 0000000..4452070 --- /dev/null +++ b/projects/backend/api/identity-and-access/user-profile.bru @@ -0,0 +1,15 @@ +meta { + name: user-profile + type: http + seq: 9 +} + +get { + url: {{baseUrl}}/me + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} diff --git a/projects/backend/behat.yml b/projects/backend/behat.yml new file mode 100644 index 0000000..2f21e1c --- /dev/null +++ b/projects/backend/behat.yml @@ -0,0 +1,16 @@ +default: + suites: + default: + contexts: + - Tests\Behat\Hook\DatabasePurger + paths: + - tests/Behat/features + + formatters: + progress: true + + extensions: + FriendsOfBehat\SymfonyExtension: + bootstrap: tests/bootstrap.php + kernel: + class: App\SharedKernel\Infrastructure\Framework\Symfony\Kernel diff --git a/projects/backend/bin/backup.sh b/projects/backend/bin/backup.sh new file mode 100644 index 0000000..638c197 --- /dev/null +++ b/projects/backend/bin/backup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Get the script directory and define data directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DATA_DIR="${SCRIPT_DIR}/../data" +DUMP_FILE="${DATA_DIR}/data.sql" +ARCHIVE_FILE="${DUMP_FILE}.gz" + +# Ensure data directory exists +mkdir -p "$DATA_DIR" + +# Load environment variables from .env.local +set -a +if [ -f "${SCRIPT_DIR}/../.env.local" ]; then + export "$(grep -v '^#' "${SCRIPT_DIR}/../.env.local" | grep '=' | xargs)" +fi +set +a + +# Parse DATABASE_URL into components +if [[ -z "$DATABASE_URL" ]]; then + echo "DATABASE_URL is not set in .env.local" + exit 1 +fi + +regex="^mysql:\/\/([^:]+):([^@]+)@([^:]+):([0-9]+)\/([^?]+)" +if [[ "$DATABASE_URL" =~ $regex ]]; then + DB_USER="${BASH_REMATCH[1]}" + DB_PASSWORD="${BASH_REMATCH[2]}" + DB_HOST="${BASH_REMATCH[3]}" + DB_PORT="${BASH_REMATCH[4]}" + DB_NAME="${BASH_REMATCH[5]}" +else + echo "Invalid DATABASE_URL format" + exit 1 +fi + +# Step 1: Dump the database +mysqldump --host="${DB_HOST}" --port="${DB_PORT}" \ + --user="${DB_USER}" --password="${DB_PASSWORD}" \ + --max_allowed-packet=1G --net-buffer-length=32704 --skip-extended-insert \ + "${DB_NAME}" > "$DUMP_FILE" + +gzip -f "$DUMP_FILE" + +# Step 2: Send the file to Telegram +curl -F "chat_id=${DEVY_CHANNEL}" \ + -F "message_thread_id=${DEVY_TOPIC}" \ + -F "document=@${ARCHIVE_FILE}" \ + "https://api.telegram.org/bot${DEVY_TOKEN}/sendDocument" + +# Step 3: Clean up +rm -f "$ARCHIVE_FILE" "$DUMP_FILE" diff --git a/projects/backend/bin/console b/projects/backend/bin/console new file mode 100755 index 0000000..2532dac --- /dev/null +++ b/projects/backend/bin/console @@ -0,0 +1,18 @@ +#!/usr/bin/env php + "$LOG_FILE" 2>&1 & +done + +echo "All crawlers started in the background." diff --git a/projects/backend/bin/open-graph-sync.sh b/projects/backend/bin/open-graph-sync.sh new file mode 100755 index 0000000..daa4799 --- /dev/null +++ b/projects/backend/bin/open-graph-sync.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +SOURCES=("7sur7.cd" "actualite.cd" "radiookapi.net" "mediacongo.net" "newscd.net") +BASE_CMD="/usr/bin/php /var/www/html/news.devscast.tech/bin/console app:open-graph" +LOG_DIR="/var/www/html/news.devscast.tech/var" + +mkdir -p "$LOG_DIR" +rm -f "${LOG_DIR}"/*.log + +for SOURCE in "${SOURCES[@]}"; do + LOG_FILE="${LOG_DIR}/${SOURCE}.log" + nohup $BASE_CMD "$SOURCE" -vvv --no-interaction > "$LOG_FILE" 2>&1 & +done + +echo "All open graph crawlers started in the background." diff --git a/projects/backend/bin/stop-crawlers.sh b/projects/backend/bin/stop-crawlers.sh new file mode 100755 index 0000000..9561100 --- /dev/null +++ b/projects/backend/bin/stop-crawlers.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +ps aux | grep '/bin/console app:' | grep -v grep | awk '{print $2}' | xargs -r kill -9 diff --git a/projects/backend/bin/update-async.sh b/projects/backend/bin/update-async.sh new file mode 100755 index 0000000..140b458 --- /dev/null +++ b/projects/backend/bin/update-async.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +SOURCES=("7sur7.cd" "actualite.cd" "radiookapi.net" "mediacongo.net" "newscd.net") +BASE_CMD="/usr/bin/php /var/www/html/news.devscast.tech/bin/console app:update" +LOG_DIR="/var/www/html/news.devscast.tech/var" + +mkdir -p "$LOG_DIR" +rm -f "${LOG_DIR}"/*.log + +for SOURCE in "${SOURCES[@]}"; do + if [[ "$SOURCE" == "7sur7.cd" ]]; then + CATEGORIES=("politique" "economie" "culture" "sport" "societe") + + for CATEGORY in "${CATEGORIES[@]}"; do + LOG_FILE="${LOG_DIR}/${SOURCE}.${CATEGORY}.log" + nohup $BASE_CMD "$SOURCE" --direction=forward -vvv --category="$CATEGORY" > "$LOG_FILE" 2>&1 & + done + else + LOG_FILE="${LOG_DIR}/${SOURCE}.log" + nohup $BASE_CMD "$SOURCE" --direction=forward -vvv > "$LOG_FILE" 2>&1 & + fi +done + +echo "All crawlers started in the background." diff --git a/projects/backend/bin/update-sync.sh b/projects/backend/bin/update-sync.sh new file mode 100755 index 0000000..3fa932f --- /dev/null +++ b/projects/backend/bin/update-sync.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +SOURCES=("7sur7.cd" "actualite.cd" "radiookapi.net" "mediacongo.net" "newscd.net") +BASE_CMD="/usr/bin/php /var/www/html/news.devscast.tech/bin/console app:update" +LOG_DIR="/var/www/html/news.devscast.tech/var" + +mkdir -p "$LOG_DIR" +rm -f "${LOG_DIR}"/*.log + +for SOURCE in "${SOURCES[@]}"; do + if [[ "$SOURCE" == "7sur7.cd" ]]; then + CATEGORIES=("politique" "economie" "culture" "sport" "societe") + + for CATEGORY in "${CATEGORIES[@]}"; do + LOG_FILE="${LOG_DIR}/${SOURCE}.${CATEGORY}.log" + $BASE_CMD "$SOURCE" --direction=forward -vvv --category="$CATEGORY" 2>&1 | tee "$LOG_FILE" + done + else + LOG_FILE="${LOG_DIR}/${SOURCE}.log" + $BASE_CMD "$SOURCE" --direction=forward -vvv 2>&1 | tee "$LOG_FILE" + fi +done + +echo "All crawlers finished." diff --git a/projects/backend/composer.json b/projects/backend/composer.json new file mode 100644 index 0000000..49bdbef --- /dev/null +++ b/projects/backend/composer.json @@ -0,0 +1,140 @@ +{ + "type": "project", + "license": "proprietary", + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.4", + "ext-ctype": "*", + "ext-iconv": "*", + "cweagans/composer-patches": "^1.7.3", + "doctrine/dbal": "^3.9.4", + "doctrine/doctrine-bundle": "^2.13.2", + "doctrine/doctrine-migrations-bundle": "^3.4.1", + "doctrine/orm": "^3.3.1", + "geoip2/geoip2": "^3.1", + "gesdinet/jwt-refresh-token-bundle": "^1.4", + "knplabs/knp-paginator-bundle": "^6.7", + "league/csv": "^9.21", + "lexik/jwt-authentication-bundle": "^3.1", + "matomo/device-detector": "^6.4", + "phpdocumentor/reflection-docblock": "^5.6", + "phpstan/phpdoc-parser": "^2.1", + "sentry/sentry-symfony": "^5.2", + "symfony/console": "7.2.*", + "symfony/css-selector": "7.2.*", + "symfony/dom-crawler": "7.2.*", + "symfony/dotenv": "7.2.*", + "symfony/flex": "^2.4.7", + "symfony/framework-bundle": "7.2.*", + "symfony/http-client": "7.2.*", + "symfony/mailer": "7.2.*", + "symfony/messenger": "7.2.*", + "symfony/monolog-bundle": "^3.10", + "symfony/property-access": "7.2.*", + "symfony/property-info": "7.2.*", + "symfony/runtime": "7.2.*", + "symfony/security-bundle": "7.2.*", + "symfony/serializer": "7.2.*", + "symfony/stopwatch": "7.2.*", + "symfony/twig-bundle": "7.2.*", + "symfony/uid": "7.2.*", + "symfony/validator": "7.2.*", + "symfony/yaml": "7.2.*", + "twig/extra-bundle": "^2.12|^3.19", + "twig/twig": "^2.12|^3.19", + "webmozart/assert": "^1.11", + "ext-dom": "*" + }, + "require-dev": { + "behat/behat": "^3.22", + "deptrac/deptrac": "^2.0.7", + "doctrine/doctrine-fixtures-bundle": "^4.1", + "friends-of-behat/symfony-extension": "^2.6", + "phpstan/phpstan": "^2.1.12", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "^12.1.1", + "rector/rector": "^2.0.12", + "shipmonk/composer-dependency-analyser": "^1.8.2", + "symfony/maker-bundle": "^1.62.1", + "symfony/web-profiler-bundle": "7.2.*", + "symplify/easy-coding-standard": "^12.1.13", + "tomasvotruba/class-leak": "^1.2.7" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true, + "cweagans/composer-patches": true + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ], + "app:cs": [ + "./vendor/bin/ecs check", + "bin/console lint:yaml config --parse-tags", + "bin/console lint:twig templates", + "bin/console lint:container", + "./vendor/bin/phpstan analyse --memory-limit=-1 --configuration=phpstan.dist.neon", + "./vendor/bin/rector --dry-run" + ], + "app:tests": [ + "APP_ENV=test ./vendor/bin/phpunit" + ], + "app:behat": [ + "APP_ENV=test bin/console doctrine:database:create", + "APP_ENV=test bin/console doctrine:migration:migrate --no-interaction --allow-no-migration --all-or-nothing", + "APP_ENV=test ./vendor/bin/behat --format=progress --no-interaction" + ], + "app:env": [ + "APP_RUNTIME_ENV=prod bin/console secrets:decrypt-to-local --force", + "bin/console dotenv:dump prod" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.2.*" + }, + "patches": { + "symfony/monolog-bundle": { + "support telegram topic in configuration": "./patches/monolog-telegram-configuration.patch", + "support telegram topic in extension": "./patches/monolog-telegram-extension.patch" + } + } + } +} diff --git a/projects/backend/composer.lock b/projects/backend/composer.lock new file mode 100644 index 0000000..46d4720 --- /dev/null +++ b/projects/backend/composer.lock @@ -0,0 +1,11379 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b542c5edda5afefd4907959fe98f9e10", + "packages": [ + { + "name": "composer/ca-bundle", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-03-06T14:30:56+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" + }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2025-03-22T10:17:19+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.9.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", + "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.22", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.9.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-01-16T08:28:55+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.14.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/persistence": "^3.1 || ^4", + "doctrine/sql-formatter": "^1.0.1", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" + }, + "conflict": { + "doctrine/annotations": ">=3.0", + "doctrine/cache": "< 1.11", + "doctrine/orm": "<2.17 || >=4.0", + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" + }, + "require-dev": { + "doctrine/annotations": "^1 || ^2", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^12", + "doctrine/deprecations": "^1.0", + "doctrine/orm": "^2.17 || ^3.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9.6.22", + "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^7.2", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.13 || ^3.0.4" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-03-22T17:28:21+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^2.4", + "doctrine/migrations": "^3.2", + "php": "^7.2 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.6 || ^3", + "doctrine/persistence": "^2.0 || ^3", + "phpstan/phpstan": "^1.4 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpstan/phpstan-symfony": "^1.3 || ^2", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/phpunit-bridge": "^6.3 || ^7", + "symfony/var-exporter": "^5.4 || ^6 || ^7" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2025-01-27T22:48:22+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" + }, + "conflict": { + "doctrine/orm": "<2.12 || >=4" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^10.3", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2025-03-26T06:48:45+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/c9557c588b3a70ed93caff069d0aa75737f25609", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpbench/phpbench": "^1.0", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.4.0", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.3.2" + }, + "time": "2025-02-04T19:43:15+00:00" + }, + { + "name": "doctrine/persistence", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", + "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5.38 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-10-30T19:48:12+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + }, + "time": "2025-01-24T11:45:48+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "geoip2/geoip2", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/GeoIP2-php.git", + "reference": "c86fbeaa7e42279dd9e7af0b015384e721832b88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/c86fbeaa7e42279dd9e7af0b015384e721832b88", + "reference": "c86fbeaa7e42279dd9e7af0b015384e721832b88", + "shasum": "" + }, + "require": { + "ext-json": "*", + "maxmind-db/reader": "^1.12.0", + "maxmind/web-service-common": "~0.10", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "GeoIp2\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind GeoIP2 PHP API", + "homepage": "https://github.com/maxmind/GeoIP2-php", + "keywords": [ + "IP", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/GeoIP2-php/issues", + "source": "https://github.com/maxmind/GeoIP2-php/tree/v3.1.0" + }, + "time": "2024-11-15T16:33:31+00:00" + }, + { + "name": "gesdinet/jwt-refresh-token-bundle", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/markitosgv/JWTRefreshTokenBundle.git", + "reference": "a47a373ec0838394bd9c1642c51be39b9c41da84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/markitosgv/JWTRefreshTokenBundle/zipball/a47a373ec0838394bd9c1642c51be39b9c41da84", + "reference": "a47a373ec0838394bd9c1642c51be39b9c41da84", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "lexik/jwt-authentication-bundle": "^2.0|^3.0", + "php": ">=7.4", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/polyfill-php80": "^1.15", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-bundle": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/security-http": "^5.4|^6.0|^7.0" + }, + "conflict": { + "doctrine/mongodb-odm": "<2.2", + "doctrine/orm": "<2.7" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/orm": "^2.7|^3.0", + "matthiasnoback/symfony-config-test": "^4.2|^5.0", + "matthiasnoback/symfony-dependency-injection-test": "^4.2|^5.0", + "phpunit/phpunit": "^9.5", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/security-guard": "^5.4" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Gesdinet\\JWTRefreshTokenBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcos Gómez Vilches", + "email": "marcos@gesdinet.com" + } + ], + "description": "Implements a refresh token system over Json Web Tokens in Symfony", + "keywords": [ + "jwt refresh token bundle symfony json web" + ], + "support": { + "issues": "https://github.com/markitosgv/JWTRefreshTokenBundle/issues", + "source": "https://github.com/markitosgv/JWTRefreshTokenBundle/tree/v1.4.0" + }, + "time": "2024-11-23T09:58:58+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "knplabs/knp-components", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/knp-components.git", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/eabf39263fff305c0024820c3736e5b03e7edf50", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/event-dispatcher-contracts": "^3.0" + }, + "conflict": { + "doctrine/dbal": "<3.8" + }, + "require-dev": { + "doctrine/dbal": "^3.8 || ^4.0", + "doctrine/mongodb-odm": "^2.5.5", + "doctrine/orm": "^2.13 || ^3.0", + "doctrine/phpcr-odm": "^1.8 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.12 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.3", + "propel/propel1": "^1.7", + "ruflin/elastica": "^7.0", + "solarium/solarium": "^6.0", + "symfony/http-foundation": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/http-kernel": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/property-access": "^5.4.38 || ^6.4.4 || ^7.0" + }, + "suggest": { + "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", + "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", + "doctrine/orm": "to allow usage pagination with Doctrine ORM", + "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", + "propel/propel1": "to allow usage pagination with Propel ORM", + "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", + "solarium/solarium": "to allow usage pagination with Solarium Client", + "symfony/http-foundation": "to retrieve arguments from Request", + "symfony/property-access": "to allow sorting arrays" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Component\\": "src/Knp/Component" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/knp-components/contributors" + } + ], + "description": "Knplabs component library", + "homepage": "https://github.com/KnpLabs/knp-components", + "keywords": [ + "components", + "knp", + "knplabs", + "pager", + "paginator" + ], + "support": { + "issues": "https://github.com/KnpLabs/knp-components/issues", + "source": "https://github.com/KnpLabs/knp-components/tree/v5.2.0" + }, + "time": "2025-03-20T07:35:37+00:00" + }, + { + "name": "knplabs/knp-paginator-bundle", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpPaginatorBundle.git", + "reference": "c3ba635cc61c8bc5de4656223d8664c4045e9fdc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpPaginatorBundle/zipball/c3ba635cc61c8bc5de4656223d8664c4045e9fdc", + "reference": "c3ba635cc61c8bc5de4656223d8664c4045e9fdc", + "shasum": "" + }, + "require": { + "knplabs/knp-components": "^4.4 || ^5.0", + "php": "^8.1", + "symfony/config": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/translation": "^6.4 || ^7.0", + "twig/twig": "^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^10.5 || ^11.3", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/templating": "^6.4 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\PaginatorBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle/contributors" + } + ], + "description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle", + "keywords": [ + "bundle", + "knp", + "knplabs", + "pager", + "pagination", + "paginator", + "symfony" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpPaginatorBundle/issues", + "source": "https://github.com/KnpLabs/KnpPaginatorBundle/tree/v6.8.0" + }, + "time": "2025-04-11T07:08:09+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/coding-standard": "^11.1.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^11.3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.3.1" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-09-24T20:45:14+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "a835af59b030d3f2967725697cf88300f579088e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", + "reference": "a835af59b030d3f2967725697cf88300f579088e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.5.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-01-26T21:29:45+00:00" + }, + { + "name": "league/csv", + "version": "9.23.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/774008ad8a634448e4f8e288905e070e8b317ff3", + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1.2" + }, + "require-dev": { + "ext-dom": "*", + "ext-xdebug": "*", + "friendsofphp/php-cs-fixer": "^3.69.0", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^1.12.18", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.7", + "symfony/var-dumper": "^6.4.8 || ^7.2.3" + }, + "suggest": { + "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "League\\Csv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "CSV data manipulation made easy in PHP", + "homepage": "https://csv.thephpleague.com", + "keywords": [ + "convert", + "csv", + "export", + "filter", + "import", + "read", + "transform", + "write" + ], + "support": { + "docs": "https://csv.thephpleague.com", + "issues": "https://github.com/thephpleague/csv/issues", + "rss": "https://github.com/thephpleague/csv/releases.atom", + "source": "https://github.com/thephpleague/csv" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2025-03-28T06:52:04+00:00" + }, + { + "name": "lexik/jwt-authentication-bundle", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git", + "reference": "ebe0e2c6a0ae17b4702feffc89e32e3aaba6cb61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/ebe0e2c6a0ae17b4702feffc89e32e3aaba6cb61", + "reference": "ebe0e2c6a0ae17b4702feffc89e32e3aaba6cb61", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "lcobucci/clock": "^3.0", + "lcobucci/jwt": "^5.0", + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.4|^3.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/security-bundle": "^6.4|^7.0", + "symfony/translation-contracts": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "api-platform/core": "^3.0|^4.0", + "rector/rector": "^1.2", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "suggest": { + "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony", + "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Lexik\\Bundle\\JWTAuthenticationBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Barthe", + "email": "j.barthe@lexik.fr", + "homepage": "https://github.com/jeremyb" + }, + { + "name": "Nicolas Cabot", + "email": "n.cabot@lexik.fr", + "homepage": "https://github.com/slashfan" + }, + { + "name": "Cedric Girard", + "email": "c.girard@lexik.fr", + "homepage": "https://github.com/cedric-g" + }, + { + "name": "Dev Lexik", + "email": "dev@lexik.fr", + "homepage": "https://github.com/lexik" + }, + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com", + "homepage": "https://github.com/chalasr" + }, + { + "name": "Lexik Community", + "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors" + } + ], + "description": "This bundle provides JWT authentication for your Symfony REST API", + "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle", + "keywords": [ + "Authentication", + "JWS", + "api", + "bundle", + "jwt", + "rest", + "symfony" + ], + "support": { + "issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues", + "source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://github.com/chalasr", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle", + "type": "tidelift" + } + ], + "time": "2025-01-06T16:34:57+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "matomo/device-detector", + "version": "6.4.5", + "source": { + "type": "git", + "url": "https://github.com/matomo-org/device-detector.git", + "reference": "270bbc41f80994e80805ac377b67324eba53c412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/270bbc41f80994e80805ac377b67324eba53c412", + "reference": "270bbc41f80994e80805ac377b67324eba53c412", + "shasum": "" + }, + "require": { + "mustangostang/spyc": "*", + "php": "^7.2|^8.0" + }, + "replace": { + "piwik/device-detector": "self.version" + }, + "require-dev": { + "matthiasmullie/scrapbook": "^1.4.7", + "mayflower/mo4-coding-standard": "^v9.0.0", + "phpstan/phpstan": "^1.10.44", + "phpunit/phpunit": "^8.5.8", + "psr/cache": "^1.0.1", + "psr/simple-cache": "^1.0.1", + "slevomat/coding-standard": "<8.16.0", + "symfony/yaml": "^5.1.7" + }, + "suggest": { + "doctrine/cache": "Can directly be used for caching purpose", + "ext-yaml": "Necessary for using the Pecl YAML parser" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeviceDetector\\": "" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The Matomo Team", + "email": "hello@matomo.org", + "homepage": "https://matomo.org/team/" + } + ], + "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.", + "homepage": "https://matomo.org", + "keywords": [ + "devicedetection", + "parser", + "useragent" + ], + "support": { + "forum": "https://forum.matomo.org/", + "issues": "https://github.com/matomo-org/device-detector/issues", + "source": "https://github.com/matomo-org/matomo", + "wiki": "https://dev.matomo.org/" + }, + "time": "2025-02-26T17:37:32+00:00" + }, + { + "name": "maxmind-db/reader", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90", + "reference": "5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "ext-maxminddb": "<1.11.1 || >=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=8.0.0,<10.0.0", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.12.0" + }, + "time": "2024-11-14T22:43:47+00:00" + }, + { + "name": "maxmind/web-service-common", + "version": "v0.10.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/web-service-common-php.git", + "reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4", + "reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0.3", + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Exception\\": "src/Exception", + "MaxMind\\WebService\\": "src/WebService" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory Oschwald", + "email": "goschwald@maxmind.com" + } + ], + "description": "Internal MaxMind Web Service API", + "homepage": "https://github.com/maxmind/web-service-common-php", + "support": { + "issues": "https://github.com/maxmind/web-service-common-php/issues", + "source": "https://github.com/maxmind/web-service-common-php/tree/v0.10.0" + }, + "time": "2024-11-14T23:14:52+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "mustangostang/spyc", + "version": "0.6.3", + "source": { + "type": "git", + "url": "https://github.com/mustangostang/spyc.git", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "Spyc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP", + "homepage": "https://github.com/mustangostang/spyc/", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "support": { + "issues": "https://github.com/mustangostang/spyc/issues", + "source": "https://github.com/mustangostang/spyc/tree/0.6.3" + }, + "time": "2019-09-10T13:16:29+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sentry/sentry", + "version": "4.11.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "ebf67deb9902b6da58a4b3383cbd12fed3f4f555" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/ebf67deb9902b6da58a4b3383cbd12fed3f4f555", + "reference": "ebf67deb9902b6da58a4b3383cbd12fed3f4f555", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5|^9.6", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.11.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2025-04-14T09:04:23+00:00" + }, + { + "name": "sentry/sentry-symfony", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-symfony.git", + "reference": "394576244d8ac03fd2f305b82d23a6fd7a083b45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/394576244d8ac03fd2f305b82d23a6fd7a083b45", + "reference": "394576244d8ac03fd2f305b82d23a6fd7a083b45", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2.1.1", + "jean85/pretty-package-versions": "^1.5||^2.0", + "php": "^7.2||^8.0", + "sentry/sentry": "^4.10.0", + "symfony/cache-contracts": "^1.1||^2.4||^3.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/polyfill-php80": "^1.22", + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0" + }, + "require-dev": { + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6", + "friendsofphp/php-cs-fixer": "^2.19||^3.40", + "masterminds/html5": "^2.8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.12.5", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-symfony": "1.4.10", + "phpunit/phpunit": "^8.5.40||^9.6.21", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/monolog-bundle": "^3.4", + "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0", + "vimeo/psalm": "^4.3||^5.16.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", + "symfony/cache": "Allow distributed tracing of cache pools using Sentry.", + "symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry." + }, + "type": "symfony-bundle", + "autoload": { + "files": [ + "src/aliases.php" + ], + "psr-4": { + "Sentry\\SentryBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "Symfony integration for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "errors", + "logging", + "sentry", + "symfony" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-symfony/issues", + "source": "https://github.com/getsentry/sentry-symfony/tree/5.2.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2025-03-03T07:47:12+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "9131e3018872d2ebb6fe8a9a4d6631273513d42c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/9131e3018872d2ebb6fe8a9a4d6631273513d42c", + "reference": "9131e3018872d2ebb6fe8a9a4d6631273513d42c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-25T15:54:33+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/config", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "7716594aaae91d9141be080240172a92ecca4d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/7716594aaae91d9141be080240172a92ecca4d44", + "reference": "7716594aaae91d9141be080240172a92ecca4d44", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-22T12:07:01+00:00" + }, + { + "name": "symfony/console", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e51498ea18570c062e7df29d05a7003585b19b88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88", + "reference": "e51498ea18570c062e7df29d05a7003585b19b88", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-12T08:11:12+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "58ab71379f14a741755717cece2868bf41ed45d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/58ab71379f14a741755717cece2868bf41ed45d8", + "reference": "58ab71379f14a741755717cece2868bf41ed45d8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.5", + "symfony/var-exporter": "^6.4.20|^7.2.5" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T12:21:46+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "f8a298bbb8eaca08d787bf4d4c74728f1cf98922" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/f8a298bbb8eaca08d787bf4d4c74728f1cf98922", + "reference": "f8a298bbb8eaca08d787bf4d4c74728f1cf98922", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/doctrine-messenger": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4.6|^7.0.6", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/type-info": "^7.1", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-25T15:54:33+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-17T15:53:07+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "28347a897771d0c28e99b75166dd2689099f3045" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/28347a897771d0c28e99b75166dd2689099f3045", + "reference": "28347a897771d0c28e99b75166dd2689099f3045", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-27T11:18:42+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b", + "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-03T07:12:39+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-03T07:50:46+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "c1c6ee8946491b698b067df2258e07918c25da02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c1c6ee8946491b698b067df2258e07918c25da02", + "reference": "c1c6ee8946491b698b067df2258e07918c25da02", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^7.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/filesystem": "^7.1", + "symfony/finder": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^7.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^6.4|^7.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<6.4", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.2", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/scheduler": "^6.4.4|^7.0.4", + "symfony/security-bundle": "^6.4|^7.0", + "symfony/semaphore": "^6.4|^7.0", + "symfony/serializer": "^7.2.5", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/type-info": "^7.1", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/webhook": "^7.2", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-24T12:37:32+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "371272aeb6286f8135e028ca535f8e4d6f114126" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/371272aeb6286f8135e028ca535f8e4d6f114126", + "reference": "371272aeb6286f8135e028ca535f8e4d6f114126", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-25T15:54:33+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b1fe91bc1fa454a806d3f98db4ba826eb9941a54", + "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-28T13:32:50+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", + "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-27T11:08:17+00:00" + }, + { + "name": "symfony/messenger", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "3ea7cdba88df1f36dad96289291a32cd9ab1862f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/3ea7cdba88df1f36dad96289291a32cd9ab1862f", + "reference": "3ea7cdba88df1f36dad96289291a32cd9ab1862f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<7.2", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^7.2", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-04T12:34:02+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-19T08:51:20+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-14T18:16:08+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-20T11:17:29+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "d8bd3d66d074c0acba1214a0d42f5941a8e1e94d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/d8bd3d66d074c0acba1214a0d42f5941a8e1e94d", + "reference": "d8bd3d66d074c0acba1214a0d42f5941a8e1e94d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T12:04:04+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/property-access", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "b28732e315d81fbec787f838034de7d6c9b2b902" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b28732e315d81fbec787f838034de7d6c9b2b902", + "reference": "b28732e315d81fbec787f838034de7d6c9b2b902", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "f00fd9685ecdbabe82ca25c7b739ce7bba99302c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/f00fd9685ecdbabe82ca25c7b739ce7bba99302c", + "reference": "f00fd9685ecdbabe82ca25c7b739ce7bba99302c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/string": "^6.4|^7.0", + "symfony/type-info": "~7.1.9|^7.2.2" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-06T16:27:19+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-26T08:57:56+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" + }, + { + "name": "symfony/runtime", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad", + "reference": "8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.2" + }, + "conflict": { + "symfony/dotenv": "<6.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-29T21:39:47+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "721de227035c6e4c322fb7dd4839586d58bc0cf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/721de227035c6e4c322fb7dd4839586d58bc0cf5", + "reference": "721de227035c6e4c322fb7dd4839586d58bc0cf5", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/security-core": "^7.2", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/security-http": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/console": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<6.4", + "symfony/ldap": "<6.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.12", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-07T09:39:55+00:00" + }, + { + "name": "symfony/security-core", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "466784ffcd0b5a16e05394335897f790b17d07e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/466784ffcd0b5a16e05394335897f790b17d07e4", + "reference": "466784ffcd0b5a16e05394335897f790b17d07e4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-27T11:08:17+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/2b4b0c46c901729e4e90719eacd980381f53e0a3", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T18:42:10+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "8478e95e273f8daa23bf4860dbad2a09d3fb3722" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/8478e95e273f8daa23bf4860dbad2a09d3fb3722", + "reference": "8478e95e273f8daa23bf4860dbad2a09d3fb3722", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/security-core": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-11T16:46:20+00:00" + }, + { + "name": "symfony/serializer", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "d8b75b2c8144c29ac43b235738411f7cca6d584d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/d8b75b2c8144c29ac43b235738411f7cca6d584d", + "reference": "d8b75b2c8144c29ac43b235738411f7cca6d584d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^7.2", + "symfony/error-handler": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.1", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-24T12:37:32+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-24T10:49:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "b1942d5515b7f0a18e16fd668a04ea952db2b0f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/b1942d5515b7f0a18e16fd668a04ea952db2b0f2", + "reference": "b1942d5515b7f0a18e16fd668a04ea952db2b0f2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.12" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<6.4", + "symfony/form": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^6.4|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/emoji": "^7.1", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4.20|^7.2.5", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/security-http": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-28T13:15:09+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "cd2be4563afaef5285bb6e0a06c5445e644a5c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/cd2be4563afaef5285bb6e0a06c5445e644a5c01", + "reference": "cd2be4563afaef5285bb6e0a06c5445e644a5c01", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-23T08:11:15+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/c4824a6b658294c828e609d3d8dbb4e87f6a375d", + "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.0|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-24T09:03:36+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/validator", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "d7edd7f44defbc4e0230512f929b5f4c067bb93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/d7edd7f44defbc4e0230512f929b5f4c067bb93e", + "reference": "d7edd7f44defbc4e0230512f929b5f4c067bb93e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-21T15:05:21+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T11:39:41+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "c37b301818bd7288715d40de634f05781b686ace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c37b301818bd7288715d40de634f05781b686ace", + "reference": "c37b301818bd7288715d40de634f05781b686ace", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T12:21:46+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-03T07:12:39+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.20.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "9df5e1dbb6a68c0665ae5603f6f2c20815647876" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/9df5e1dbb6a68c0665ae5603f6f2c20815647876", + "reference": "9df5e1dbb6a68c0665ae5603f6f2c20815647876", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "twig/twig": "^3.2|^4.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.20.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-02-08T09:47:15+00:00" + }, + { + "name": "twig/twig", + "version": "v3.20.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3468920399451a384bef53cf7996965f7cd40183" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-02-13T08:34:43+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "behat/behat", + "version": "v3.22.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "a93098a77753c3cfdc4c485d75141924a78ceb93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/a93098a77753c3cfdc4c485d75141924a78ceb93", + "reference": "a93098a77753c3cfdc4c485d75141924a78ceb93", + "shasum": "" + }, + "require": { + "behat/gherkin": "^4.12.0", + "composer-runtime-api": "^2.2", + "composer/xdebug-handler": "^3.0", + "ext-mbstring": "*", + "nikic/php-parser": "^5.2", + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ", + "psr/container": "^1.0 || ^2.0", + "symfony/config": "^5.4 || ^6.4 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/translation": "^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.6", + "sebastian/diff": "^4.0", + "symfony/polyfill-php84": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.0" + }, + "suggest": { + "ext-dom": "Needed to output test results in JUnit format." + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Hook\\": "src/Behat/Hook/", + "Behat\\Step\\": "src/Behat/Step/", + "Behat\\Behat\\": "src/Behat/Behat/", + "Behat\\Config\\": "src/Behat/Config/", + "Behat\\Testwork\\": "src/Behat/Testwork/", + "Behat\\Transformation\\": "src/Behat/Transformation/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP", + "homepage": "https://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "support": { + "issues": "https://github.com/Behat/Behat/issues", + "source": "https://github.com/Behat/Behat/tree/v3.22.0" + }, + "time": "2025-05-06T15:25:03+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.12.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cc3a7e224b36373be382b53ef02ede0f1807bb58", + "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" + }, + "require-dev": { + "cucumber/cucumber": "dev-gherkin-24.1.0", + "friendsofphp/php-cs-fixer": "^3.65", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^10.5", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "https://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP", + "homepage": "https://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "support": { + "issues": "https://github.com/Behat/Gherkin/issues", + "source": "https://github.com/Behat/Gherkin/tree/v4.12.0" + }, + "time": "2025-02-26T14:28:23+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "deptrac/deptrac", + "version": "2.0.7", + "source": { + "type": "git", + "url": "https://github.com/deptrac/deptrac.git", + "reference": "815c349a027ed4f0c866405b783f4d4623f1960c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deptrac/deptrac/zipball/815c349a027ed4f0c866405b783f4d4623f1960c", + "reference": "815c349a027ed4f0c866405b783f4d4623f1960c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1" + }, + "suggest": { + "ext-dom": "For using the JUnit output formatter" + }, + "bin": [ + "bin/deptrac", + "deptrac.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Qossmic\\Deptrac\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Deptrac is a static code analysis tool that helps to enforce rules for dependencies between software layers.", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/deptrac/deptrac/issues", + "source": "https://github.com/deptrac/deptrac/tree/2.0.7" + }, + "time": "2025-03-07T14:29:19+00:00" + }, + { + "name": "doctrine/data-fixtures", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^3.1 || ^4.0", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5.3", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/2.0.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2025-01-21T13:21:31+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/a06db6b81ff20a2980bf92063d80c013bb8b4b7c", + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^2.0", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4 || ^3.0 || ^4.0", + "php": "^8.1", + "psr/log": "^2 || ^3", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^6.4 || ^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" + }, + "require-dev": { + "doctrine/coding-standard": "13.0.0", + "phpstan/phpstan": "2.1.11", + "phpunit/phpunit": "^10.5.38 || 11.4.14" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2025-03-26T10:56:26+00:00" + }, + { + "name": "friends-of-behat/symfony-extension", + "version": "v2.6.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfBehat/SymfonyExtension.git", + "reference": "814dc0e253c527b9739072c8b84f6c9b6c51fc58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfBehat/SymfonyExtension/zipball/814dc0e253c527b9739072c8b84f6c9b6c51fc58", + "reference": "814dc0e253c527b9739072c8b84f6c9b6c51fc58", + "shasum": "" + }, + "require": { + "behat/behat": "^3.6.1", + "php": "^8.1", + "symfony/dependency-injection": "^6.2 || ^7.0", + "symfony/http-kernel": "^6.2 || ^7.0" + }, + "require-dev": { + "behat/mink": "^1.9", + "behat/mink-browserkit-driver": "^2.0", + "behat/mink-selenium2-driver": "^1.3", + "friends-of-behat/mink-extension": "^2.5", + "friends-of-behat/page-object-extension": "^0.3.2", + "friends-of-behat/service-container-extension": "^1.1", + "sylius-labs/coding-standard": ">=4.1.1, <=4.2.1", + "symfony/browser-kit": "^6.2 || ^7.0", + "symfony/framework-bundle": "^6.2 || ^7.0", + "symfony/process": "^6.2 || ^7.0", + "symfony/yaml": "^6.2 || ^7.0", + "vimeo/psalm": "4.30.0" + }, + "suggest": { + "behat/mink": "^1.9", + "behat/mink-browserkit-driver": "^2.0", + "friends-of-behat/mink-extension": "^2.5" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "FriendsOfBehat\\SymfonyExtension\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kamil Kokot", + "email": "kamil@kokot.me", + "homepage": "https://kamilkokot.com" + } + ], + "description": "Integrates Behat with Symfony.", + "support": { + "issues": "https://github.com/FriendsOfBehat/SymfonyExtension/issues", + "source": "https://github.com/FriendsOfBehat/SymfonyExtension/tree/v2.6.1" + }, + "time": "2025-05-27T09:31:49+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-02-12T12:17:51+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", + "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-04-16T13:19:18+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "a61a04a361b60014ec04881ccb87252d3bf02e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/a61a04a361b60014ec04881ccb87252d3bf02e94", + "reference": "a61a04a361b60014ec04881ccb87252d3bf02e94", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.3" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "cache/array-adapter": "^1.1", + "composer/semver": "^3.3.2", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^2.0", + "doctrine/collections": "^1.6 || ^2.1", + "doctrine/common": "^2.7 || ^3.0", + "doctrine/dbal": "^3.3.8", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", + "gedmo/doctrine-extensions": "^3.8", + "nesbot/carbon": "^2.49", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-doctrine/issues", + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.2" + }, + "time": "2025-03-03T09:29:16+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/648087fb4dd865a09b1828a3b0396eb447665f2e", + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.2" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.0 || 1.1.1", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.4" + }, + "time": "2025-03-28T12:02:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "05c33d01a856f9f62488d144bafddc3d7b7a4ebb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/05c33d01a856f9f62488d144bafddc3d7b7a4ebb", + "reference": "05c33d01a856f9f62488d144bafddc3d7b7a4ebb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-04-03T14:34:39+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "72ca50e817dd7d65356c16772c30f06c01a6fae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/72ca50e817dd7d65356c16772c30f06c01a6fae2", + "reference": "72ca50e817dd7d65356c16772c30f06c01a6fae2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.1.2", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.0.0", + "sebastian/comparator": "^7.0.1", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.0", + "sebastian/exporter": "^7.0.0", + "sebastian/global-state": "^8.0.0", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.2", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.1-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.1.3" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-04-22T06:11:09+00:00" + }, + { + "name": "rector/rector", + "version": "2.0.12", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "a7f9b968f6c15abfd0d2a1442c9dcd9ade677192" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/a7f9b968f6c15abfd0d2a1442c9dcd9ade677192", + "reference": "a7f9b968f6c15abfd0d2a1442c9dcd9ade677192", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.12" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.0.12" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-04-22T12:47:33+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:53:50+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b478f34614f934e0291598d0c08cbaba9644bee5", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T07:00:32+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8afe311eca49171bf95405cc0078be9a3821f9f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8afe311eca49171bf95405cc0078be9a3821f9f2", + "reference": "8afe311eca49171bf95405cc0078be9a3821f9f2", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:08+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:42+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:59+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:01+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:37:31+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "shipmonk/composer-dependency-analyser", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "f374f5366028fd7ece8aeaffa76895d73dc0a05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/f374f5366028fd7ece8aeaffa76895d73dc0a05a", + "reference": "f374f5366028fd7ece8aeaffa76895d73dc0a05a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.6.0", + "ergebnis/composer-normalize": "^2.19.0", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3.5", + "phpstan/phpstan": "^1.12.3", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "shipmonk/name-collision-detector": "^2.1.1", + "slevomat/coding-standard": "^8.15.0" + }, + "bin": [ + "bin/composer-dependency-analyser" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", + "keywords": [ + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.8.2" + }, + "time": "2024-12-30T12:31:04+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.62.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/468ff2708200c95ebc0d85d3174b6c6711b8a590", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.18|^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.62.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-15T00:21:40+00:00" + }, + { + "name": "symfony/process", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T12:21:46+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4ffde1c860a100533b02697d9aaf5f45759ec26a", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/form": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/serializer": "<7.2" + }, + "require-dev": { + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T14:27:24+00:00" + }, + { + "name": "symplify/easy-coding-standard", + "version": "12.5.12", + "source": { + "type": "git", + "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", + "reference": "cb1ba037e756f8953d99789883879113daeb0ba9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/cb1ba037e756f8953d99789883879113daeb0ba9", + "reference": "cb1ba037e756f8953d99789883879113daeb0ba9", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "<3.46", + "phpcsstandards/php_codesniffer": "<3.8", + "symplify/coding-standard": "<12.1" + }, + "suggest": { + "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter" + }, + "bin": [ + "bin/ecs" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer", + "keywords": [ + "Code style", + "automation", + "fixer", + "static analysis" + ], + "support": { + "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", + "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.5.12" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-04-23T11:23:07+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "tomasvotruba/class-leak", + "version": "1.2.7", + "source": { + "type": "git", + "url": "https://github.com/TomasVotruba/class-leak.git", + "reference": "119d9fb16d4a90114d335b5e7a48cba32d0f30b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TomasVotruba/class-leak/zipball/119d9fb16d4a90114d335b5e7a48cba32d0f30b4", + "reference": "119d9fb16d4a90114d335b5e7a48cba32d0f30b4", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "bin": [ + "bin/class-leak", + "bin/class-leak.php" + ], + "type": "library", + "autoload": { + "psr-4": { + "TomasVotruba\\ClassLeak\\": "app" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect leaking classes", + "support": { + "issues": "https://github.com/TomasVotruba/class-leak/issues", + "source": "https://github.com/TomasVotruba/class-leak/tree/1.2.7" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-12-09T11:29:49+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.4", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-dom": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/projects/backend/config/bundles.php b/projects/backend/config/bundles.php new file mode 100644 index 0000000..9366ef9 --- /dev/null +++ b/projects/backend/config/bundles.php @@ -0,0 +1,19 @@ + ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], + Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true], + Sentry\SentryBundle\SentryBundle::class => ['prod' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], +]; diff --git a/projects/backend/config/doctrine/Aggregator/Entity.Article.orm.xml b/projects/backend/config/doctrine/Aggregator/Entity.Article.orm.xml new file mode 100644 index 0000000..647ef1e --- /dev/null +++ b/projects/backend/config/doctrine/Aggregator/Entity.Article.orm.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/Aggregator/Entity.Source.orm.xml b/projects/backend/config/doctrine/Aggregator/Entity.Source.orm.xml new file mode 100644 index 0000000..e30847d --- /dev/null +++ b/projects/backend/config/doctrine/Aggregator/Entity.Source.orm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/Aggregator/ValueObject.Link.orm.xml b/projects/backend/config/doctrine/Aggregator/ValueObject.Link.orm.xml new file mode 100644 index 0000000..9b63302 --- /dev/null +++ b/projects/backend/config/doctrine/Aggregator/ValueObject.Link.orm.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/projects/backend/config/doctrine/Aggregator/ValueObject.ReadingTime.orm.xml b/projects/backend/config/doctrine/Aggregator/ValueObject.ReadingTime.orm.xml new file mode 100644 index 0000000..79b06a7 --- /dev/null +++ b/projects/backend/config/doctrine/Aggregator/ValueObject.ReadingTime.orm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/Aggregator/ValueObject.Scoring.Credibility.orm.xml b/projects/backend/config/doctrine/Aggregator/ValueObject.Scoring.Credibility.orm.xml new file mode 100644 index 0000000..1370fba --- /dev/null +++ b/projects/backend/config/doctrine/Aggregator/ValueObject.Scoring.Credibility.orm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/FeedManagement/Entity.Bookmark.orm.xml b/projects/backend/config/doctrine/FeedManagement/Entity.Bookmark.orm.xml new file mode 100644 index 0000000..cba7872 --- /dev/null +++ b/projects/backend/config/doctrine/FeedManagement/Entity.Bookmark.orm.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/FeedManagement/Entity.Comment.orm.xml b/projects/backend/config/doctrine/FeedManagement/Entity.Comment.orm.xml new file mode 100644 index 0000000..195fb24 --- /dev/null +++ b/projects/backend/config/doctrine/FeedManagement/Entity.Comment.orm.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/FeedManagement/Entity.FollowedSource.orm.xml b/projects/backend/config/doctrine/FeedManagement/Entity.FollowedSource.orm.xml new file mode 100644 index 0000000..f4cb3f2 --- /dev/null +++ b/projects/backend/config/doctrine/FeedManagement/Entity.FollowedSource.orm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginAttempt.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginAttempt.orm.xml new file mode 100644 index 0000000..8f3b322 --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginAttempt.orm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginHistory.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginHistory.orm.xml new file mode 100644 index 0000000..b941e0f --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/Entity.LoginHistory.orm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/Entity.RefreshToken.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/Entity.RefreshToken.orm.xml new file mode 100644 index 0000000..aa70e52 --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/Entity.RefreshToken.orm.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/Entity.User.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/Entity.User.orm.xml new file mode 100644 index 0000000..ff15e89 --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/Entity.User.orm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/Entity.VerificationToken.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/Entity.VerificationToken.orm.xml new file mode 100644 index 0000000..13b4c5c --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/Entity.VerificationToken.orm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Roles.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Roles.orm.xml new file mode 100644 index 0000000..ce096c6 --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Roles.orm.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Secret.GeneratedToken.orm.xml b/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Secret.GeneratedToken.orm.xml new file mode 100644 index 0000000..4cc375c --- /dev/null +++ b/projects/backend/config/doctrine/IdentityAndAccess/ValueObject.Secret.GeneratedToken.orm.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.Device.orm.xml b/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.Device.orm.xml new file mode 100644 index 0000000..83f319b --- /dev/null +++ b/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.Device.orm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.GeoLocation.orm.xml b/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.GeoLocation.orm.xml new file mode 100644 index 0000000..2296046 --- /dev/null +++ b/projects/backend/config/doctrine/SharedKernel/ValueObject.Tracking.GeoLocation.orm.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/projects/backend/config/migrations/Version20241008030057.php b/projects/backend/config/migrations/Version20241008030057.php new file mode 100644 index 0000000..d60fe25 --- /dev/null +++ b/projects/backend/config/migrations/Version20241008030057.php @@ -0,0 +1,34 @@ + + */ +final class Version20241008030057 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'Add article table'; + } + + #[\Override] + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE article (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', title VARCHAR(255) NOT NULL, body LONGTEXT NOT NULL, link VARCHAR(255) NOT NULL, source VARCHAR(255) NOT NULL, categories VARCHAR(255) DEFAULT NULL, published_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', crawled_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_23A0E6636AC99F1 (link), INDEX IDX_23A0E665F8A7F73 (source), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE article'); + } +} diff --git a/projects/backend/config/migrations/Version20241010041217.php b/projects/backend/config/migrations/Version20241010041217.php new file mode 100644 index 0000000..ade783e --- /dev/null +++ b/projects/backend/config/migrations/Version20241010041217.php @@ -0,0 +1,34 @@ + + */ +final class Version20241010041217 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'remove unique index on article link'; + } + + #[\Override] + public function up(Schema $schema): void + { + $this->addSql('DROP INDEX UNIQ_23A0E6636AC99F1 ON article'); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('CREATE UNIQUE INDEX UNIQ_23A0E6636AC99F1 ON article (link)'); + } +} diff --git a/projects/backend/config/migrations/Version20241010041432.php b/projects/backend/config/migrations/Version20241010041432.php new file mode 100644 index 0000000..7e8550e --- /dev/null +++ b/projects/backend/config/migrations/Version20241010041432.php @@ -0,0 +1,36 @@ + + */ +final class Version20241010041432 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'increase link column size'; + } + + #[\Override] + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE article CHANGE link link VARCHAR(2048) NOT NULL'); + } + + #[\Override] + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE article CHANGE link link VARCHAR(255) NOT NULL'); + } +} diff --git a/projects/backend/config/migrations/Version20241010042241.php b/projects/backend/config/migrations/Version20241010042241.php new file mode 100644 index 0000000..3f4a24e --- /dev/null +++ b/projects/backend/config/migrations/Version20241010042241.php @@ -0,0 +1,37 @@ + + */ +final class Version20241010042241 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'add hash column to article'; + } + + #[\Override] + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE article ADD hash VARCHAR(32) NOT NULL'); + $this->addSql('UPDATE article SET hash = MD5(link)'); + $this->addSql('CREATE INDEX IDX_23A0E66D1B862B8 ON article (hash)'); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX IDX_23A0E66D1B862B8 ON article'); + $this->addSql('ALTER TABLE article DROP hash'); + } +} diff --git a/projects/backend/config/migrations/Version20250314140326.php b/projects/backend/config/migrations/Version20250314140326.php new file mode 100644 index 0000000..05b9a4b --- /dev/null +++ b/projects/backend/config/migrations/Version20250314140326.php @@ -0,0 +1,34 @@ + + */ +final class Version20250314140326 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'add user table'; + } + + #[\Override] + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE user (id BINARY(16) NOT NULL COMMENT \'(DC2Type:user_id)\', name VARCHAR(255) NOT NULL, email VARCHAR(500) NOT NULL, password VARCHAR(4098) NOT NULL, created_at DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', updated_at DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', roles JSON NOT NULL COMMENT \'(DC2Type:json)\', password_reset_token_token VARCHAR(255) DEFAULT NULL, password_reset_token_generated_at DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE user'); + } +} diff --git a/projects/backend/config/migrations/Version20250314145254.php b/projects/backend/config/migrations/Version20250314145254.php new file mode 100644 index 0000000..09d6873 --- /dev/null +++ b/projects/backend/config/migrations/Version20250314145254.php @@ -0,0 +1,34 @@ + + */ +final class Version20250314145254 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'add refresh token'; + } + + #[\Override] + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE refresh_tokens (id INT AUTO_INCREMENT NOT NULL, refresh_token VARCHAR(128) NOT NULL, username VARCHAR(255) NOT NULL, valid DATETIME NOT NULL, UNIQUE INDEX UNIQ_9BACE7E1C74F2195 (refresh_token), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE refresh_tokens'); + } +} diff --git a/projects/backend/config/migrations/Version20250315154326.php b/projects/backend/config/migrations/Version20250315154326.php new file mode 100644 index 0000000..33576a4 --- /dev/null +++ b/projects/backend/config/migrations/Version20250315154326.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE user CHANGE password_reset_token_generated_at password_reset_token_generated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE user CHANGE password_reset_token_generated_at password_reset_token_generated_at DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\''); + } +} diff --git a/projects/backend/config/migrations/Version20250423183329.php b/projects/backend/config/migrations/Version20250423183329.php new file mode 100644 index 0000000..a64bd0d --- /dev/null +++ b/projects/backend/config/migrations/Version20250423183329.php @@ -0,0 +1,83 @@ + + */ +final class Version20250423183329 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'refactoring identity and access module'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE login_attempt (id BINARY(16) NOT NULL COMMENT '(DC2Type:login_attempt_id)', user_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', INDEX IDX_8C11C1BA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE login_history (id BINARY(16) NOT NULL COMMENT '(DC2Type:login_history_id)', user_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', device_operating_system VARCHAR(255) DEFAULT NULL, device_client VARCHAR(255) DEFAULT NULL, device_device VARCHAR(255) DEFAULT NULL, device_is_bot TINYINT(1) DEFAULT 0 NOT NULL, location_time_zone VARCHAR(255) DEFAULT NULL, location_longitude DOUBLE PRECISION DEFAULT NULL, location_latitude DOUBLE PRECISION DEFAULT NULL, location_accuracy_radius INT DEFAULT NULL, INDEX IDX_37976E36A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE verification_token (id BINARY(16) NOT NULL COMMENT '(DC2Type:verification_token_id)', user_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', purpose VARCHAR(255) NOT NULL, created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', token_token VARCHAR(255) DEFAULT NULL, INDEX IDX_C1CC006BA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_attempt ADD CONSTRAINT FK_8C11C1BA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history ADD CONSTRAINT FK_37976E36A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token ADD CONSTRAINT FK_C1CC006BA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE id id BINARY(16) NOT NULL COMMENT '(DC2Type:article_id)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE user ADD is_locked TINYINT(1) DEFAULT 0 NOT NULL, ADD is_confirmed TINYINT(1) DEFAULT 0 NOT NULL, DROP password_reset_token_token, DROP password_reset_token_generated_at + SQL); + + $this->addSql(<<<'SQL' + UPDATE user SET is_locked = 0, is_confirmed = 1 + SQL); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE login_attempt DROP FOREIGN KEY FK_8C11C1BA76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history DROP FOREIGN KEY FK_37976E36A76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token DROP FOREIGN KEY FK_C1CC006BA76ED395 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE login_attempt + SQL); + $this->addSql(<<<'SQL' + DROP TABLE login_history + SQL); + $this->addSql(<<<'SQL' + DROP TABLE verification_token + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE user ADD password_reset_token_token VARCHAR(255) DEFAULT NULL, ADD password_reset_token_generated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', DROP is_locked, DROP is_confirmed + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE id id BINARY(16) NOT NULL COMMENT '(DC2Type:uuid)' + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250423185205.php b/projects/backend/config/migrations/Version20250423185205.php new file mode 100644 index 0000000..6b6d896 --- /dev/null +++ b/projects/backend/config/migrations/Version20250423185205.php @@ -0,0 +1,37 @@ + + */ +final class Version20250423185205 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'add ip to login history'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE login_history ADD ip VARCHAR(45) DEFAULT NULL + SQL); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE login_history DROP ip + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250423190105.php b/projects/backend/config/migrations/Version20250423190105.php new file mode 100644 index 0000000..32af40c --- /dev/null +++ b/projects/backend/config/migrations/Version20250423190105.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE token_token token VARCHAR(255) DEFAULT NULL + SQL); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE token token_token VARCHAR(255) DEFAULT NULL + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250501041246.php b/projects/backend/config/migrations/Version20250501041246.php new file mode 100644 index 0000000..102c9bb --- /dev/null +++ b/projects/backend/config/migrations/Version20250501041246.php @@ -0,0 +1,66 @@ + + */ +final class Version20250501041246 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'introduce new source entity'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE source (name VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL, updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)', bias VARCHAR(255) DEFAULT 'neutral' NOT NULL, reliability VARCHAR(255) DEFAULT 'reliable' NOT NULL, transparency VARCHAR(255) DEFAULT 'medium' NOT NULL, PRIMARY KEY(name)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article ADD updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)', ADD bias VARCHAR(255) DEFAULT 'neutral' NOT NULL, ADD reliability VARCHAR(255) DEFAULT 'reliable' NOT NULL, ADD transparency VARCHAR(255) DEFAULT 'medium' NOT NULL + SQL); + + $this->write("Fetching sources from crawled articles..."); + $sources = $this->connection + ->executeQuery("SELECT DISTINCT source FROM article WHERE source IS NOT NULL") + ->fetchFirstColumn(); + + $this->write(sprintf("%d unique sources found", count($sources))); + + foreach ($sources as $sourceName) { + $this->addSql("INSERT INTO source (name, url) VALUES (:name, :url)", [ + "name" => $sourceName, + "url" => 'https://' . $sourceName + ]); + } + $this->addSql("UPDATE article SET categories = LOWER(categories)"); + + $this->addSql(<<<'SQL' + ALTER TABLE article ADD CONSTRAINT FK_23A0E665F8A7F73 FOREIGN KEY (source) REFERENCES source (name) ON DELETE RESTRICT + SQL); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article DROP FOREIGN KEY FK_23A0E665F8A7F73 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE source + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article DROP updated_at, DROP bias, DROP reliability, DROP transparency + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250501041950.php b/projects/backend/config/migrations/Version20250501041950.php new file mode 100644 index 0000000..66aaece --- /dev/null +++ b/projects/backend/config/migrations/Version20250501041950.php @@ -0,0 +1,37 @@ + + */ +final class Version20250501041950 extends AbstractMigration +{ + #[\Override] + public function getDescription(): string + { + return 'increase title length'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE title title VARCHAR(2048) NOT NULL + SQL); + } + + #[\Override] + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE title title VARCHAR(255) NOT NULL + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250501143015.php b/projects/backend/config/migrations/Version20250501143015.php new file mode 100644 index 0000000..b731578 --- /dev/null +++ b/projects/backend/config/migrations/Version20250501143015.php @@ -0,0 +1,35 @@ + + */ +final class Version20250501143015 extends AbstractMigration +{ + public function getDescription(): string + { + return 'add sentiment score'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article ADD sentiment VARCHAR(255) DEFAULT 'neutral' NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article DROP sentiment + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250502181706.php b/projects/backend/config/migrations/Version20250502181706.php new file mode 100644 index 0000000..ad5fb71 --- /dev/null +++ b/projects/backend/config/migrations/Version20250502181706.php @@ -0,0 +1,35 @@ + + */ +final class Version20250502181706 extends AbstractMigration +{ + public function getDescription(): string + { + return 'add metadata column to article table'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article ADD metadata JSON DEFAULT NULL COMMENT '(DC2Type:open_graph)' + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article DROP metadata + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250502184108.php b/projects/backend/config/migrations/Version20250502184108.php new file mode 100644 index 0000000..721f61b --- /dev/null +++ b/projects/backend/config/migrations/Version20250502184108.php @@ -0,0 +1,33 @@ + + */ +final class Version20250502184108 extends AbstractMigration +{ + public function getDescription(): string + { + return 'relative url to absolue'; + } + + public function up(Schema $schema): void + { + $this->addSql('UPDATE article SET link = CONCAT("https://", source, "/", TRIM(BOTH "/" FROM link)) WHERE link NOT LIKE "http%"'); + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException( + 'This migration is irreversible. You cannot revert the link to relative url.' + ); + } +} diff --git a/projects/backend/config/migrations/Version20250513081958.php b/projects/backend/config/migrations/Version20250513081958.php new file mode 100644 index 0000000..d48b3df --- /dev/null +++ b/projects/backend/config/migrations/Version20250513081958.php @@ -0,0 +1,39 @@ + + */ +final class Version20250513081958 extends AbstractMigration +{ + public function getDescription(): string + { + return 'adding reading time to articles'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article ADD reading_time INT DEFAULT NULL + SQL); + + $this->addSql(<<<'SQL' + UPDATE article SET reading_time = FLOOR(LENGTH(body) - LENGTH(REPLACE(body, ' ', '')) + 1) / 200 + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article DROP reading_time + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250514211949.php b/projects/backend/config/migrations/Version20250514211949.php new file mode 100644 index 0000000..dc262dd --- /dev/null +++ b/projects/backend/config/migrations/Version20250514211949.php @@ -0,0 +1,59 @@ + + */ +final class Version20250514211949 extends AbstractMigration +{ + public function getDescription(): string + { + return '[FeedManagement] add bookmark'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE bookmark (id BINARY(16) NOT NULL COMMENT '(DC2Type:bookmark_id)', user_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', name VARCHAR(255) NOT NULL, description VARCHAR(2048) DEFAULT NULL, is_public TINYINT(1) DEFAULT 0 NOT NULL, created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)', INDEX IDX_DA62921DA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE bookmark_article (bookmark_id BINARY(16) NOT NULL COMMENT '(DC2Type:bookmark_id)', article_id BINARY(16) NOT NULL COMMENT '(DC2Type:article_id)', INDEX IDX_6FE2655D92741D25 (bookmark_id), INDEX IDX_6FE2655D7294869C (article_id), PRIMARY KEY(bookmark_id, article_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark ADD CONSTRAINT FK_DA62921DA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark_article ADD CONSTRAINT FK_6FE2655D92741D25 FOREIGN KEY (bookmark_id) REFERENCES bookmark (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark_article ADD CONSTRAINT FK_6FE2655D7294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE bookmark DROP FOREIGN KEY FK_DA62921DA76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark_article DROP FOREIGN KEY FK_6FE2655D92741D25 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark_article DROP FOREIGN KEY FK_6FE2655D7294869C + SQL); + $this->addSql(<<<'SQL' + DROP TABLE bookmark + SQL); + $this->addSql(<<<'SQL' + DROP TABLE bookmark_article + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250515023707.php b/projects/backend/config/migrations/Version20250515023707.php new file mode 100644 index 0000000..04158c1 --- /dev/null +++ b/projects/backend/config/migrations/Version20250515023707.php @@ -0,0 +1,72 @@ + + */ +final class Version20250515023707 extends AbstractMigration +{ + public function getDescription(): string + { + return 'date_immutable to datetime_immutable'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE updated_at updated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark CHANGE created_at created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', CHANGE updated_at updated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_attempt CHANGE created_at created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history CHANGE created_at created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE source CHANGE updated_at updated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE user CHANGE created_at created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', CHANGE updated_at updated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE created_at created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)' + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE user CHANGE created_at created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', CHANGE updated_at updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark CHANGE created_at created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)', CHANGE updated_at updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE source CHANGE updated_at updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_attempt CHANGE created_at created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE created_at created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history CHANGE created_at created_at DATE NOT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE updated_at updated_at DATE DEFAULT NULL COMMENT '(DC2Type:date_immutable)' + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250516123343.php b/projects/backend/config/migrations/Version20250516123343.php new file mode 100644 index 0000000..7ff6965 --- /dev/null +++ b/projects/backend/config/migrations/Version20250516123343.php @@ -0,0 +1,28 @@ +addSql(<<<'SQL' + ALTER TABLE source ADD display_name VARCHAR(255) DEFAULT NULL, ADD description VARCHAR(2048) DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE source DROP display_name, DROP description + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250517055913.php b/projects/backend/config/migrations/Version20250517055913.php new file mode 100644 index 0000000..cbe26ea --- /dev/null +++ b/projects/backend/config/migrations/Version20250517055913.php @@ -0,0 +1,35 @@ + + */ +final class Version20250517055913 extends AbstractMigration +{ + public function getDescription(): string + { + return '[article] add index on publication date'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE INDEX IDX_23A0E66E0D4FDE1 ON article (published_at) + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX IDX_23A0E66E0D4FDE1 ON article + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250522140030.php b/projects/backend/config/migrations/Version20250522140030.php new file mode 100644 index 0000000..b617689 --- /dev/null +++ b/projects/backend/config/migrations/Version20250522140030.php @@ -0,0 +1,47 @@ + + */ +final class Version20250522140030 extends AbstractMigration +{ + public function getDescription(): string + { + return 'Create FollowedSource table'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE followed_source (id BINARY(16) NOT NULL COMMENT '(DC2Type:followed_source_id)', follower_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', source VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_7A763A3EAC24F853 (follower_id), INDEX IDX_7A763A3E5F8A7F73 (source), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE followed_source ADD CONSTRAINT FK_7A763A3EAC24F853 FOREIGN KEY (follower_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE followed_source ADD CONSTRAINT FK_7A763A3E5F8A7F73 FOREIGN KEY (source) REFERENCES source (name) ON DELETE CASCADE + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE followed_source DROP FOREIGN KEY FK_7A763A3EAC24F853 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE followed_source DROP FOREIGN KEY FK_7A763A3E5F8A7F73 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE followed_source + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250525183408.php b/projects/backend/config/migrations/Version20250525183408.php new file mode 100644 index 0000000..caf9a36 --- /dev/null +++ b/projects/backend/config/migrations/Version20250525183408.php @@ -0,0 +1,65 @@ + + */ +final class Version20250525183408 extends AbstractMigration +{ + public function getDescription(): string + { + return 'optimize data lengths for various fields in the database schema'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE title title VARCHAR(1024) NOT NULL, CHANGE link link VARCHAR(1024) NOT NULL, CHANGE bias bias VARCHAR(30) DEFAULT 'neutral' NOT NULL, CHANGE reliability reliability VARCHAR(30) DEFAULT 'reliable' NOT NULL, CHANGE transparency transparency VARCHAR(30) DEFAULT 'medium' NOT NULL, CHANGE reading_time reading_time INT UNSIGNED DEFAULT 1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark CHANGE description description VARCHAR(512) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history ADD ip_address VARCHAR(15) DEFAULT NULL, DROP ip + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE source CHANGE bias bias VARCHAR(30) DEFAULT 'neutral' NOT NULL, CHANGE reliability reliability VARCHAR(30) DEFAULT 'reliable' NOT NULL, CHANGE transparency transparency VARCHAR(30) DEFAULT 'medium' NOT NULL, CHANGE description description VARCHAR(1024) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE user CHANGE email email VARCHAR(255) NOT NULL, CHANGE password password VARCHAR(512) NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE token token VARCHAR(60) DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE user CHANGE email email VARCHAR(500) NOT NULL, CHANGE password password VARCHAR(4098) NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE bookmark CHANGE description description VARCHAR(2048) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE source CHANGE description description VARCHAR(2048) DEFAULT NULL, CHANGE bias bias VARCHAR(255) DEFAULT 'neutral' NOT NULL, CHANGE reliability reliability VARCHAR(255) DEFAULT 'reliable' NOT NULL, CHANGE transparency transparency VARCHAR(255) DEFAULT 'medium' NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE verification_token CHANGE token token VARCHAR(255) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE login_history ADD ip VARCHAR(45) DEFAULT NULL, DROP ip_address + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE title title VARCHAR(2048) NOT NULL, CHANGE link link VARCHAR(2048) NOT NULL, CHANGE bias bias VARCHAR(255) DEFAULT 'neutral' NOT NULL, CHANGE reliability reliability VARCHAR(255) DEFAULT 'reliable' NOT NULL, CHANGE transparency transparency VARCHAR(255) DEFAULT 'medium' NOT NULL, CHANGE reading_time reading_time INT DEFAULT NULL + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250526101759.php b/projects/backend/config/migrations/Version20250526101759.php new file mode 100644 index 0000000..78e1474 --- /dev/null +++ b/projects/backend/config/migrations/Version20250526101759.php @@ -0,0 +1,47 @@ + + */ +final class Version20250526101759 extends AbstractMigration +{ + public function getDescription(): string + { + return 'add comment table with foreign keys to user and article tables'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE comment (id BINARY(16) NOT NULL COMMENT '(DC2Type:comment_id)', user_id BINARY(16) NOT NULL COMMENT '(DC2Type:user_id)', article_id BINARY(16) NOT NULL COMMENT '(DC2Type:article_id)', content VARCHAR(512) NOT NULL, sentiment VARCHAR(30) DEFAULT 'neutral' NOT NULL, is_spam TINYINT(1) DEFAULT 0 NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_9474526CA76ED395 (user_id), INDEX IDX_9474526C7294869C (article_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE comment ADD CONSTRAINT FK_9474526CA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE comment ADD CONSTRAINT FK_9474526C7294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE comment DROP FOREIGN KEY FK_9474526CA76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE comment DROP FOREIGN KEY FK_9474526C7294869C + SQL); + $this->addSql(<<<'SQL' + DROP TABLE comment + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250526102035.php b/projects/backend/config/migrations/Version20250526102035.php new file mode 100644 index 0000000..76fb007 --- /dev/null +++ b/projects/backend/config/migrations/Version20250526102035.php @@ -0,0 +1,35 @@ + + */ +final class Version20250526102035 extends AbstractMigration +{ + public function getDescription(): string + { + return 'optimize sentiment column in article table'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE sentiment sentiment VARCHAR(30) DEFAULT 'neutral' NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article CHANGE sentiment sentiment VARCHAR(255) DEFAULT 'neutral' NOT NULL + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250526164157.php b/projects/backend/config/migrations/Version20250526164157.php new file mode 100644 index 0000000..be10b34 --- /dev/null +++ b/projects/backend/config/migrations/Version20250526164157.php @@ -0,0 +1,37 @@ + + */ +final class Version20250526164157 extends AbstractMigration +{ + public function getDescription(): string + { + return 'Add image and excerpt columns to article table'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article + ADD image VARCHAR(1024) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.image'))) STORED, + ADD excerpt VARCHAR(255) GENERATED ALWAYS AS (CONCAT(LEFT(body, 200), '...')) STORED + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article DROP image, DROP excerpt + SQL); + } +} diff --git a/projects/backend/config/migrations/Version20250526231341.php b/projects/backend/config/migrations/Version20250526231341.php new file mode 100644 index 0000000..8fc814f --- /dev/null +++ b/projects/backend/config/migrations/Version20250526231341.php @@ -0,0 +1,73 @@ + + */ +final class Version20250526231341 extends AbstractMigration +{ + public function getDescription(): string + { + return 'move from source_name to source_id in article and followed_source tables'; + } + + public function up(Schema $schema): void + { + $this->addSql("SET FOREIGN_KEY_CHECKS = 0"); + + // delete the old indexes and foreign keys + $this->addSql("DROP INDEX `primary` ON source"); + $this->addSql("ALTER TABLE article DROP FOREIGN KEY FK_23A0E665F8A7F73"); + $this->addSql("ALTER TABLE followed_source DROP FOREIGN KEY FK_7A763A3E5F8A7F73"); + $this->addSql("DROP INDEX IDX_23A0E665F8A7F73 ON article"); + $this->addSql("DROP INDEX IDX_7A763A3E5F8A7F73 ON followed_source"); + + // add the new id column to source table + $this->addSql("ALTER TABLE source ADD id BINARY(16) DEFAULT NULL COMMENT '(DC2Type:source_id)' FIRST"); + $sources = $this->connection + ->executeQuery("SELECT name FROM source") + ->fetchFirstColumn(); + + foreach ($sources as $source) { + $this->addSql("UPDATE source SET id = :id WHERE name = :name", [ + "id" => new SourceId()->toBinary(), + "name" => $source, + ]); + } + + // set the id column as NOT NULL and create a unique index + $this->addSql("ALTER TABLE source MODIFY id BINARY(16) NOT NULL COMMENT '(DC2Type:source_id)'"); + $this->addSql("CREATE UNIQUE INDEX UNIQ_5F8A7F735E237E06 ON source (name)"); + $this->addSql("ALTER TABLE source ADD PRIMARY KEY (id)"); + + // Update article table + $this->addSql("ALTER TABLE article ADD source_id BINARY(16) NOT NULL COMMENT '(DC2Type:source_id)'"); + $this->addSql("UPDATE article JOIN source ON article.source = source.name SET article.source_id = source.id"); + $this->addSql("ALTER TABLE article DROP source"); + $this->addSql("ALTER TABLE article ADD CONSTRAINT FK_23A0E66953C1C61 FOREIGN KEY (source_id) REFERENCES source (id) ON DELETE CASCADE"); + $this->addSql(" CREATE INDEX IDX_23A0E66953C1C61 ON article (source_id)"); + + // Update followed_source table + $this->addSql("ALTER TABLE followed_source ADD source_id BINARY(16) NOT NULL COMMENT '(DC2Type:source_id)'"); + $this->addSql("ALTER TABLE followed_source DROP source"); + $this->addSql("ALTER TABLE followed_source ADD CONSTRAINT FK_7A763A3E953C1C61 FOREIGN KEY (source_id) REFERENCES source (id) ON DELETE CASCADE"); + $this->addSql("CREATE INDEX IDX_7A763A3E953C1C61 ON followed_source (source_id)"); + + // Re-enable foreign key checks + $this->addSql("SET FOREIGN_KEY_CHECKS = 1"); + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException('This migration is irreversible.'); + } +} diff --git a/projects/backend/config/migrations/Version20250530121647.php b/projects/backend/config/migrations/Version20250530121647.php new file mode 100644 index 0000000..38a677f --- /dev/null +++ b/projects/backend/config/migrations/Version20250530121647.php @@ -0,0 +1,35 @@ + + */ +final class Version20250530121647 extends AbstractMigration +{ + public function getDescription(): string + { + return 'Create index on article table for published_at and id columns'; + } + + public function up(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE article ADD INDEX IDX_PUBLISHED_AT_ID (published_at DESC, id DESC); + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX IDX_PUBLISHED_AT_ID ON article + SQL); + } +} diff --git a/projects/backend/config/packages/cache.yaml b/projects/backend/config/packages/cache.yaml new file mode 100644 index 0000000..7f85014 --- /dev/null +++ b/projects/backend/config/packages/cache.yaml @@ -0,0 +1,5 @@ +framework: + cache: + pools: + cache.dbal: + adapter: cache.adapter.filesystem diff --git a/projects/backend/config/packages/doctrine.yaml b/projects/backend/config/packages/doctrine.yaml new file mode 100644 index 0000000..37b2c42 --- /dev/null +++ b/projects/backend/config/packages/doctrine.yaml @@ -0,0 +1,86 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '16' + + profiling_collect_backtrace: false + use_savepoints: true + result_cache: 'cache.dbal' + types: + # Shared Kernel + email: App\SharedKernel\Infrastructure\Persistence\Doctrine\DBAL\Types\EmailType + + # Aggregator + article_id: App\Aggregator\Infrastructure\Persistence\Doctrine\DBAL\Types\ArticleIdType + source_id: App\Aggregator\Infrastructure\Persistence\Doctrine\DBAL\Types\SourceIdType + open_graph: App\Aggregator\Infrastructure\Persistence\Doctrine\DBAL\Types\OpenGraphType + + # Identity and Access + user_id: App\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types\UserIdType + login_attempt_id: App\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types\LoginAttemptIdType + login_history_id: App\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types\LoginHistoryIdType + verification_token_id: App\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types\VerificationTokenIdType + + # FeedManagement + bookmark_id: App\FeedManagement\Infrastructure\Persistence\Doctrine\DBAL\Types\BookmarkIdType + followed_source_id: App\FeedManagement\Infrastructure\Persistence\Doctrine\DBAL\Types\FollowedSourceIdType + comment_id: App\FeedManagement\Infrastructure\Persistence\Doctrine\DBAL\Types\CommentIdType + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: false + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + Aggregator: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/config/doctrine/Aggregator' + prefix: 'App\Aggregator\Domain\Model' + IdentityAndAccess: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/config/doctrine/IdentityAndAccess' + prefix: 'App\IdentityAndAccess\Domain\Model' + FeedManagement: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/config/doctrine/FeedManagement' + prefix: 'App\FeedManagement\Domain\Model' + SharedKernel: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/config/doctrine/SharedKernel' + prefix: 'App\SharedKernel\Domain\Model' + controller_resolver: + auto_mapping: false + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/projects/backend/config/packages/doctrine_migrations.yaml b/projects/backend/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..ee12422 --- /dev/null +++ b/projects/backend/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/config/migrations' + enable_profiler: false diff --git a/projects/backend/config/packages/framework.yaml b/projects/backend/config/packages/framework.yaml new file mode 100644 index 0000000..6d85c29 --- /dev/null +++ b/projects/backend/config/packages/framework.yaml @@ -0,0 +1,25 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + http_method_override: false + handle_all_throwables: true + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: null + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native + + #esi: true + #fragments: true + php_errors: + log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/projects/backend/config/packages/gesdinet_jwt_refresh_token.yaml b/projects/backend/config/packages/gesdinet_jwt_refresh_token.yaml new file mode 100644 index 0000000..97aabc3 --- /dev/null +++ b/projects/backend/config/packages/gesdinet_jwt_refresh_token.yaml @@ -0,0 +1,4 @@ +gesdinet_jwt_refresh_token: + refresh_token_class: App\IdentityAndAccess\Domain\Model\Entity\RefreshToken + ttl: 2592000 # 1 month + ttl_update: true diff --git a/projects/backend/config/packages/lexik_jwt_authentication.yaml b/projects/backend/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 0000000..89dbe96 --- /dev/null +++ b/projects/backend/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,5 @@ +lexik_jwt_authentication: + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' + token_ttl: '%env(resolve:JWT_TTL)%' diff --git a/projects/backend/config/packages/mailer.yaml b/projects/backend/config/packages/mailer.yaml new file mode 100644 index 0000000..56a650d --- /dev/null +++ b/projects/backend/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/projects/backend/config/packages/messenger.yaml b/projects/backend/config/packages/messenger.yaml new file mode 100644 index 0000000..5e5f19a --- /dev/null +++ b/projects/backend/config/packages/messenger.yaml @@ -0,0 +1,39 @@ +framework: + messenger: + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. + failure_transport: failed + + transports: + # https://symfony.com/doc/current/messenger.html#transport-configuration + async: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + retry_strategy: + max_retries: 3 + delay: 1000 + multiplier: 2 + max_delay: 0 + options: + auto_setup: true + failed: + dsn: 'doctrine://default?queue_name=failed' + options: + auto_setup: true + sync: 'sync://' + + routing: + App\SharedKernel\Application\Messaging\AsyncMessage: async + Symfony\Component\Mailer\Messenger\SendEmailMessage: sync + + default_bus: command.bus + buses: + command.bus: ~ + query.bus: ~ + message.bus: ~ + +when@test: + framework: + messenger: + transports: + # replace with your transport name here (e.g., my_transport: 'in-memory://') + # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test + async: 'in-memory://' \ No newline at end of file diff --git a/projects/backend/config/packages/monolog.yaml b/projects/backend/config/packages/monolog.yaml new file mode 100644 index 0000000..73c2d44 --- /dev/null +++ b/projects/backend/config/packages/monolog.yaml @@ -0,0 +1,72 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!app"] + app: + type: stream + path: "%kernel.logs_dir%/app.%kernel.environment%.log" + level: debug + channels: ["app"] + console: + type: console + level: debug + process_psr_3_messages: false + channels: ["app"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + telegram: + type: telegram + level: critical + channels: [ "!event" ] + token: "%env(DEVY_TOKEN)%" + channel: "%env(DEVY_CHANNEL)%" + topic: "%env(int:DEVY_TOPIC)%" + parse_mode: "MarkdownV2" + disable_webpage_preview: true + disable_notification: false + split_long_messages: false + formatter: App\SharedKernel\Infrastructure\Framework\Symfony\Logging\TelegramFormatter + nested: + type: rotating_file + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: error + max_files: 10 + console: + type: console + level: info + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: "%kernel.logs_dir%/deprecation.log" diff --git a/projects/backend/config/packages/routing.yaml b/projects/backend/config/packages/routing.yaml new file mode 100644 index 0000000..4b766ce --- /dev/null +++ b/projects/backend/config/packages/routing.yaml @@ -0,0 +1,12 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/projects/backend/config/packages/security.yaml b/projects/backend/config/packages/security.yaml new file mode 100644 index 0000000..c50f1b8 --- /dev/null +++ b/projects/backend/config/packages/security.yaml @@ -0,0 +1,61 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + app_user_provider: + id: App\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\SecurityUserProvider + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: + + login: + pattern: ^/api/login + stateless: true + json_login: + check_path: /api/login_check + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + user_checker: App\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\UserChecker + + api: + pattern: ^/api + stateless: true + entry_point: jwt + jwt: ~ + refresh_jwt: + check_path: /api/token/refresh + logout: + path: api_token_invalidate + user_checker: App\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\UserChecker + + main: + lazy: true + provider: app_user_provider + user_checker: App\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\UserChecker + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/api/register, roles: PUBLIC_ACCESS } + - { path: ^/api/login, roles: PUBLIC_ACCESS } + - { path: ^/api/token/refresh, roles: PUBLIC_ACCESS } + - { path: ^/api/password/(request|reset), roles: PUBLIC_ACCESS } + - { path: ^/api/account/(unlock|confirm), roles: PUBLIC_ACCESS } + - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/projects/backend/config/packages/sentry.yaml b/projects/backend/config/packages/sentry.yaml new file mode 100644 index 0000000..edff7ba --- /dev/null +++ b/projects/backend/config/packages/sentry.yaml @@ -0,0 +1,24 @@ +when@prod: + sentry: + dsn: "%env(SENTRY_DSN)%" + register_error_listener: false + register_error_handler: false + options: + traces_sample_rate: 0.1 + ignore_exceptions: + - 'Symfony\Component\ErrorHandler\Error\FatalError' + - 'Symfony\Component\Debug\Exception\FatalErrorException' + monolog: + handlers: + sentry_fingers_crossed: + type: fingers_crossed + action_level: error + handler: sentry + excluded_http_codes: [404, 405] + buffer_size: 50 + sentry: + type: sentry + level: !php/const Monolog\Logger::ERROR + hub_id: Sentry\State\HubInterface + fill_extra_context: true + process_psr_3_messages: false diff --git a/projects/backend/config/packages/translation.yaml b/projects/backend/config/packages/translation.yaml new file mode 100644 index 0000000..0af682d --- /dev/null +++ b/projects/backend/config/packages/translation.yaml @@ -0,0 +1,7 @@ +framework: + default_locale: fr + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - en + providers: diff --git a/projects/backend/config/packages/twig.yaml b/projects/backend/config/packages/twig.yaml new file mode 100644 index 0000000..a0b2dad --- /dev/null +++ b/projects/backend/config/packages/twig.yaml @@ -0,0 +1,16 @@ +twig: + file_name_pattern: '*.twig' + date: + format: 'd M Y' + interval_format: '%d days' + timezone: null + number_format: + decimals: 2 + decimal_point: ',' + thousands_separator: '.' + globals: + 'application': '@App\SharedKernel\Domain\Application' + +when@test: + twig: + strict_variables: true diff --git a/projects/backend/config/packages/validator.yaml b/projects/backend/config/packages/validator.yaml new file mode 100644 index 0000000..dd47a6a --- /dev/null +++ b/projects/backend/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/projects/backend/config/packages/web_profiler.yaml b/projects/backend/config/packages/web_profiler.yaml new file mode 100644 index 0000000..1e039b7 --- /dev/null +++ b/projects/backend/config/packages/web_profiler.yaml @@ -0,0 +1,11 @@ +when@dev: + web_profiler: + toolbar: true + + framework: + profiler: + collect_serializer_data: true + +when@test: + framework: + profiler: { collect: false } diff --git a/projects/backend/config/preload.php b/projects/backend/config/preload.php new file mode 100644 index 0000000..db37723 --- /dev/null +++ b/projects/backend/config/preload.php @@ -0,0 +1,5 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + + ->withRules([ + NoUnusedImportsFixer::class, + ]) + ->withConfiguredRule(MethodArgumentSpaceFixer::class, [ + 'on_multiline' => 'ensure_fully_multiline', + 'attribute_placement' => 'same_line' + ]) + ->withSkip([ + ConcatSpaceFixer::class + ]) + ->withPreparedSets( + psr12: true, + common: true, + cleanCode: true, + ); \ No newline at end of file diff --git a/projects/backend/geoip_city.mmdb b/projects/backend/geoip_city.mmdb new file mode 100644 index 0000000..0482b28 Binary files /dev/null and b/projects/backend/geoip_city.mmdb differ diff --git a/projects/backend/geoip_country.mmdb b/projects/backend/geoip_country.mmdb new file mode 100644 index 0000000..f83ca7f Binary files /dev/null and b/projects/backend/geoip_country.mmdb differ diff --git a/projects/backend/patches/monolog-telegram-configuration.patch b/projects/backend/patches/monolog-telegram-configuration.patch new file mode 100644 index 0000000..e76d82b --- /dev/null +++ b/projects/backend/patches/monolog-telegram-configuration.patch @@ -0,0 +1,10 @@ +--- a/vendor/symfony/monolog-bundle/DependencyInjection/Configuration.php 2023-11-06 11:08:12.000000000 +0200 ++++ b/vendor/symfony/monolog-bundle/DependencyInjection/Configuration.php 2024-08-16 05:05:49.286417317 +0200 +@@ -595,6 +595,7 @@ + ->booleanNode('disable_notification')->defaultNull()->end() // telegram + ->booleanNode('split_long_messages')->defaultFalse()->end() // telegram + ->booleanNode('delay_between_messages')->defaultFalse()->end() // telegram ++ ->integerNode('topic')->defaultNull()->end() // telegram + ->integerNode('factor')->defaultValue(1)->min(1)->end() // sampling + ->arrayNode('tags') // loggly + ->beforeNormalization() \ No newline at end of file diff --git a/projects/backend/patches/monolog-telegram-extension.patch b/projects/backend/patches/monolog-telegram-extension.patch new file mode 100644 index 0000000..4415ecd --- /dev/null +++ b/projects/backend/patches/monolog-telegram-extension.patch @@ -0,0 +1,9 @@ +--- a/vendor/symfony/monolog-bundle/DependencyInjection/MonologExtension.php 2023-11-06 11:08:12.000000000 +0200 ++++ b/vendor/symfony/monolog-bundle/DependencyInjection/MonologExtension.php 2024-08-16 05:05:49.266417128 +0200 +@@ -351,6 +351,7 @@ + $handler['disable_notification'], + $handler['split_long_messages'], + $handler['delay_between_messages'], ++ $handler['topic'] + ]); + break; \ No newline at end of file diff --git a/projects/backend/phpstan.dist.neon b/projects/backend/phpstan.dist.neon new file mode 100644 index 0000000..5fbf1f5 --- /dev/null +++ b/projects/backend/phpstan.dist.neon @@ -0,0 +1,19 @@ +includes: + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-symfony/rules.neon +# - vendor/phpstan/phpstan-doctrine/extension.neon +# - vendor/phpstan/phpstan-doctrine/rules.neon + +parameters: + level: 8 + paths: + - bin/ + - config/ + - public/ + - src/ + - tests/ + ignoreErrors: + - identifier: missingType.iterableValue +# doctrine: +# objectManagerLoader: tests/object-manager.php +# allowNullablePropertyForRequiredField: true diff --git a/projects/backend/phpunit.xml.dist b/projects/backend/phpunit.xml.dist new file mode 100644 index 0000000..7b454b6 --- /dev/null +++ b/projects/backend/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + tests + + + diff --git a/projects/backend/public/.htaccess b/projects/backend/public/.htaccess new file mode 100644 index 0000000..2776637 --- /dev/null +++ b/projects/backend/public/.htaccess @@ -0,0 +1,66 @@ +# Use the front controller as index file. It serves as a fallback solution when +# every other rewrite/redirect fails (e.g. in an aliased environment without +# mod_rewrite). Additionally, this reduces the matching process for the +# start page (path "/") because otherwise Apache will apply the rewriting rules +# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). +DirectoryIndex index.php + +# By default, Apache does not evaluate symbolic links if you did not enable this +# feature in your server configuration. Uncomment the following line if you +# install assets as symlinks or if you experience problems related to symlinks +# when compiling LESS/Sass/CoffeScript assets. +# Options +FollowSymlinks + +# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve +# to the front controller "/index.php" but be rewritten to "/index.php/index". + + Options -MultiViews + + + + RewriteEngine On + + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$ + RewriteRule .* - [E=BASE:%1] + + # Sets the HTTP_AUTHORIZATION header removed by Apache + RewriteCond %{HTTP:Authorization} .+ + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + + # Redirect to URI without front controller to prevent duplicate content + # (with and without `/index.php`). Only do this redirect on the initial + # rewrite by Apache and not on subsequent cycles. Otherwise we would get an + # endless redirect loop (request -> rewrite to front controller -> + # redirect -> request -> ...). + # So in case you get a "too many redirects" error or you always get redirected + # to the start page because your Apache does not expose the REDIRECT_STATUS + # environment variable, you have 2 choices: + # - disable this feature by commenting the following 2 lines or + # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the + # following RewriteCond (best solution) + RewriteCond %{ENV:REDIRECT_STATUS} ="" + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + # If the requested filename exists, simply serve it. + # We only want to let Apache serve files and not directories. + # Rewrite all other queries to the front controller. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ %{ENV:BASE}/index.php [L] + + + + + # When mod_rewrite is not available, we instruct a temporary redirect of + # the start page to the front controller explicitly so that the website + # and the generated links can still be used. + RedirectMatch 307 ^/$ /index.php/ + # RedirectTemp cannot be used instead + + diff --git a/projects/backend/public/images/sources/7sur7.cd.png b/projects/backend/public/images/sources/7sur7.cd.png new file mode 100644 index 0000000..48a6191 Binary files /dev/null and b/projects/backend/public/images/sources/7sur7.cd.png differ diff --git a/projects/backend/public/images/sources/actualite.cd.png b/projects/backend/public/images/sources/actualite.cd.png new file mode 100644 index 0000000..b6b8b86 Binary files /dev/null and b/projects/backend/public/images/sources/actualite.cd.png differ diff --git a/projects/backend/public/images/sources/beto.cd.png b/projects/backend/public/images/sources/beto.cd.png new file mode 100644 index 0000000..cae839b Binary files /dev/null and b/projects/backend/public/images/sources/beto.cd.png differ diff --git a/projects/backend/public/images/sources/mediacongo.net.png b/projects/backend/public/images/sources/mediacongo.net.png new file mode 100644 index 0000000..bda666d Binary files /dev/null and b/projects/backend/public/images/sources/mediacongo.net.png differ diff --git a/projects/backend/public/images/sources/newscd.net.png b/projects/backend/public/images/sources/newscd.net.png new file mode 100644 index 0000000..98c0ef1 Binary files /dev/null and b/projects/backend/public/images/sources/newscd.net.png differ diff --git a/projects/backend/public/images/sources/radiookapi.net.png b/projects/backend/public/images/sources/radiookapi.net.png new file mode 100644 index 0000000..597ed67 Binary files /dev/null and b/projects/backend/public/images/sources/radiookapi.net.png differ diff --git a/projects/backend/public/index.php b/projects/backend/public/index.php new file mode 100644 index 0000000..79ef949 --- /dev/null +++ b/projects/backend/public/index.php @@ -0,0 +1,7 @@ + new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); diff --git a/projects/backend/rector.php b/projects/backend/rector.php new file mode 100644 index 0000000..758f96b --- /dev/null +++ b/projects/backend/rector.php @@ -0,0 +1,39 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->withImportNames( + importDocBlockNames: false, + importShortClasses: false, + removeUnusedImports: true + ) + ->withPhpVersion(PhpVersion::PHP_84) + ->withPhpSets(php84: true) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + privatization: true, + instanceOf: true, + earlyReturn: true, + doctrineCodeQuality: true + ) + ->withSkip([ + CatchExceptionNameMatchingTypeRector::class + ]); +} catch (InvalidConfigurationException $e) { + echo $e->getMessage(); + exit(1); +} diff --git a/projects/backend/src/Aggregator/Application/EventListener/SourceCrawledListener.php b/projects/backend/src/Aggregator/Application/EventListener/SourceCrawledListener.php new file mode 100644 index 0000000..5dff625 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/EventListener/SourceCrawledListener.php @@ -0,0 +1,38 @@ + + */ +final readonly class SourceCrawledListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private string $crawlingNotificationEmail + ) { + } + + public function __invoke(SourceCrawled $event): void + { + if ($event->notify) { + $email = new SourceCrawledEmail( + EmailAddress::from($this->crawlingNotificationEmail), + $event->event, + $event->source + ); + + $this->mailer->send($email); + } + } +} diff --git a/projects/backend/src/Aggregator/Application/Mailing/SourceCrawledEmail.php b/projects/backend/src/Aggregator/Application/Mailing/SourceCrawledEmail.php new file mode 100644 index 0000000..447785c --- /dev/null +++ b/projects/backend/src/Aggregator/Application/Mailing/SourceCrawledEmail.php @@ -0,0 +1,68 @@ + + */ +final readonly class SourceCrawledEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private string $event, + private string $source, + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'aggregator.emails.source_crawled.subject'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'aggregator/source_crawled'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'source' => $this->source, + 'event' => $this->event, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php b/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php new file mode 100644 index 0000000..200ba6a --- /dev/null +++ b/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php @@ -0,0 +1,44 @@ + + */ +final readonly class ArticleForExport +{ + public function __construct( + public ArticleId $id, + public string $title, + public string $link, + public string $categories, + public string $body, + public string $source, + public string $hash, + public \DateTimeImmutable $publishedAt, + public \DateTimeImmutable $crawledAt + ) { + } + + public static function create(array $item): self + { + return new self( + ArticleId::fromBinary($item['article_id']), + DataMapping::string($item, 'article_title'), + DataMapping::string($item, 'article_link'), + DataMapping::string($item, 'article_categories'), + DataMapping::string($item, 'article_body'), + DataMapping::string($item, 'article_source'), + DataMapping::string($item, 'article_hash'), + DataMapping::datetime($item, 'article_published_at'), + DataMapping::datetime($item, 'article_crawled_at') + ); + } +} diff --git a/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php new file mode 100644 index 0000000..1af2994 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php @@ -0,0 +1,36 @@ + + */ +final readonly class SourceStatistics +{ + public function __construct( + public SourceId $id, + public string $name, + public int $articlesCount, + public int $metadataAvailable, + public ?\DateTimeImmutable $crawledAt = null + ) { + } + + public static function create(array $item): self + { + return new self( + SourceId::fromBinary($item['source_id']), + DataMapping::string($item, 'source_name'), + DataMapping::integer($item, 'articles_count'), + DataMapping::integer($item, 'article_metadata_available'), + DataMapping::nullableDatetime($item, 'source_crawled_at') + ); + } +} diff --git a/projects/backend/src/Aggregator/Application/ReadModel/SourceStatisticsList.php b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatisticsList.php new file mode 100644 index 0000000..8a3fdca --- /dev/null +++ b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatisticsList.php @@ -0,0 +1,28 @@ + + */ +final readonly class SourceStatisticsList +{ + public function __construct( + public array $items, + ) { + Assert::allIsInstanceOf($items, SourceStatistics::class); + } + + public static function create(array $items): self + { + return new self( + array_map(fn (array $item): SourceStatistics => SourceStatistics::create($item), $items), + ); + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Command/CreateArticle.php b/projects/backend/src/Aggregator/Application/UseCase/Command/CreateArticle.php new file mode 100644 index 0000000..d6f12ac --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Command/CreateArticle.php @@ -0,0 +1,27 @@ + + */ +final readonly class CreateArticle +{ + public function __construct( + public string $title, + public Link $link, + public string $categories, + public string $body, + public string $source, + public int $timestamp, + public ?OpenGraphObject $metadata = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Command/CreateSource.php b/projects/backend/src/Aggregator/Application/UseCase/Command/CreateSource.php new file mode 100644 index 0000000..05b3d37 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Command/CreateSource.php @@ -0,0 +1,23 @@ + + */ +final readonly class CreateSource +{ + public function __construct( + public string $name, + public Credibility $credibility, + public ?string $displayName = null, + public ?string $description = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Command/DeleteArticles.php b/projects/backend/src/Aggregator/Application/UseCase/Command/DeleteArticles.php new file mode 100644 index 0000000..722e8ec --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Command/DeleteArticles.php @@ -0,0 +1,19 @@ + + */ +final readonly class DeleteArticles +{ + public function __construct( + public string $source, + public ?string $category = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Command/ExportArticles.php b/projects/backend/src/Aggregator/Application/UseCase/Command/ExportArticles.php new file mode 100644 index 0000000..ccdb5be --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Command/ExportArticles.php @@ -0,0 +1,21 @@ + + */ +final readonly class ExportArticles +{ + public function __construct( + public ?string $source = null, + public ?DateRange $date = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateArticleHandler.php b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateArticleHandler.php new file mode 100644 index 0000000..f21c611 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateArticleHandler.php @@ -0,0 +1,56 @@ + + */ +final readonly class CreateArticleHandler implements CommandHandler +{ + public function __construct( + private SourceRepository $sourceRepository, + private ArticleRepository $articleRepository, + private HashCalculator $hashCalculator + ) { + } + + public function __invoke(CreateArticle $command): void + { + $hash = $this->hashCalculator->calculate((string) $command->link); + $article = $this->articleRepository->getByHash($hash); + if ($article instanceof Article) { + throw DuplicatedArticle::withLink($command->link); + } + + /** @var \DateTimeImmutable $publishedAt */ + $publishedAt = \DateTimeImmutable::createFromFormat('U', (string) $command->timestamp); + $source = $this->sourceRepository->getByName($command->source); + + $article = new Article( + title: $command->title, + link: $command->link, + body: $command->body, + hash: $hash, + categories: mb_strtolower($command->categories), + source: $source, + publishedAt: $publishedAt + ); + $article + ->defineOpenGraph($command->metadata) + ->computeReadingTime(); + + $this->articleRepository->add($article); + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateSourceHandler.php b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateSourceHandler.php new file mode 100644 index 0000000..15909cc --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/CreateSourceHandler.php @@ -0,0 +1,32 @@ + + */ +final readonly class CreateSourceHandler implements CommandHandler +{ + public function __construct( + private SourceRepository $sourceRepository + ) { + } + + public function __invoke(CreateSource $command): void + { + $source = Source::create($command->name, sprintf('https://%s', $command->name)) + ->defineCredibility($command->credibility) + ->defineProfileInfos($command->displayName, $command->description); + + $this->sourceRepository->add($source); + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/DeleteArticlesHandler.php b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/DeleteArticlesHandler.php new file mode 100644 index 0000000..ce45ec8 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/DeleteArticlesHandler.php @@ -0,0 +1,27 @@ + + */ +final readonly class DeleteArticlesHandler implements CommandHandler +{ + public function __construct( + private ArticleRepository $articleRepository, + ) { + } + + public function __invoke(DeleteArticles $command): int + { + return $this->articleRepository->clear($command->source, $command->category); + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/ExportArticlesHandler.php b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/ExportArticlesHandler.php new file mode 100644 index 0000000..596bad8 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/CommandHandler/ExportArticlesHandler.php @@ -0,0 +1,42 @@ + + */ +final readonly class ExportArticlesHandler implements CommandHandler +{ + public function __construct( + private QueryBus $queryBus, + private DataExporter $exporter, + private string $projectDir + ) { + } + + public function __invoke(ExportArticles $command): void + { + $filename = sprintf( + '%s/data/export-%s.csv', + $this->projectDir, + new \DateTimeImmutable('now')->format('U') + ); + + /** @var iterable $articles */ + $articles = $this->queryBus->handle(new GetArticlesForExport($command->source, $command->date)); + + $this->exporter->export($articles, new TransfertSetting($filename)); + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Query/GetArticlesForExport.php b/projects/backend/src/Aggregator/Application/UseCase/Query/GetArticlesForExport.php new file mode 100644 index 0000000..9dc1ad2 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Query/GetArticlesForExport.php @@ -0,0 +1,21 @@ + + */ +final readonly class GetArticlesForExport +{ + public function __construct( + public ?string $source = null, + public ?DateRange $date = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Query/GetEarliestPublicationDate.php b/projects/backend/src/Aggregator/Application/UseCase/Query/GetEarliestPublicationDate.php new file mode 100644 index 0000000..0d4e596 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Query/GetEarliestPublicationDate.php @@ -0,0 +1,19 @@ + + */ +final readonly class GetEarliestPublicationDate +{ + public function __construct( + public string $source, + public ?string $category = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Query/GetLatestPublicationDate.php b/projects/backend/src/Aggregator/Application/UseCase/Query/GetLatestPublicationDate.php new file mode 100644 index 0000000..a8359f3 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Query/GetLatestPublicationDate.php @@ -0,0 +1,19 @@ + + */ +final readonly class GetLatestPublicationDate +{ + public function __construct( + public string $source, + public ?string $category = null + ) { + } +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/Query/GetSourceStatisticsList.php b/projects/backend/src/Aggregator/Application/UseCase/Query/GetSourceStatisticsList.php new file mode 100644 index 0000000..8e41768 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/Query/GetSourceStatisticsList.php @@ -0,0 +1,14 @@ + + */ +final readonly class GetSourceStatisticsList +{ +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetArticlesForExportHandler.php b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetArticlesForExportHandler.php new file mode 100644 index 0000000..9ecc946 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetArticlesForExportHandler.php @@ -0,0 +1,22 @@ + + */ +interface GetArticlesForExportHandler extends QueryHandler +{ + /** + * @return iterable + */ + public function __invoke(GetArticlesForExport $query): iterable; +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetEarliestPublicationDateHandler.php b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetEarliestPublicationDateHandler.php new file mode 100644 index 0000000..d25ff53 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetEarliestPublicationDateHandler.php @@ -0,0 +1,18 @@ + + */ +interface GetEarliestPublicationDateHandler extends QueryHandler +{ + public function __invoke(GetEarliestPublicationDate $query): \DateTimeImmutable; +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetLatestPublicationDateHandler.php b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetLatestPublicationDateHandler.php new file mode 100644 index 0000000..e0717be --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetLatestPublicationDateHandler.php @@ -0,0 +1,18 @@ + + */ +interface GetLatestPublicationDateHandler extends QueryHandler +{ + public function __invoke(GetLatestPublicationDate $query): \DateTimeImmutable; +} diff --git a/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetSourceStatisticsListHandler.php b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetSourceStatisticsListHandler.php new file mode 100644 index 0000000..05433d1 --- /dev/null +++ b/projects/backend/src/Aggregator/Application/UseCase/QueryHandler/GetSourceStatisticsListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetSourceStatisticsListHandler extends QueryHandler +{ + public function __invoke(GetSourceStatisticsList $query): SourceStatisticsList; +} diff --git a/projects/backend/src/Aggregator/Domain/Event/SourceCrawled.php b/projects/backend/src/Aggregator/Domain/Event/SourceCrawled.php new file mode 100644 index 0000000..392685a --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Event/SourceCrawled.php @@ -0,0 +1,20 @@ + + */ +final readonly class SourceCrawled +{ + public function __construct( + public string $event, + public string $source, + public bool $notify = false + ) { + } +} diff --git a/projects/backend/src/Aggregator/Domain/Exception/ArticleNotFound.php b/projects/backend/src/Aggregator/Domain/Exception/ArticleNotFound.php new file mode 100644 index 0000000..a9713ab --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Exception/ArticleNotFound.php @@ -0,0 +1,36 @@ + + */ +final class ArticleNotFound extends \DomainException implements UserFacingError +{ + public static function withId(ArticleId $id): self + { + return new self(sprintf('article with id %s was not found', $id->toString())); + } + + public function translationId(): string + { + return 'aggregator.exceptions.article_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Exception/ArticleOutOfRange.php b/projects/backend/src/Aggregator/Domain/Exception/ArticleOutOfRange.php new file mode 100644 index 0000000..c62b882 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Exception/ArticleOutOfRange.php @@ -0,0 +1,40 @@ + + */ +final class ArticleOutOfRange extends \DomainException implements UserFacingError +{ + public static function with(string $timestamp, DateRange $dateRange): self + { + $date = new \DateTimeImmutable('@' . $timestamp) + ->format('Y-m-d H:i:s'); + $range = $dateRange->format('Y-m-d H:i:s'); + + return new self(sprintf('article with timestamp %s is out of range %s', $date, $range)); + } + + public function translationId(): string + { + return 'aggregator.exceptions.article_out_of_range'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Exception/DuplicatedArticle.php b/projects/backend/src/Aggregator/Domain/Exception/DuplicatedArticle.php new file mode 100644 index 0000000..d459594 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Exception/DuplicatedArticle.php @@ -0,0 +1,36 @@ + + */ +final class DuplicatedArticle extends \DomainException implements UserFacingError +{ + public static function withLink(Link $link): self + { + return new self(sprintf('duplicate article with %s link', (string) $link)); + } + + public function translationId(): string + { + return 'aggregator.exceptions.duplicate_article'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Exception/DuplicatedSource.php b/projects/backend/src/Aggregator/Domain/Exception/DuplicatedSource.php new file mode 100644 index 0000000..a54b990 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Exception/DuplicatedSource.php @@ -0,0 +1,35 @@ + + */ +final class DuplicatedSource extends \DomainException implements UserFacingError +{ + public static function withName(string $name): self + { + return new self(sprintf('duplicate source with %s name', $name)); + } + + public function translationId(): string + { + return 'aggregator.exceptions.duplicate_source'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Exception/SourceNotFound.php b/projects/backend/src/Aggregator/Domain/Exception/SourceNotFound.php new file mode 100644 index 0000000..c5d5a87 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Exception/SourceNotFound.php @@ -0,0 +1,41 @@ + + */ +final class SourceNotFound extends \DomainException implements UserFacingError +{ + public static function withName(string $name): self + { + return new self(sprintf('source with name %s was not found', $name)); + } + + public static function withId(SourceId $sourceId): self + { + return new self(sprintf('source with id %s was not found', $sourceId->toString())); + } + + public function translationId(): string + { + return 'aggregator.exceptions.source_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'aggregator'; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Entity/Article.php b/projects/backend/src/Aggregator/Domain/Model/Entity/Article.php new file mode 100644 index 0000000..a0e6a80 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Entity/Article.php @@ -0,0 +1,95 @@ + + */ +class Article +{ + public readonly ArticleId $id; + + public function __construct( + public readonly string $title, + public readonly Link $link, + public readonly string $body, + public readonly string $hash, + private(set) string $categories, + public readonly Source $source, + public readonly \DateTimeImmutable $publishedAt, + public readonly \DateTimeImmutable $crawledAt = new \DateTimeImmutable(), + private(set) Credibility $credibility = new Credibility(), + private(set) Sentiment $sentiment = Sentiment::NEUTRAL, + private(set) ?OpenGraph $metadata = null, + private(set) ?ReadingTime $readingTime = null, + private(set) ?\DateTimeImmutable $updatedAt = null, + public readonly ?string $image = null, + public readonly ?string $excerpt = null, + ) { + $this->id = new ArticleId(); + } + + public function defineCredibility(Credibility $credibility): self + { + $this->credibility = $credibility; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function defineSentiment(Sentiment $sentiment): self + { + $this->sentiment = $sentiment; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function assignCategories(string $categories): self + { + $this->categories = $categories; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function computeReadingTime(): self + { + $this->readingTime = ReadingTime::fromContent($this->body); + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function defineOpenGraph(?OpenGraphObject $object): self + { + if ($object instanceof OpenGraphObject) { + $image = $object->images[0] ?? null; + $video = $object->videos[0] ?? null; + $audio = $object->audios[0] ?? null; + + $this->metadata = new OpenGraph( + title: $object->title, + description: $object->description, + image: $image->url ?? $image?->secureUrl, + video: $video->url ?? $video?->secureUrl, + audio: $audio->url ?? $audio?->secureUrl, + locale: $object->locale + ); + } + + return $this; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Entity/Category.php b/projects/backend/src/Aggregator/Domain/Model/Entity/Category.php new file mode 100644 index 0000000..c67be9f --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Entity/Category.php @@ -0,0 +1,27 @@ + + */ +class Category +{ + public readonly CategoryId $id; + + public function __construct( + public string $name, + public string $slug, + public array $children = [], + public ?string $description = null, + public ?string $image = null, + ) { + $this->id = new CategoryId(); + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Entity/Source.php b/projects/backend/src/Aggregator/Domain/Model/Entity/Source.php new file mode 100644 index 0000000..0c1b180 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Entity/Source.php @@ -0,0 +1,52 @@ + + */ +class Source +{ + public readonly SourceId $id; + + public function __construct( + public readonly string $name, + public readonly string $url, + private(set) Credibility $credibility = new Credibility(), + private(set) ?string $displayName = null, + private(set) ?string $description = null, + private(set) ?\DateTimeImmutable $updatedAt = null + ) { + $this->id = new SourceId(); + } + + public static function create(string $name, string $url): self + { + return new self($name, $url); + } + + public function defineCredibility(Credibility $credibility): self + { + $this->credibility = $credibility; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function defineProfileInfos(?string $displayName, ?string $description): self + { + $this->displayName = $displayName; + $this->description = $description; + + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Identity/ArticleId.php b/projects/backend/src/Aggregator/Domain/Model/Identity/ArticleId.php new file mode 100644 index 0000000..2612a0d --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Identity/ArticleId.php @@ -0,0 +1,16 @@ + + */ +final class ArticleId extends UuidV7 +{ +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Identity/CategoryId.php b/projects/backend/src/Aggregator/Domain/Model/Identity/CategoryId.php new file mode 100644 index 0000000..5cf6f5c --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Identity/CategoryId.php @@ -0,0 +1,16 @@ + + */ +final class CategoryId extends UuidV7 +{ +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Identity/SourceId.php b/projects/backend/src/Aggregator/Domain/Model/Identity/SourceId.php new file mode 100644 index 0000000..3c305af --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Identity/SourceId.php @@ -0,0 +1,16 @@ + + */ +final class SourceId extends UuidV7 +{ +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Repository/ArticleRepository.php b/projects/backend/src/Aggregator/Domain/Model/Repository/ArticleRepository.php new file mode 100644 index 0000000..785d955 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Repository/ArticleRepository.php @@ -0,0 +1,29 @@ + + */ +interface ArticleRepository +{ + public function add(Article $article): void; + + public function remove(Article $article): void; + + public function getById(ArticleId $id): Article; + + public function getByHash(string $hash): ?Article; + + public function export(?string $source, ?DateRange $date): \Generator; + + public function clear(string $source, ?string $category): int; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/Repository/SourceRepository.php b/projects/backend/src/Aggregator/Domain/Model/Repository/SourceRepository.php new file mode 100644 index 0000000..a3a4748 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/Repository/SourceRepository.php @@ -0,0 +1,24 @@ + + */ +interface SourceRepository +{ + public function add(Source $source): void; + + public function remove(Source $source): void; + + public function getByName(string $name): Source; + + public function getById(SourceId $sourceId): Source; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/CrawlingSettings.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/CrawlingSettings.php new file mode 100644 index 0000000..c87adbd --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/CrawlingSettings.php @@ -0,0 +1,26 @@ + + */ +final readonly class CrawlingSettings +{ + public function __construct( + public string $id, + public ?PageRange $pageRange = null, + public ?DateRange $dateRange = null, + public ?string $category = null, + public bool $notify = false + ) { + Assert::notEmpty($this->id); + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/OpenGraph.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/OpenGraph.php new file mode 100644 index 0000000..3cffdd2 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/OpenGraph.php @@ -0,0 +1,58 @@ + + */ +final readonly class OpenGraph implements \JsonSerializable +{ + public function __construct( + public ?string $title = null, + public ?string $description = null, + public ?string $image = null, + public ?string $video = null, + public ?string $audio = null, + public ?string $locale = null, + ) { + } + + public static function tryFrom(?string $value): ?self + { + if ($value === null) { + return null; + } + + try { + $object = \json_decode($value, true, 512, JSON_THROW_ON_ERROR); + + return new self( + $object['title'] ?? null, + $object['description'] ?? null, + $object['image'] ?? null, + $object['video'] ?? null, + $object['audio'] ?? null, + $object['locale'] ?? null, + ); + } catch (\Throwable) { + return null; + } + } + + #[\Override] + public function jsonSerialize(): array + { + return [ + 'title' => $this->title, + 'description' => $this->description, + 'image' => $this->image, + 'video' => $this->video, + 'audio' => $this->audio, + 'locale' => $this->locale, + ]; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/PageRange.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/PageRange.php new file mode 100644 index 0000000..5835b65 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/PageRange.php @@ -0,0 +1,50 @@ + + */ +final readonly class PageRange implements \Stringable +{ + public int $start; + + public int $end; + + private function __construct(int $start, int $end) + { + Assert::greaterThanEq($start, 0); + Assert::greaterThanEq($end, 0); + Assert::greaterThan($end, $start); + + $this->start = $start; + $this->end = $end; + } + + #[\Override] + public function __toString(): string + { + return $this->start . ':' . $this->end; + } + + public static function from(string $interval): self + { + [$start, $end] = explode(':', $interval); + + $start = (int) $start; + $end = (int) $end; + + return new self($start, $end); + } + + public function inRange(int $page): bool + { + return $page >= $this->start && $page <= $this->end; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/UpdateDirection.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/UpdateDirection.php new file mode 100644 index 0000000..d4f668a --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Crawling/UpdateDirection.php @@ -0,0 +1,16 @@ + + */ +enum UpdateDirection: string +{ + case FORWARD = 'forward'; + case BACKWARD = 'backward'; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Link.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Link.php new file mode 100644 index 0000000..76cfe00 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Link.php @@ -0,0 +1,42 @@ + + */ +final readonly class Link implements \Stringable, \JsonSerializable +{ + public string $link; + + private function __construct(string $url, ?string $source = null) + { + if (! str_starts_with($url, 'http')) { + Assert::notNull($source, 'You must provide a source if the URL is not absolute.'); + $this->link = sprintf('https://%s/%s', $source, trim($url, '/')); + } else { + $this->link = $url; + } + } + + public function __toString(): string + { + return $this->link; + } + + public static function from(string $url, ?string $source = null): self + { + return new self($url, $source); + } + + public function jsonSerialize(): string + { + return $this->link; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/ReadingTime.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/ReadingTime.php new file mode 100644 index 0000000..8e54429 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/ReadingTime.php @@ -0,0 +1,55 @@ + + */ +final readonly class ReadingTime implements \Stringable, \JsonSerializable +{ + public const int WORDS_PER_MINUTE = 200; + + public int $readingTime; + + public function __construct( + string|int $value + ) { + $this->readingTime = is_string($value) ? intval(str_word_count($value) / self::WORDS_PER_MINUTE) : $value; + } + + public function __toString(): string + { + return (string) $this->readingTime; + } + + public static function create(?int $value): self + { + return new self($value ?? 1); + } + + public static function fromContent(string $content): self + { + return new self($content); + } + + public function jsonSerialize(): string + { + return (string) $this->readingTime; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Bias.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Bias.php new file mode 100644 index 0000000..5a6d08e --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Bias.php @@ -0,0 +1,18 @@ + + */ +enum Bias: string +{ + case NEUTRAL = 'neutral'; + case SLIGHTLY = 'slightly'; + case PARTISAN = 'partisan'; + case EXTREME = 'extreme'; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Credibility.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Credibility.php new file mode 100644 index 0000000..ba9d9d7 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Credibility.php @@ -0,0 +1,29 @@ + + */ +final readonly class Credibility implements \JsonSerializable +{ + public function __construct( + public Bias $bias = Bias::NEUTRAL, + public Reliability $reliability = Reliability::RELIABLE, + public Transparency $transparency = Transparency::MEDIUM + ) { + } + + public function jsonSerialize(): mixed + { + return [ + 'bias' => $this->bias->value, + 'reliability' => $this->reliability->value, + 'transparency' => $this->transparency->value, + ]; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Reliability.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Reliability.php new file mode 100644 index 0000000..948ce0c --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Reliability.php @@ -0,0 +1,19 @@ + + */ +enum Reliability: string +{ + case TRUSTED = 'trusted'; + case RELIABLE = 'reliable'; + case AVERAGE = 'average'; + case LOW_TRUST = 'low_trust'; + case UNRELIABLE = 'unreliable'; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Sentiment.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Sentiment.php new file mode 100644 index 0000000..6fc1896 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Sentiment.php @@ -0,0 +1,17 @@ + + */ +enum Sentiment: string +{ + case NEGATIVE = 'negative'; + case POSITIVE = 'positive'; + case NEUTRAL = 'neutral'; +} diff --git a/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Transparency.php b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Transparency.php new file mode 100644 index 0000000..4759756 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Model/ValueObject/Scoring/Transparency.php @@ -0,0 +1,17 @@ + + */ +enum Transparency: string +{ + case HIGH = 'high'; + case MEDIUM = 'medium'; + case LOW = 'low'; +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/DateParser.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/DateParser.php new file mode 100644 index 0000000..f17ec36 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/DateParser.php @@ -0,0 +1,72 @@ + + */ +final readonly class DateParser +{ + public const array MONTHS = [ + 'janvier' => '01', + 'février' => '02', + 'mars' => '03', + 'avril' => '04', + 'mai' => '05', + 'juin' => '06', + 'juillet' => '07', + 'août' => '08', + 'septembre' => '09', + 'octobre' => '10', + 'novembre' => '11', + 'décembre' => '12', + ]; + + public const array DAYS = [ + 'dimanche' => '0', + 'lundi' => '1', + 'mardi' => '2', + 'mercredi' => '3', + 'jeudi' => '4', + 'vendredi' => '5', + 'samedi' => '6', + ]; + + public const string DEFAULT_DATE_FORMAT = 'Y-m-d H:i'; + + /** + * @throws \Throwable + */ + public function createTimeStamp( + string $date, + ?string $format = null, + ?string $pattern = null, + ?string $replacement = null + ): string { + /** @var string $date */ + $date = strtr(strtr(strtolower($date), self::DAYS), self::MONTHS); + if ($pattern !== null && $replacement !== null) { + /** @var string $date */ + $date = preg_replace( + pattern: $pattern, + replacement: $replacement, + subject: $date + ); + } + + if ($format === 'c') { + $date = str_replace('t', ' ', $date); + $format = 'Y-m-d H:i:s'; + } + + $datetime = \DateTime::createFromFormat($format ?? self::DEFAULT_DATE_FORMAT, $date); + + return $datetime !== false ? + $datetime->format('U') : + new \DateTime('midnight')->format('U'); + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Audio.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Audio.php new file mode 100644 index 0000000..b569b2a --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Audio.php @@ -0,0 +1,32 @@ + + */ +final class Audio extends OpenGraphElement +{ + public function __construct( + public ?string $url = null, + public ?string $secureUrl = null, + public ?string $type = null + ) { + } + + public function supportedProperties(): array + { + return [ + OpenGraphProperty::AUDIO_URL => $this->url, + OpenGraphProperty::AUDIO_SECURE_URL => $this->secureUrl, + OpenGraphProperty::AUDIO_TYPE => $this->type, + ]; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Image.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Image.php new file mode 100644 index 0000000..71c881a --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Image.php @@ -0,0 +1,38 @@ + + */ +final class Image extends OpenGraphElement +{ + public function __construct( + public ?string $url = null, + public ?string $secureUrl = null, + public ?string $type = null, + public ?int $width = null, + public ?int $height = null, + public ?bool $userGenerated = null + ) { + } + + public function supportedProperties(): array + { + return [ + OpenGraphProperty::IMAGE => $this->url, + OpenGraphProperty::IMAGE_SECURE_URL => $this->secureUrl, + OpenGraphProperty::IMAGE_TYPE => $this->type, + OpenGraphProperty::IMAGE_WIDTH => $this->width, + OpenGraphProperty::IMAGE_HEIGHT => $this->height, + OpenGraphProperty::IMAGE_USER_GENERATED => $this->userGenerated, + ]; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Video.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Video.php new file mode 100644 index 0000000..8a3f0a3 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Elements/Video.php @@ -0,0 +1,36 @@ + + */ +final class Video extends OpenGraphElement +{ + public function __construct( + public ?string $url = null, + public ?string $secureUrl = null, + public ?string $type = null, + public ?int $width = null, + public ?int $height = null + ) { + } + + public function supportedProperties(): array + { + return [ + OpenGraphProperty::VIDEO_URL => $this->url, + OpenGraphProperty::VIDEO_SECURE_URL => $this->secureUrl, + OpenGraphProperty::VIDEO_TYPE => $this->type, + OpenGraphProperty::VIDEO_WIDTH => $this->width, + OpenGraphProperty::VIDEO_HEIGHT => $this->height, + ]; + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Objects/Website.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Objects/Website.php new file mode 100644 index 0000000..196ede6 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/Objects/Website.php @@ -0,0 +1,16 @@ + + */ +final class Website extends OpenGraphObject +{ +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphConsumer.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphConsumer.php new file mode 100644 index 0000000..62e3b9c --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphConsumer.php @@ -0,0 +1,17 @@ + + */ +interface OpenGraphConsumer +{ + public function consumeUrl(string $url): ?OpenGraphObject; + + public function consumeHtml(string $html, string $fallbackUrl): ?OpenGraphObject; +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphElement.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphElement.php new file mode 100644 index 0000000..e7ee794 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphElement.php @@ -0,0 +1,26 @@ + + */ +abstract class OpenGraphElement +{ + abstract public function supportedProperties(): array; + + public function getProperties(): array + { + return array_filter( + array_map( + fn (string $key, mixed $value): ?OpenGraphProperty => $value !== null ? new OpenGraphProperty($key, $value) : null, + array_keys($this->supportedProperties()), + array_values($this->supportedProperties()) + ), + ); + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphObject.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphObject.php new file mode 100644 index 0000000..44e9a9b --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphObject.php @@ -0,0 +1,283 @@ + + */ +abstract class OpenGraphObject +{ + public function __construct( + public array $audios = [], + public ?string $description = null, + public ?string $determiner = null, + public array $images = [], + public ?string $locale = null, + public array $localeAlternate = [], + public ?bool $richAttachment = null, + public array $seeAlso = [], + public ?string $siteName = null, + public ?string $title = null, + public ?string $type = null, + public ?\DateTimeImmutable $updatedTime = null, + public ?string $url = null, + public array $videos = [] + ) { + } + + public function assignProperties(array $properties, bool $debug = false): void + { + foreach ($properties as $property) { + $name = $property->key; + $value = $property->value; + + switch ($name) { + case OpenGraphProperty::AUDIO: + case OpenGraphProperty::AUDIO_URL: + $this->audios[] = new Audio($value); + break; + case OpenGraphProperty::AUDIO_SECURE_URL: + case OpenGraphProperty::AUDIO_TYPE: + if ($this->audios !== []) { + $this->handleAudioAttribute($this->audios[\count($this->audios) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException( + \sprintf( + "Found '%s' property but no audio was found before.", + $name + ) + ); + } + + break; + case OpenGraphProperty::DESCRIPTION: + if ($this->description === null) { + $this->description = $value; + } + + break; + case OpenGraphProperty::DETERMINER: + if ($this->determiner === null) { + $this->determiner = $value; + } + + break; + case OpenGraphProperty::IMAGE: + case OpenGraphProperty::IMAGE_URL: + $this->images[] = new Image($value); + break; + case OpenGraphProperty::IMAGE_HEIGHT: + case OpenGraphProperty::IMAGE_SECURE_URL: + case OpenGraphProperty::IMAGE_TYPE: + case OpenGraphProperty::IMAGE_WIDTH: + case OpenGraphProperty::IMAGE_USER_GENERATED: + if ($this->images !== []) { + $this->handleImageAttribute($this->images[\count($this->images) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException( + \sprintf( + "Found '%s' property but no image was found before.", + $name + ) + ); + } + + break; + case OpenGraphProperty::LOCALE: + if ($this->locale === null) { + $this->locale = $value; + } + + break; + case OpenGraphProperty::LOCALE_ALTERNATE: + $this->localeAlternate[] = $value; + break; + case OpenGraphProperty::RICH_ATTACHMENT: + $this->richAttachment = $this->convertToBoolean($value); + break; + case OpenGraphProperty::SEE_ALSO: + $this->seeAlso[] = $value; + break; + case OpenGraphProperty::SITE_NAME: + if ($this->siteName === null) { + $this->siteName = $value; + } + + break; + case OpenGraphProperty::TITLE: + if ($this->title === null) { + $this->title = $value; + } + + break; + case OpenGraphProperty::UPDATED_TIME: + if (! $this->updatedTime instanceof \DateTimeImmutable) { + $this->updatedTime = $this->convertToDateTime($value); + } + + break; + case OpenGraphProperty::URL: + if ($this->url === null) { + $this->url = $value; + } + + break; + case OpenGraphProperty::VIDEO: + case OpenGraphProperty::VIDEO_URL: + $this->videos[] = new Video($value); + break; + case OpenGraphProperty::VIDEO_HEIGHT: + case OpenGraphProperty::VIDEO_SECURE_URL: + case OpenGraphProperty::VIDEO_TYPE: + case OpenGraphProperty::VIDEO_WIDTH: + if ($this->videos !== []) { + $this->handleVideoAttribute($this->videos[\count($this->videos) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException(\sprintf( + "Found '%s' property but no video was found before.", + $name + )); + } + } + } + } + + public function getProperties(): array + { + $properties = []; + + foreach ($this->audios as $audio) { + $properties = array_merge($properties, $audio->getProperties()); + } + + if ($this->title !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::TITLE, $this->title); + } + + if ($this->description !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::DESCRIPTION, $this->description); + } + + if ($this->determiner !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::DETERMINER, $this->determiner); + } + + foreach ($this->images as $image) { + $properties = array_merge($properties, $image->getProperties()); + } + + if ($this->locale !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::LOCALE, $this->locale); + } + + foreach ($this->localeAlternate as $locale) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::LOCALE_ALTERNATE, $locale); + } + + if ($this->richAttachment !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::RICH_ATTACHMENT, (int) $this->richAttachment); + } + + foreach ($this->seeAlso as $seeAlso) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::SEE_ALSO, $seeAlso); + } + + if ($this->siteName !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::SITE_NAME, $this->siteName); + } + + if ($this->type !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::TYPE, $this->type); + } + + if ($this->updatedTime instanceof \DateTimeImmutable) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::UPDATED_TIME, $this->updatedTime->format('c')); + } + + if ($this->url !== null) { + $properties[] = new OpenGraphProperty(OpenGraphProperty::URL, $this->url); + } + + foreach ($this->videos as $video) { + $properties = array_merge($properties, $video->getProperties()); + } + + return $properties; + } + + protected function convertToBoolean(string $value): bool + { + return match (strtolower($value)) { + '1', 'true' => true, + default => false, + }; + } + + protected function convertToDateTime(string $value): ?\DateTimeImmutable + { + try { + return new \DateTimeImmutable($value); + } catch (\Throwable) { + return null; + } + } + + private function handleAudioAttribute(Audio $element, string $name, string $value): void + { + switch ($name) { + case OpenGraphProperty::AUDIO_TYPE: + $element->type = $value; + break; + case OpenGraphProperty::AUDIO_SECURE_URL: + $element->secureUrl = $value; + break; + } + } + + private function handleImageAttribute(Image $element, string $name, string $value): void + { + switch ($name) { + case OpenGraphProperty::IMAGE_HEIGHT: + $element->height = (int) $value; + break; + case OpenGraphProperty::IMAGE_WIDTH: + $element->width = (int) $value; + break; + case OpenGraphProperty::IMAGE_TYPE: + $element->type = $value; + break; + case OpenGraphProperty::IMAGE_SECURE_URL: + $element->secureUrl = $value; + break; + case OpenGraphProperty::IMAGE_USER_GENERATED: + $element->userGenerated = $this->convertToBoolean($value); + break; + } + } + + private function handleVideoAttribute(Video $element, string $name, string $value): void + { + switch ($name) { + case OpenGraphProperty::VIDEO_HEIGHT: + $element->height = (int) $value; + break; + case OpenGraphProperty::VIDEO_WIDTH: + $element->width = (int) $value; + break; + case OpenGraphProperty::VIDEO_TYPE: + $element->type = $value; + break; + case OpenGraphProperty::VIDEO_SECURE_URL: + $element->secureUrl = $value; + break; + } + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphProperty.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphProperty.php new file mode 100644 index 0000000..281c864 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/OpenGraph/OpenGraphProperty.php @@ -0,0 +1,75 @@ + + */ +final readonly class OpenGraphProperty +{ + public const string AUDIO = 'og:audio'; + + public const string AUDIO_SECURE_URL = 'og:audio:secure_url'; + + public const string AUDIO_TYPE = 'og:audio:type'; + + public const string AUDIO_URL = 'og:audio:url'; + + public const string DESCRIPTION = 'og:description'; + + public const string DETERMINER = 'og:determiner'; + + public const string IMAGE = 'og:image'; + + public const string IMAGE_HEIGHT = 'og:image:height'; + + public const string IMAGE_SECURE_URL = 'og:image:secure_url'; + + public const string IMAGE_TYPE = 'og:image:type'; + + public const string IMAGE_URL = 'og:image:url'; + + public const string IMAGE_WIDTH = 'og:image:width'; + + public const string IMAGE_USER_GENERATED = 'og:image:user_generated'; + + public const string LOCALE = 'og:locale'; + + public const string LOCALE_ALTERNATE = 'og:locale:alternate'; + + public const string RICH_ATTACHMENT = 'og:rich_attachment'; + + public const string SEE_ALSO = 'og:see_also'; + + public const string SITE_NAME = 'og:site_name'; + + public const string TITLE = 'og:title'; + + public const string TYPE = 'og:type'; + + public const string UPDATED_TIME = 'og:updated_time'; + + public const string URL = 'og:url'; + + public const string VIDEO = 'og:video'; + + public const string VIDEO_HEIGHT = 'og:video:height'; + + public const string VIDEO_SECURE_URL = 'og:video:secure_url'; + + public const string VIDEO_TYPE = 'og:video:type'; + + public const string VIDEO_URL = 'og:video:url'; + + public const string VIDEO_WIDTH = 'og:video:width'; + + public function __construct( + public string $key, + public mixed $value, + ) { + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Crawling/SourceCrawler.php b/projects/backend/src/Aggregator/Domain/Service/Crawling/SourceCrawler.php new file mode 100644 index 0000000..b928a96 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Crawling/SourceCrawler.php @@ -0,0 +1,22 @@ + + */ +interface SourceCrawler +{ + public function fetch(CrawlingSettings $settings): void; + + public function fetchOne(string $html, ?DateRange $dateRange = null): void; + + public function supports(string $source): bool; +} diff --git a/projects/backend/src/Aggregator/Domain/Service/HashCalculator.php b/projects/backend/src/Aggregator/Domain/Service/HashCalculator.php new file mode 100644 index 0000000..e2c5247 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/HashCalculator.php @@ -0,0 +1,18 @@ + + */ +final readonly class HashCalculator +{ + public function calculate(string $data): string + { + return md5($data); + } +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Scoring/CredibilityAnalyser.php b/projects/backend/src/Aggregator/Domain/Service/Scoring/CredibilityAnalyser.php new file mode 100644 index 0000000..8da299f --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Scoring/CredibilityAnalyser.php @@ -0,0 +1,26 @@ + + */ +interface CredibilityAnalyser +{ + public function getBias(string $content): Bias; + + public function getTransparency(string $content): Transparency; + + public function getReliability(string $content): Reliability; + + public function analyse(string $content): Credibility; +} diff --git a/projects/backend/src/Aggregator/Domain/Service/Scoring/SentimentAnalyser.php b/projects/backend/src/Aggregator/Domain/Service/Scoring/SentimentAnalyser.php new file mode 100644 index 0000000..2deac96 --- /dev/null +++ b/projects/backend/src/Aggregator/Domain/Service/Scoring/SentimentAnalyser.php @@ -0,0 +1,17 @@ + + */ +interface SentimentAnalyser +{ + public function analyse(string $content): Sentiment; +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/HttpClientFactory.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/HttpClientFactory.php new file mode 100644 index 0000000..43eb699 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/HttpClientFactory.php @@ -0,0 +1,65 @@ + + */ +final readonly class HttpClientFactory +{ + public function __construct( + private string $projectDir, + private Filesystem $filesystem, + private HttpClientInterface $client, + private LoggerInterface $logger + ) { + } + + public function create(): HttpClientInterface + { + $proxy = $this->getProxy(); + + return $this->client->withOptions([ + 'headers' => [ + 'User-Agent' => UserAgents::random(), + ], + 'proxy' => $proxy !== null ? 'https://' . $proxy : null, + ]); + } + + private function getProxy(): ?string + { + $flag = boolval(getenv('USE_PROXY')); + if ($flag === false) { + return null; + } + + try { + $filename = sprintf('%s/data/proxies.txt', $this->projectDir); + $content = $this->filesystem->readFile($filename); + + /** @var list $proxies */ + $proxies = preg_split('/\r\n|\n|\r/', $content); + $proxies = array_filter($proxies, static fn ($proxy): bool => $proxy !== '' && $proxy !== '0'); + + $proxy = $proxies[array_rand($proxies)]; + $this->logger->info('HttpClient is using proxy: ' . $proxy); + + return $proxy; + } catch (\Throwable $e) { + $this->logger->error('Unable to read proxy file', [ + 'exception' => $e, + ]); + + return null; + } + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/OpenGraph/DomCrawlerConsumer.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/OpenGraph/DomCrawlerConsumer.php new file mode 100644 index 0000000..268e12f --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/OpenGraph/DomCrawlerConsumer.php @@ -0,0 +1,128 @@ + + */ +final readonly class DomCrawlerConsumer implements OpenGraphConsumer +{ + private HttpClientInterface $client; + + public function __construct( + HttpClientFactory $clientFactory, + private LoggerInterface $logger, + private bool $useFallbackMode = true, + private bool $debug = false, + ) { + $this->client = $clientFactory->create(); + } + + public function consumeUrl(string $url): ?OpenGraphObject + { + try { + $response = $this->client->request('GET', $url, [ + 'headers' => [ + 'User-Agent' => UserAgents::OPEN_GRAPH->value, + ], + ])->getContent(); + + return $this->consumeHtml($response, $url); + } catch (\Throwable $e) { + $this->logger->error( + 'Unable to consume OpenGraph URL', + [ + 'url' => $url, + 'exception' => $e, + ] + ); + + return null; + } + } + + public function consumeHtml(string $html, string $fallbackUrl): ?OpenGraphObject + { + try { + $object = $this->consume($html); + + if ($this->useFallbackMode && $object->url === null) { + $object->url = $fallbackUrl; + } + + return $object; + } catch (\Throwable $e) { + $this->logger->error( + 'Unable to consume OpenGraph HTML', + [ + 'html' => $html, + 'exception' => $e, + ] + ); + + return null; + } + } + + private function consume(string $content): OpenGraphObject + { + $crawler = new Crawler($content); + $object = new Website(type: 'website'); + $properties = []; + + foreach (['name', 'property'] as $t) { + $props = []; + + /** @var \DOMElement $tag */ + foreach ($crawler->filter(sprintf("meta[%s^='og:']", $t)) as $tag) { + $name = strtolower(trim($tag->getAttribute($t))); + $value = trim($tag->getAttribute('content')); + $props[] = new OpenGraphProperty($name, $value); + } + + $properties = array_merge($properties, $props); + } + + $object->assignProperties($properties, $this->debug); + + // Fallback for url + if ($this->useFallbackMode && $object->url === null) { + $urlElement = $crawler->filter("link[rel='canonical']")->first(); + if ($urlElement->count() > 0) { + $object->url = trim($urlElement->attr('href') ?? ''); + } + } + + // Fallback for title + if ($this->useFallbackMode && $object->title === null) { + $titleElement = $crawler->filter('title')->first(); + if ($titleElement->count() > 0) { + $object->title = trim($titleElement->text()); + } + } + + // Fallback for description + if ($this->useFallbackMode && $object->description === null) { + $descriptionElement = $crawler->filter("meta[property='description']")->first(); + if ($descriptionElement->count() > 0) { + $object->description = trim($descriptionElement->attr('content') ?? ''); + } + } + + return $object; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/Source.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/Source.php new file mode 100644 index 0000000..bdd5017 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/Source.php @@ -0,0 +1,154 @@ + + */ +#[AutoconfigureTag('app.data_source')] +abstract class Source implements SourceCrawler +{ + protected const string URL = 'url'; + + protected const string ID = 'id'; + + private const string WATCH_EVENT_NAME = 'crawling'; + + protected Stopwatch $stopwatch; + + protected HttpClientInterface $client; + + public function __construct( + HttpClientFactory $clientFactory, + protected EventDispatcherInterface $dispatcher, + protected LoggerInterface $logger, + protected DateParser $dateParser, + protected CommandBus $commandBus, + protected OpenGraphConsumer $openGraphConsumer + ) { + $this->stopwatch = new Stopwatch(); + $this->client = $clientFactory->create(); + } + + #[\Override] + public function supports(string $source): bool + { + return $source === $this->getId(); + } + + abstract public function getPagination(?string $category = null): PageRange; + + protected function getId(): string + { + return static::ID; + } + + protected function getUrl(): string + { + return static::URL; + } + + /** + * @throws \Throwable + */ + protected function crawle(string $url, ?int $page = null): Crawler + { + if ($page !== null) { + $this->logger->notice('> Page ' . $page); + } + + $response = $this->client->request('GET', $url)->getContent(); + return new Crawler($response); + } + + protected function save( + string $title, + string $link, + string $categories, + string $body, + string $timestamp, + ?OpenGraphObject $metadata = null + ): void { + try { + $this->commandBus->handle( + new CreateArticle( + title: $title, + link: Link::from($link, $this->getId()), + categories: $categories, + body: $body, + source: $this->getId(), + timestamp: (int) $timestamp, + metadata: $metadata + ) + ); + $this->logger->notice(sprintf('> %s ✅', $title)); + } catch (\Throwable $e) { + $this->logger->error(sprintf('> %s [Failed] ❌', $e->getMessage())); + } + } + + protected function initialize(): void + { + $this->stopwatch->start(self::WATCH_EVENT_NAME); + $this->logger->notice('Initialized'); + } + + protected function completed(bool $notify = false): void + { + $event = $this->stopwatch->stop(self::WATCH_EVENT_NAME); + $this->dispatcher->dispatch(new SourceCrawled((string) $event, $this->getId(), $notify)); + $this->logger->notice('Done'); + } + + protected function skip(DateRange $dateRange, string $timestamp, string $title, string $date): void + { + if ($dateRange->outRange((int) $timestamp)) { + throw ArticleOutOfRange::with($timestamp, $dateRange); + } + + $this->logger->notice(sprintf('> %s [Skipped %s]', $title, $date)); + } + + /** + * @throws \Throwable + */ + protected function getLastPage(?string $url = null): int + { + $result = []; + + /** @var string $node */ + $node = $this->crawle($url ?? $this->getUrl()) + ->filter('ul.pagination > li a') + ->last() + ->attr('href'); + + /** @var string $query */ + $query = parse_url($node, PHP_URL_QUERY); + parse_str($query, $result); + + return (int) $result['page']; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/SourceCrawler.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/SourceCrawler.php new file mode 100644 index 0000000..f2924be --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/SourceCrawler.php @@ -0,0 +1,63 @@ + + */ +final readonly class SourceCrawler implements SourceCrawlerInterface +{ + /** + * @var iterable + */ + private iterable $sources; + + public function __construct( + #[AutowireIterator('app.data_source')] \Traversable $sources + ) { + $this->sources = iterator_to_array($sources); + } + + #[\Override] + public function fetch(CrawlingSettings $settings): void + { + foreach ($this->sources as $source) { + if ($source->supports($settings->id)) { + $source->fetch($settings); + } + } + } + + #[\Override] + public function supports(string $source): bool + { + return true; + } + + #[\Override] + public function fetchOne(string $html, ?DateRange $dateRange = null): void + { + throw new \RuntimeException('Not implemented'); + } + + public function get(string $id): Source + { + /** @var Source $source */ + foreach ($this->sources as $source) { + if ($source->supports($id)) { + return $source; + } + } + + throw new \RuntimeException('Source not found'); + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/WordPressJson.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/WordPressJson.php new file mode 100644 index 0000000..c0896d6 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/Source/WordPressJson.php @@ -0,0 +1,146 @@ + + */ +class WordPressJson extends Source +{ + public const string POST_QUERY = '_fields=date,slug,link,title.rendered,content.rendered,categories&orderby=date&order=desc'; + + public const string CATEGORY_QUERY = '_fields=id,slug,count&orderby=count&order=desc&per_page=100'; + + public const string TOTAL_PAGES_HEADER = 'x-wp-totalpages'; + + public const string TOTAL_POSTS_HEADER = 'x-wp-total'; + + private array $categoryMap = []; + + #[\Override] + public function getPagination(?string $category = null): PageRange + { + $response = $this->client->request('GET', sprintf('%s/wp-json/wp/v2/posts?_fields=id&per_page=100', $this->getUrl())); + $headers = $response->getHeaders(); + $pages = (int) $headers[self::TOTAL_PAGES_HEADER][0]; + $posts = (int) $headers[self::TOTAL_POSTS_HEADER][0]; + + $this->logger->notice(sprintf('WordPressJson %d posts, %d pages', $posts, $pages)); + return PageRange::from(sprintf('1:%d', $pages)); + } + + #[\Override] + public function fetch(CrawlingSettings $settings): void + { + $this->initialize(); + $page = $settings->pageRange ?? $this->getPagination(); + + for ($i = $page->start; $i <= $page->end; $i++) { + try { + $response = $this->client->request( + method: 'GET', + url: sprintf('%s/wp-json/wp/v2/posts?%s&page=%d&per_page=100', $this->getUrl(), self::POST_QUERY, $i) + ); + + /** @var array $articles */ + $articles = json_decode($this->removeMisconfigurationError($response->getContent()), true); + } catch (\Throwable $e) { + $this->logger->error(sprintf('> page %d => %s [Failed] ❌', $i, $e->getMessage())); + continue; + } + + try { + foreach ($articles as $article) { + $this->fetchOne((string) json_encode($article), $settings->dateRange); + } + } catch (ArticleOutOfRange) { + $this->logger->notice('No more articles to fetch in this range.'); + break; + } + } + + $this->completed($settings->notify); + } + + #[\Override] + public function fetchOne(string $html, ?DateRange $dateRange = null): void + { + try { + /** + * @var array{ + * link:string, + * title:array{rendered:string}, + * content:array{rendered:string}, + * date:string, + * categories:int[] + * } $data + */ + $data = json_decode($html, true); + + $link = str_replace($this->getUrl(), '', $data['link']); + $title = strip_tags($data['title']['rendered']); + $body = strip_tags($data['content']['rendered']); + $timestamp = $this->dateParser->createTimeStamp($data['date'], format: 'c'); + $categories = $this->mapCategories($data['categories']); + + if (! $dateRange instanceof DateRange || $dateRange->inRange((int) $timestamp)) { + $metadata = $this->openGraphConsumer->consumeUrl($data['link']); + + $this->save($title, $link, $categories, $body, $timestamp, $metadata); + } else { + $this->skip($dateRange, $timestamp, $title, $data['date']); + } + } catch (ArticleOutOfRange $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error(sprintf('> %s [Failed] ❌', $e->getMessage())); + return; + } + } + + /** + * edge case for some politico.cd website + * this invalidates the json, so we have to remove it + */ + private function removeMisconfigurationError(string $content): string + { + $error = '
+Notice: ob_end_flush(): Failed to send buffer of zlib output compression (0) in /home/politico/public_html/wp-includes/functions.php on line 5427
'; + return str_replace($error, '', $content); + } + + private function fetchCategories(): void + { + $response = $this->client->request('GET', sprintf('%s/wp-json/wp/v2/categories?%s', $this->getUrl(), self::CATEGORY_QUERY)); + + /** @var array{id: int, slug: string}[] $categories */ + $categories = json_decode($response->getContent(), true); + + foreach ($categories as $category) { + $this->categoryMap[$category['id']] = $category['slug']; + } + } + + private function mapCategories(array $categories): string + { + if ($this->categoryMap === []) { + $this->fetchCategories(); + } + + return strtolower(implode(',', array_map(fn ($category) => $this->categoryMap[$category], $categories))); + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Crawler/UserAgents.php b/projects/backend/src/Aggregator/Infrastructure/Crawler/UserAgents.php new file mode 100644 index 0000000..20e7b7c --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Crawler/UserAgents.php @@ -0,0 +1,31 @@ + + */ +enum UserAgents: string +{ + case OPEN_GRAPH = 'facebookexternalhit/1.1'; + case IPHONE = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_4_8; like Mac OS X) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.3638.271 Mobile Safari/537.5'; + case LINUX = 'Mozilla/5.0 (Linux; U; Linux x86_64; en-US) Gecko/20130401 Firefox/52.7'; + case ANDROID = 'Mozilla/5.0 (Linux; U; Android 5.0; SM-P815 Build/LRX22G) AppleWebKit/600.4 (KHTML, like Gecko) Chrome/48.0.1562.260 Mobile Safari/600.0'; + case CHROME_WINDOWS = 'Mozilla/5.0 (Windows; U; Windows NT 6.3;) AppleWebKit/533.34 (KHTML, like Gecko) Chrome/51.0.1883.215 Safari/533'; + case EXPLORER = 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.3; x64; en-US Trident/4.0)'; + case MAC_FIREFOX = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_10_3) Gecko/20100101 Firefox/63.4'; + case CHROME_LINUX = 'Mozilla/5.0 (Linux; Linux x86_64; en-US) AppleWebKit/603.50 (KHTML, like Gecko) Chrome/55.0.2226.116 Safari/601'; + case MAC_FIREFOX_OLD = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_8_3; en-US) Gecko/20100101 Firefox/68.9'; + case MOBILE_IPHONE = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_9_8; like Mac OS X) AppleWebKit/603.34 (KHTML, like Gecko) Chrome/47.0.1126.107 Mobile Safari/602.7'; + case MOBILE_IPOD = 'Mozilla/5.0 (iPod; CPU iPod OS 8_2_0; like Mac OS X) AppleWebKit/601.40 (KHTML, like Gecko) Chrome/47.0.1590.178 Mobile Safari/535.2'; + + public static function random(): string + { + $userAgents = array_map(fn (self $userAgent) => $userAgent->value, self::cases()); + return $userAgents[array_rand($userAgents)]; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php new file mode 100644 index 0000000..3e591b9 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php @@ -0,0 +1,76 @@ + + */ +final readonly class GetArticlesForExportDbalHandler implements GetArticlesForExportHandler +{ + private const int BATCH_SIZE = 1000; + + public function __construct( + private Connection $connection + ) { + } + + #[\Override] + public function __invoke(GetArticlesForExport $query): iterable + { + $qb = $this->connection->createQueryBuilder() + ->select( + 'a.id as article_id', + 'a.title as article_title', + 'a.link as article_link', + 'a.categories as article_categories', + 'a.body as article_body', + 's.name as article_source', + 'a.hash as article_hash', + 'a.published_at as article_published_at', + 'a.crawled_at as article_crawled_at' + ) + ->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->orderBy('a.published_at', 'DESC'); + + if ($query->source !== null) { + $qb->andWhere('s.name = :source') + ->setParameter('source', $query->source); + } + + if ($query->date instanceof DateRange) { + $qb->andWhere('a.published_at BETWEEN :start AND :end') + ->setParameter('start', $query->date->start) + ->setParameter('end', $query->date->end); + } + + $offset = 0; + + while (true) { + $qb->setFirstResult($offset); + $qb->setMaxResults(self::BATCH_SIZE); + + /** @var array> $data */ + $data = $qb->executeQuery()->fetchAllAssociative(); + if (count($data) === 0) { + break; + } + + foreach ($data as $article) { + yield ArticleForExport::create($article); + } + + $offset += self::BATCH_SIZE; + } + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetEarliestPublicationDateDBalHandler.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetEarliestPublicationDateDBalHandler.php new file mode 100644 index 0000000..8544241 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetEarliestPublicationDateDBalHandler.php @@ -0,0 +1,51 @@ + + */ +final readonly class GetEarliestPublicationDateDBalHandler implements GetEarliestPublicationDateHandler +{ + public function __construct( + private Connection $connection, + private LoggerInterface $logger + ) { + } + + #[\Override] + public function __invoke(GetEarliestPublicationDate $query): \DateTimeImmutable + { + $qb = $this->connection->createQueryBuilder() + ->select('MIN(a.published_at)') + ->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where('s.name = :source') + ->setParameter('source', $query->source); + + if ($query->category !== null) { + $qb->andWhere('a.categories LIKE :category') + ->setParameter('category', sprintf('%%%s%%', $query->category)); + } + + try { + /** @var string|null $date */ + $date = $qb->executeQuery()->fetchOne(); + + return new \DateTimeImmutable($date ?? 'now'); + } catch (\Throwable $e) { + $this->logger->critical('Unable to fetch earliest publication date'); + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetLatestPublicationDateDBalHandler.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetLatestPublicationDateDBalHandler.php new file mode 100644 index 0000000..d18f15c --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetLatestPublicationDateDBalHandler.php @@ -0,0 +1,51 @@ + + */ +final readonly class GetLatestPublicationDateDBalHandler implements GetLatestPublicationDateHandler +{ + public function __construct( + private Connection $connection, + private LoggerInterface $logger + ) { + } + + #[\Override] + public function __invoke(GetLatestPublicationDate $query): \DateTimeImmutable + { + $qb = $this->connection->createQueryBuilder() + ->select('MAX(a.published_at)') + ->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where('s.name = :source') + ->setParameter('source', $query->source); + + if ($query->category !== null) { + $qb->andWhere('a.categories LIKE :category') + ->setParameter('category', sprintf('%%%s%%', $query->category)); + } + + try { + /** @var string|null $date */ + $date = $qb->executeQuery()->fetchOne(); + + return new \DateTimeImmutable($date ?? 'now'); + } catch (\Throwable $e) { + $this->logger->critical('Unable to fetch latest publication date'); + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetSourceStatisticsListDbalHandler.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetSourceStatisticsListDbalHandler.php new file mode 100644 index 0000000..d1b9b12 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetSourceStatisticsListDbalHandler.php @@ -0,0 +1,48 @@ + + */ +final readonly class GetSourceStatisticsListDbalHandler implements GetSourceStatisticsListHandler +{ + public function __construct( + private Connection $connection + ) { + } + + public function __invoke(GetSourceStatisticsList $query): SourceStatisticsList + { + $qb = $this->connection->createQueryBuilder() + ->select( + 's.id as source_id', + 's.name as source_name', + 'MAX(a.crawled_at) as source_crawled_at', + 'COUNT(a.id) as articles_count', + 'SUM(CASE WHEN a.metadata IS NOT NULL THEN 1 ELSE 0 END) as article_metadata_available' + ) + ->from('source', 's') + ->leftJoin('s', 'article', 'a', 'a.source_id = s.id') + ->groupBy('s.id') + ->orderBy('s.name', 'ASC'); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + return SourceStatisticsList::create($data); + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/ArticleIdType.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/ArticleIdType.php new file mode 100644 index 0000000..1ded30b --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/ArticleIdType.php @@ -0,0 +1,28 @@ + + */ +final class ArticleIdType extends AbstractUidType +{ + #[\Override] + public function getName(): string + { + return 'article_id'; + } + + #[\Override] + protected function getUidClass(): string + { + return ArticleId::class; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/OpenGraphType.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/OpenGraphType.php new file mode 100644 index 0000000..eb1f23d --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/OpenGraphType.php @@ -0,0 +1,66 @@ + + */ +final class OpenGraphType extends Type +{ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $platform->getJsonTypeDeclarationSQL([ + 'nullable' => true, + ]); + } + + public function getName(): string + { + return 'open_graph'; + } + + #[\Override] + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?OpenGraph + { + if ($value === null) { + return null; + } + + if (! \is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', OpenGraph::class]); + } + + try { + return OpenGraph::tryFrom($value); + } catch (\Throwable $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + #[\Override] + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if ($value instanceof OpenGraph) { + return json_encode($value) ?: null; + } + + if ($value === null || $value === '') { + return null; + } + + if (! \is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', OpenGraph::class]); + } + + throw ConversionException::conversionFailed($value, $this->getName()); + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/SourceIdType.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/SourceIdType.php new file mode 100644 index 0000000..635f579 --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/Types/SourceIdType.php @@ -0,0 +1,26 @@ + + */ +final class SourceIdType extends AbstractUidType +{ + public function getName(): string + { + return 'source_id'; + } + + protected function getUidClass(): string + { + return SourceId::class; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/ArticleOrmRepository.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/ArticleOrmRepository.php new file mode 100644 index 0000000..526eaaa --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/ArticleOrmRepository.php @@ -0,0 +1,130 @@ + + * + * @author bernard-ng + */ +final class ArticleOrmRepository extends ServiceEntityRepository implements ArticleRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Article::class); + } + + #[\Override] + public function add(Article $article): void + { + $this->getEntityManager()->persist($article); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function remove(Article $article): void + { + $this->getEntityManager()->remove($article); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function getById(ArticleId $id): Article + { + /** @var Article|null $article */ + $article = $this->findOneBy([ + 'id' => $id, + ]); + + if ($article === null) { + throw ArticleNotFound::withId($id); + } + + return $article; + } + + #[\Override] + public function export(?string $source, ?DateRange $date): \Generator + { + $qb = $this->createQueryBuilder('a') + ->orderBy('a.publishedAt', 'DESC'); + + if ($source !== null) { + $qb + ->leftJoin('a.source', 's') + ->andWhere('s.name = :source') + ->setParameter('source', $source); + } + + if ($date instanceof DateRange) { + $qb->andWhere('a.publishedAt BETWEEN FROM_UNIXTIME(:start) AND FROM_UNIXTIME(:end)') + ->setParameter('start', $date->start) + ->setParameter('end', $date->end); + } + + $limit = 1000; + $offset = 0; + + while (true) { + $qb->setFirstResult($offset); + $qb->setMaxResults($limit); + + /** @var Article[] $articles */ + $articles = $qb->getQuery()->getResult(); + if (count($articles) === 0) { + break; + } + + foreach ($articles as $article) { + yield $article; + $this->getEntityManager()->detach($article); + } + + $offset += $limit; + } + } + + #[\Override] + public function getByHash(string $hash): ?Article + { + /** @var Article|null $article */ + $article = $this->findOneBy([ + 'hash' => $hash, + ]); + + return $article; + } + + #[\Override] + public function clear(string $source, ?string $category): int + { + $qb = $this->createQueryBuilder('a') + ->leftJoin('a.source', 's') + ->where('s.name = :source') + ->setParameter('source', $source); + + if ($category !== null) { + $qb->andWhere('a.categories LIKE :category') + ->setParameter('category', sprintf('%%%s%%', $category)); + } + + /** @var int $result */ + $result = $qb->delete(Article::class, 'a') + ->getQuery() + ->execute(); + + return $result; + } +} diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/SourceOrmRepository.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/SourceOrmRepository.php new file mode 100644 index 0000000..e73147b --- /dev/null +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/ORM/SourceOrmRepository.php @@ -0,0 +1,65 @@ + + * + * @author bernard-ng + */ +final class SourceOrmRepository extends ServiceEntityRepository implements SourceRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Source::class); + } + + public function add(Source $source): void + { + $this->getEntityManager()->persist($source); + $this->getEntityManager()->flush(); + } + + public function remove(Source $source): void + { + $this->getEntityManager()->remove($source); + $this->getEntityManager()->flush(); + } + + public function getByName(string $name): Source + { + $source = $this->findOneBy([ + 'name' => $name, + ]); + + if ($source === null) { + throw SourceNotFound::withName($name); + } + + return $source; + } + + public function getById(SourceId $sourceId): Source + { + $source = $this->findOneBy([ + 'id' => $sourceId, + ]); + + if ($source === null) { + throw SourceNotFound::withId($sourceId); + } + + return $source; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/ConsumeOpenGraphConsole.php b/projects/backend/src/Aggregator/Presentation/Console/ConsumeOpenGraphConsole.php new file mode 100644 index 0000000..6dd724a --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/ConsumeOpenGraphConsole.php @@ -0,0 +1,117 @@ +io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function configure(): void + { + $this->addArgument('source', InputArgument::REQUIRED, 'The source to crawl'); + $this->addOption('batch', null, InputOption::VALUE_OPTIONAL, 'Batch size', 50); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setProcessTitle('[DRC News] OpenGraph Consumer'); + if ($input->getOption('no-interaction') === false && ! $this->io->confirm('This is a long process, do you want to continue ?', false)) { + $this->io->warning('Process aborted'); + return Command::SUCCESS; + } + + $index = 0; + $batchSize = $input->getOption('batch') ?? 50; + $source = $input->getArgument('source'); + + try { + $this->entityManager->getConnection()->executeQuery('SET SESSION interactive_timeout = 86400;'); + $this->entityManager->getConnection()->executeQuery('SET SESSION wait_timeout = 86400;'); + } catch (Exception $e) { + $this->logger->critical('Unable to set session timeout', [ + 'exception' => $e, + ]); + return Command::FAILURE; + } + + $query = $this->entityManager + ->createQuery(<<<'DQL' + SELECT a + FROM App\Aggregator\Domain\Model\Entity\Article a + LEFT JOIN App\Aggregator\Domain\Model\Entity\Source s + WHERE s.name = :source AND a.metadata IS NULL + ORDER BY a.publishedAt DESC + DQL) + ->setParameter('source', $source); + + $this->stopwatch->start(self::WATCH_EVENT_NAME); + + /** @var Article $article */ + foreach ($query->toIterable() as $article) { + $object = $this->openGraphConsumer->consumeUrl((string) $article->link); + + if ($object instanceof OpenGraphObject) { + $article->defineOpenGraph($object); + $this->logger->notice(sprintf('> %s ✅', $article->title)); + } else { + $this->logger->notice(sprintf('> %s ❌', $article->title)); + } + + ++$index; + if ($index % $batchSize === 0) { + $this->entityManager->flush(); + $this->entityManager->clear(); + } + } + + $this->entityManager->flush(); + + $event = $this->stopwatch->stop(self::WATCH_EVENT_NAME); + $this->eventDispatcher->dispatch([new SourceCrawled((string) $event, 'open-graph')]); + $this->logger->notice('OpenGraph data fetched successfully'); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/CrawlConsole.php b/projects/backend/src/Aggregator/Presentation/Console/CrawlConsole.php new file mode 100644 index 0000000..9c50091 --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/CrawlConsole.php @@ -0,0 +1,121 @@ +addArgument('source', InputArgument::REQUIRED, 'the website source to crawle'); + $this->addOption('date', null, InputOption::VALUE_OPTIONAL, 'Date interval to crawle'); + $this->addOption('page', null, InputOption::VALUE_OPTIONAL, 'PageRange interval to crawle'); + $this->addOption('category', null, InputOption::VALUE_OPTIONAL, 'the category to crawle'); + $this->addOption('parallel', null, InputOption::VALUE_OPTIONAL, 'the number of parallel requests', default: 1); + $this->addOption('notify', null, InputOption::VALUE_NONE, 'enable notifications'); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $source */ + $source = $input->getArgument('source'); + + /** @var string|null $page */ + $page = $input->getOption('page'); + + /** @var string|null $date */ + $date = $input->getOption('date'); + + /** @var string|null $category */ + $category = $input->getOption('category'); + + /** @var string $parallel */ + $parallel = $input->getOption('parallel'); + $parallel = intval($parallel); + + if ($parallel > 1) { + return $this->parallel($parallel, $source, $category); + } + + $this->sourceCrawler->fetch( + settings: new CrawlingSettings( + id: $source, + pageRange: $page !== null ? PageRange::from($page) : null, + dateRange: $date !== null ? DateRange::from($date) : null, + category: $category, + notify: $input->getOption('notify') !== null + ) + ); + + $this->io->success('website crawled successfully'); + return Command::SUCCESS; + } + + private function parallel(int $workers, string $source, ?string $category): int + { + $fetcher = $this->sourceCrawler->get($source); + $range = $fetcher->getPagination($category); + $workPerWorker = ceil(($range->end - $range->start + 1) / $workers); + + $this->io->title(sprintf('Crawling %d pages with %d workers, %d pages per worker', $range->end - $range->start + 1, $workers, $workPerWorker)); + + $processes = []; + for ($i = 0; $i < $workers; $i++) { + $start = $range->start + ($i * $workPerWorker); + $end = min($range->start + (($i + 1) * $workPerWorker) - 1, $range->end); + + $process = new PhpSubprocess(['bin/console', 'app:crawl', $source, sprintf('--page=%d:%d', $start, $end), '-v']); + $process->start(); + $processes[] = $process; + + if ($start > $range->end) { + break; + } + } + + foreach ($processes as $process) { + while ($process->isRunning()) { + // waiting for process to finish + } + + $this->io->writeln($process->getOutput()); + } + + $this->io->success('Website crawled successfully'); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/CreateSourceConsole.php b/projects/backend/src/Aggregator/Presentation/Console/CreateSourceConsole.php new file mode 100644 index 0000000..bb1241e --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/CreateSourceConsole.php @@ -0,0 +1,95 @@ +addArgument('source', InputArgument::REQUIRED, 'the website source to crawle'); + $this->addArgument('displayName', InputArgument::OPTIONAL, 'the display name of the source'); + $this->addArgument('description', InputArgument::OPTIONAL, 'the description of the source'); + $this->addOption('bias', 'b', InputArgument::OPTIONAL, 'bias of the source', Bias::NEUTRAL->value); + $this->addOption('reliability', 'r', InputArgument::OPTIONAL, 'reliability of the source', Reliability::AVERAGE->value); + $this->addOption('transparency', 't', InputArgument::OPTIONAL, 'transparency of the source', Transparency::MEDIUM->value); + } + + #[\Override] + protected function interact(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + $this->io->title('Create a new data source'); + + $this->askArgument($input, 'source'); + $this->askArgument($input, 'displayName'); + $this->askOption($input, 'bias'); + $this->askOption($input, 'reliability'); + $this->askOption($input, 'transparency'); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (! $this->io->confirm('Do you want to continue?', false)) { + $this->io->warning('Process aborted'); + return Command::FAILURE; + } + + /** @var string $source */ + $source = $input->getArgument('source'); + + /** @var string|null $displayName */ + $displayName = $input->getArgument('displayName'); + + /** @var string|null $description */ + $description = $input->getArgument('description'); + + $credibility = new Credibility( + bias: Bias::from($input->getOption('bias')), + reliability: Reliability::from($input->getOption('reliability')), + transparency: Transparency::from($input->getOption('transparency')), + ); + + $this->commandBus->handle(new CreateSource($source, $credibility, $displayName, $description)); + + $this->io->success('Source add successfully'); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/DeleteArticlesConsole.php b/projects/backend/src/Aggregator/Presentation/Console/DeleteArticlesConsole.php new file mode 100644 index 0000000..5df94b4 --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/DeleteArticlesConsole.php @@ -0,0 +1,71 @@ +addArgument('source', InputArgument::REQUIRED, 'the website source to crawle'); + $this->addOption('category', null, InputOption::VALUE_OPTIONAL, 'the category to crawle'); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $source */ + $source = $input->getArgument('source'); + + /** @var string|null $category */ + $category = $input->getOption('category'); + + if ( + $this->io->confirm('Delete all articles ?', false) && + $this->io->confirm('Are you sure ?', false) + ) { + + $confirmation = $this->io->askQuestion(new Question('Specify the source to confirm : ')); + if ($confirmation === $source) { + /** @var int $count */ + $count = $this->commandBus->handle(new DeleteArticles($source, $category)); + $this->io->success(sprintf('%d articles from %s removed', $count, $source)); + } else { + $this->io->warning('Source does not match, aborting !'); + } + } + + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/ExportArticlesConsole.php b/projects/backend/src/Aggregator/Presentation/Console/ExportArticlesConsole.php new file mode 100644 index 0000000..f38d1c4 --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/ExportArticlesConsole.php @@ -0,0 +1,65 @@ +addArgument('source', InputArgument::OPTIONAL, 'the website source to crawle'); + $this->addOption('date', null, InputOption::VALUE_OPTIONAL, 'Date interval to crawle', null); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string|null $source */ + $source = $input->getArgument('source'); + + /** @var string|null $date */ + $date = $input->getOption('date'); + + $confirmation = $this->io->confirm('This can take a while, would like to continue ?', false); + if ($confirmation) { + $this->commandBus->handle(new ExportArticles( + source: $source, + date: $date !== null ? DateRange::from($date) : null + )); + } + + $this->io->success('articles exported successfully'); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/GetSourceStatisticsListConsole.php b/projects/backend/src/Aggregator/Presentation/Console/GetSourceStatisticsListConsole.php new file mode 100644 index 0000000..e898b16 --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/GetSourceStatisticsListConsole.php @@ -0,0 +1,63 @@ +io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var SourceStatisticsList $stats */ + $stats = $this->queryBus->handle(new GetSourceStatisticsList()); + + $stopWatch = new Stopwatch(true); + $stopWatch->start('app:stats'); + + $this->io->table( + ['Source', 'Articles', 'Metadata', 'CrawledAt'], + array_map( + fn (SourceStatistics $source): array => [ + $source->name, + number_format($source->articlesCount, decimal_separator: '.', thousands_separator: ','), + number_format($source->metadataAvailable, decimal_separator: '.', thousands_separator: ','), + $source->crawledAt?->format('Y-m-d H:i:s') ?? 'Never', + ], + $stats->items + ) + ); + + $this->io->text((string) $stopWatch->stop('app:stats')); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/UpdateConsole.php b/projects/backend/src/Aggregator/Presentation/Console/UpdateConsole.php new file mode 100644 index 0000000..f7b23dc --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/UpdateConsole.php @@ -0,0 +1,90 @@ +addArgument('source', InputArgument::REQUIRED, 'the website source to crawle'); + $this->addOption('category', null, InputOption::VALUE_OPTIONAL, 'the category to crawle'); + $this->addOption('direction', null, InputOption::VALUE_OPTIONAL, 'the direction to crawle', 'forward', ['forward', 'backward']); + $this->addOption('days', null, InputOption::VALUE_OPTIONAL, 'the number of days to crawle'); + $this->addOption('notify', null, InputOption::VALUE_OPTIONAL, 'enable notifications', default: false); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var int|null $days */ + $days = $input->getOption('days'); + + /** @var string $source */ + $source = $input->getArgument('source'); + + /** @var string|null $category */ + $category = $input->getOption('category'); + + /** @var string $direction */ + $direction = $input->getOption('direction'); + $direction = UpdateDirection::from($direction); + + /** @var \DateTimeImmutable $date */ + $date = $this->queryBus->handle(match ($direction) { + UpdateDirection::FORWARD => new GetLatestPublicationDate($source, $category), + UpdateDirection::BACKWARD => new GetEarliestPublicationDate($source, $category), + }); + + $dateRange = $direction === UpdateDirection::FORWARD ? + DateRange::forward($date) : + DateRange::backward($date, $days); + + $this->io->title(sprintf('[%s] Updating with range %s', $direction->value, $dateRange->format())); + $this->sourceCrawler->fetch(new CrawlingSettings( + $source, + dateRange: $dateRange, + category: $category, + notify: $input->getOption('notify') !== null + )); + $this->io->success('website crawled successfully'); + + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/Aggregator/Presentation/Console/UpdateProxiesConsole.php b/projects/backend/src/Aggregator/Presentation/Console/UpdateProxiesConsole.php new file mode 100644 index 0000000..879090c --- /dev/null +++ b/projects/backend/src/Aggregator/Presentation/Console/UpdateProxiesConsole.php @@ -0,0 +1,66 @@ +io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $response = $this->client->request('GET', self::UPDATE_URL); + + $content = $response->getContent(); + $content = preg_replace('/^([0-9\.]+:[0-9]+):.*$/m', '$1', $content); + Assert::string($content); + + $this->filesystem->dumpFile( + filename: $this->projectDir . '/data/proxies.txt', + content: $content + ); + } catch (\Throwable $e) { + $this->logger->critical('Failed to update proxies', [ + 'exception' => $e, + ]); + return Command::FAILURE; + } + + $this->io->success('Proxies updated successfully.'); + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/FeedManagement/Application/Cache/SourceCacheAttributes.php b/projects/backend/src/FeedManagement/Application/Cache/SourceCacheAttributes.php new file mode 100644 index 0000000..05a23bd --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/Cache/SourceCacheAttributes.php @@ -0,0 +1,24 @@ + + */ +enum SourceCacheAttributes: string +{ + case CATEGORIES = 'categories_shares'; + + case PUBLICATIONS = 'publications_graph'; + + public const int CACHE_TTL = 86400; + + public function withId(string $id): string + { + return sprintf('%s_%s', $this->value, $id); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php new file mode 100644 index 0000000..2413051 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php @@ -0,0 +1,68 @@ + + */ +final readonly class ArticleDetails +{ + public function __construct( + public ArticleId $id, + public string $title, + public Link $link, + public array $categories, + public string $body, + public SourceReference $source, + public string $hash, + public Credibility $credibility, + public Sentiment $sentiment, + public ?OpenGraph $metadata, + public ReadingTime $readingTime, + public \DateTimeImmutable $publishedAt, + public \DateTimeImmutable $crawledAt, + public ?\DateTimeImmutable $updatedAt, + public bool $bookmarked = false + ) { + } + + public static function create(array $item): self + { + return new self( + ArticleId::fromBinary($item['article_id']), + DataMapping::string($item, 'article_title'), + Link::from(DataMapping::string($item, 'article_link')), + explode(',', DataMapping::string($item, 'article_categories')), + DataMapping::string($item, 'article_body'), + SourceReference::create($item), + DataMapping::string($item, 'article_hash'), + new Credibility( + DataMapping::enum($item, 'article_bias', Bias::class), + DataMapping::enum($item, 'article_reliability', Reliability::class), + DataMapping::enum($item, 'article_transparency', Transparency::class) + ), + DataMapping::enum($item, 'article_sentiment', Sentiment::class), + OpenGraph::tryFrom(DataMapping::nullableString($item, 'article_metadata')), + ReadingTime::create(DataMapping::nullableInteger($item, 'article_reading_time')), + DataMapping::datetime($item, 'article_published_at'), + DataMapping::datetime($item, 'article_crawled_at'), + DataMapping::nullableDatetime($item, 'article_updated_at'), + DataMapping::boolean($item, 'article_is_bookmarked'), + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php new file mode 100644 index 0000000..fd6508d --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php @@ -0,0 +1,48 @@ + + */ +final readonly class ArticleOverview +{ + public function __construct( + public ArticleId $id, + public string $title, + public Link $link, + public array $categories, + public string $excerpt, + public SourceReference $source, + public ?string $image, + public ReadingTime $readingTime, + public \DateTimeImmutable $publishedAt, + public bool $bookmarked = false + ) { + } + + public static function create(array $item): self + { + return new self( + ArticleId::fromBinary($item['article_id']), + DataMapping::string($item, 'article_title'), + Link::from(DataMapping::string($item, 'article_link')), + explode(',', DataMapping::string($item, 'article_categories')), + trim(DataMapping::string($item, 'article_excerpt')), + SourceReference::create($item), + DataMapping::nullableString($item, 'article_image'), + ReadingTime::create(DataMapping::nullableInteger($item, 'article_reading_time')), + DataMapping::datetime($item, 'article_published_at'), + DataMapping::boolean($item, 'article_is_bookmarked'), + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverviewList.php b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverviewList.php new file mode 100644 index 0000000..04426bd --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverviewList.php @@ -0,0 +1,31 @@ + + */ +final readonly class ArticleOverviewList +{ + public function __construct( + public array $items, + public PaginationInfo $pagination + ) { + Assert::allIsInstanceOf($this->items, ArticleOverview::class); + } + + public static function create(array $items, PaginationInfo $pagination): self + { + return new self( + array_map(fn (array $item): ArticleOverview => ArticleOverview::create($item), $items), + $pagination + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php b/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php new file mode 100644 index 0000000..91d9c4b --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php @@ -0,0 +1,40 @@ + + */ +final readonly class Bookmark +{ + public function __construct( + public BookmarkId $id, + public string $name, + public \DateTimeImmutable $createdAt, + public ?string $description = null, + public int $articlesCount = 0, + public bool $isPublic = false, + public ?\DateTimeImmutable $updatedAt = null + ) { + } + + public static function create(array $item): self + { + return new self( + BookmarkId::fromBinary($item['bookmark_id']), + DataMapping::string($item, 'bookmark_name'), + DataMapping::datetime($item, 'bookmark_created_at'), + DataMapping::nullableString($item, 'bookmark_description'), + DataMapping::integer($item, 'bookmark_articles_count'), + DataMapping::boolean($item, 'bookmark_is_public'), + DataMapping::nullableDatetime($item, 'bookmark_updated_at') + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/BookmarkList.php b/projects/backend/src/FeedManagement/Application/ReadModel/BookmarkList.php new file mode 100644 index 0000000..4b40881 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/BookmarkList.php @@ -0,0 +1,31 @@ + + */ +final readonly class BookmarkList +{ + public function __construct( + public array $items, + public PaginationInfo $pagination + ) { + Assert::allIsInstanceOf($this->items, Bookmark::class); + } + + public static function create(array $items, PaginationInfo $pagination): self + { + return new self( + array_map(fn (array $item): Bookmark => Bookmark::create($item), $items), + $pagination + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShare.php b/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShare.php new file mode 100644 index 0000000..b19310a --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShare.php @@ -0,0 +1,20 @@ + + */ +final readonly class CategoryShare +{ + public function __construct( + public string $category, + public int $count, + public float $percentage + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShares.php b/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShares.php new file mode 100644 index 0000000..9fd335d --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/CategoryShares.php @@ -0,0 +1,22 @@ + + */ +final readonly class CategoryShares +{ + public function __construct( + public array $items = [], + public int $total = 0 + ) { + Assert::allIsInstanceOf($this->items, CategoryShare::class); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php b/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php new file mode 100644 index 0000000..2776dc9 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php @@ -0,0 +1,37 @@ + + */ +final readonly class Comment +{ + public function __construct( + public CommentId $id, + public UserReference $user, + public Sentiment $sentiment, + public string $content, + public \DateTimeImmutable $createdAt, + ) { + } + + public static function create(array $item): self + { + return new self( + CommentId::fromBinary($item['comment_id']), + UserReference::create($item), + DataMapping::enum($item, 'comment_sentiment', Sentiment::class), + DataMapping::string($item, 'comment_content'), + DataMapping::dateTime($item, 'comment_created_at') + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/CommentList.php b/projects/backend/src/FeedManagement/Application/ReadModel/CommentList.php new file mode 100644 index 0000000..ab08170 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/CommentList.php @@ -0,0 +1,31 @@ + + */ +final readonly class CommentList +{ + public function __construct( + public array $items, + public PaginationInfo $pagination + ) { + Assert::allIsInstanceOf($this->items, Comment::class); + } + + public static function create(array $items, PaginationInfo $pagination): self + { + return new self( + array_map(fn (array $item): Comment => Comment::create($item), $items), + $pagination + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/PublicationEntry.php b/projects/backend/src/FeedManagement/Application/ReadModel/PublicationEntry.php new file mode 100644 index 0000000..1fc82a7 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/PublicationEntry.php @@ -0,0 +1,19 @@ + + */ +final readonly class PublicationEntry +{ + public function __construct( + public string $date, + public int $count + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/PublicationGraph.php b/projects/backend/src/FeedManagement/Application/ReadModel/PublicationGraph.php new file mode 100644 index 0000000..d3da270 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/PublicationGraph.php @@ -0,0 +1,22 @@ + + */ +final readonly class PublicationGraph +{ + public function __construct( + public array $items = [], + public int $total = 0 + ) { + Assert::allIsInstanceOf($this->items, PublicationEntry::class); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php new file mode 100644 index 0000000..03d8377 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php @@ -0,0 +1,62 @@ + + */ +final readonly class SourceDetails +{ + public function __construct( + public SourceId $id, + public string $name, + public string $url, + public Credibility $credibility, + public PublicationGraph $publicationGraph, + public CategoryShares $categoryShares, + public int $articlesCount, + public string $crawledAt, + public ?string $displayName = null, + public ?string $description = null, + public ?string $updatedAt = null, + public int $metadataAvailable = 0, + public bool $followed = false, + public ?string $image = null, + ) { + } + + public static function create(array $item, PublicationGraph $publicationGraph, CategoryShares $categoryShares): self + { + return new self( + SourceId::fromBinary($item['source_id']), + DataMapping::string($item, 'source_name'), + DataMapping::string($item, 'source_url'), + new Credibility( + DataMapping::enum($item, 'source_bias', Bias::class), + DataMapping::enum($item, 'source_reliability', Reliability::class), + DataMapping::enum($item, 'source_transparency', Transparency::class) + ), + $publicationGraph, + $categoryShares, + DataMapping::integer($item, 'articles_count'), + DataMapping::string($item, 'source_crawled_at'), + DataMapping::nullableString($item, 'source_display_name'), + DataMapping::nullableString($item, 'source_description'), + DataMapping::nullableString($item, 'source_updated_at'), + DataMapping::integer($item, 'articles_metadata_available'), + DataMapping::boolean($item, 'source_is_followed'), + DataMapping::nullableString($item, 'source_image'), + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php new file mode 100644 index 0000000..34b60ec --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php @@ -0,0 +1,38 @@ + + */ +final readonly class SourceOverview +{ + public function __construct( + public SourceId $id, + public string $name, + public string $url, + public ?string $displayName = null, + public bool $followed = false, + public ?string $image = null + ) { + } + + public static function create(array $item): self + { + return new self( + SourceId::fromBinary($item['source_id']), + DataMapping::string($item, 'source_name'), + DataMapping::string($item, 'source_url'), + DataMapping::nullableString($item, 'source_display_name'), + DataMapping::boolean($item, 'source_is_followed'), + DataMapping::nullableString($item, 'source_image') + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverviewList.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverviewList.php new file mode 100644 index 0000000..8e4ed83 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverviewList.php @@ -0,0 +1,31 @@ + + */ +final readonly class SourceOverviewList +{ + public function __construct( + public array $items, + public PaginationInfo $pagination, + ) { + Assert::allIsInstanceOf($items, SourceOverview::class); + } + + public static function create(array $items, PaginationInfo $pagination): self + { + return new self( + array_map(fn (array $item): SourceOverview => SourceOverview::create($item), $items), + $pagination + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php new file mode 100644 index 0000000..5d68309 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php @@ -0,0 +1,36 @@ + + */ +final readonly class SourceReference +{ + public function __construct( + public SourceId $id, + public string $name, + public ?string $displayName, + public ?string $image, + public string $url + ) { + } + + public static function create(array $item): self + { + return new self( + SourceId::fromBinary($item['source_id']), + DataMapping::string($item, 'source_name'), + DataMapping::nullableString($item, 'source_display_name'), + DataMapping::nullableString($item, 'source_image'), + DataMapping::string($item, 'source_url'), + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php b/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php new file mode 100644 index 0000000..57b9b52 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php @@ -0,0 +1,30 @@ + + */ +final readonly class UserReference +{ + public function __construct( + public UserId $id, + public string $name, + ) { + } + + public static function create(array $item): self + { + return new self( + UserId::fromBinary($item['user_id']), + DataMapping::string($item, 'user_name') + ); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/AddArticleToBookmark.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/AddArticleToBookmark.php new file mode 100644 index 0000000..6a134ed --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/AddArticleToBookmark.php @@ -0,0 +1,24 @@ + + */ +final readonly class AddArticleToBookmark +{ + public function __construct( + public UserId $userId, + public ArticleId $articleId, + public BookmarkId $bookmarkId, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/AddCommentToArticle.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/AddCommentToArticle.php new file mode 100644 index 0000000..9c7774b --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/AddCommentToArticle.php @@ -0,0 +1,23 @@ + + */ +final readonly class AddCommentToArticle +{ + public function __construct( + public UserId $userId, + public ArticleId $articleId, + public string $content, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/CreateBookmark.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/CreateBookmark.php new file mode 100644 index 0000000..66508c1 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/CreateBookmark.php @@ -0,0 +1,22 @@ + + */ +final readonly class CreateBookmark +{ + public function __construct( + public UserId $userId, + public string $name, + public ?string $description, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/DeleteBookmark.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/DeleteBookmark.php new file mode 100644 index 0000000..75a8ce5 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/DeleteBookmark.php @@ -0,0 +1,22 @@ + + */ +final readonly class DeleteBookmark +{ + public function __construct( + public UserId $userId, + public BookmarkId $bookmarkId + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/FollowSource.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/FollowSource.php new file mode 100644 index 0000000..6035307 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/FollowSource.php @@ -0,0 +1,22 @@ + + */ +final readonly class FollowSource +{ + public function __construct( + public SourceId $sourceId, + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveArticleFromBookmark.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveArticleFromBookmark.php new file mode 100644 index 0000000..b009923 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveArticleFromBookmark.php @@ -0,0 +1,24 @@ + + */ +final readonly class RemoveArticleFromBookmark +{ + public function __construct( + public UserId $userId, + public ArticleId $articleId, + public BookmarkId $bookmarkId, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveCommentFromArticle.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveCommentFromArticle.php new file mode 100644 index 0000000..98dc0f9 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/RemoveCommentFromArticle.php @@ -0,0 +1,22 @@ + + */ +final readonly class RemoveCommentFromArticle +{ + public function __construct( + public UserId $userId, + public CommentId $commentId + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/UnfollowSource.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/UnfollowSource.php new file mode 100644 index 0000000..3a23373 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/UnfollowSource.php @@ -0,0 +1,22 @@ + + */ +final readonly class UnfollowSource +{ + public function __construct( + public SourceId $sourceId, + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Command/UpdateBookmark.php b/projects/backend/src/FeedManagement/Application/UseCase/Command/UpdateBookmark.php new file mode 100644 index 0000000..1415b1b --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Command/UpdateBookmark.php @@ -0,0 +1,25 @@ + + */ +final readonly class UpdateBookmark +{ + public function __construct( + public UserId $userId, + public BookmarkId $bookmarkId, + public string $name, + public ?string $description, + public bool $isPublic = false, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddArticleToBookmarkHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddArticleToBookmarkHandler.php new file mode 100644 index 0000000..3aa2333 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddArticleToBookmarkHandler.php @@ -0,0 +1,38 @@ + + */ +final readonly class AddArticleToBookmarkHandler implements CommandHandler +{ + public function __construct( + private BookmarkRepository $bookmarkRepository, + private ArticleRepository $articleRepository + ) { + } + + public function __invoke(AddArticleToBookmark $command): void + { + $bookmark = $this->bookmarkRepository->getById($command->bookmarkId); + if ($bookmark->user->id !== $command->userId) { + throw PermissionNotGranted::withReason('feed_management.exceptions.cannot_add_article_to_bookmark'); + } + + $article = $this->articleRepository->getById($command->articleId); + + $bookmark->addArticle($article); + $this->bookmarkRepository->add($bookmark); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddCommentToArticleHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddCommentToArticleHandler.php new file mode 100644 index 0000000..c13a31b --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/AddCommentToArticleHandler.php @@ -0,0 +1,36 @@ + + */ +final readonly class AddCommentToArticleHandler implements CommandHandler +{ + public function __construct( + public UserRepository $userRepository, + public ArticleRepository $articleRepository, + public CommentRepository $commentRepository + ) { + } + + public function __invoke(AddCommentToArticle $comment): void + { + $user = $this->userRepository->getById($comment->userId); + $article = $this->articleRepository->getById($comment->articleId); + + $comment = Comment::create($user, $article, $comment->content); + $this->commentRepository->add($comment); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/CreateBookHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/CreateBookHandler.php new file mode 100644 index 0000000..0740dcd --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/CreateBookHandler.php @@ -0,0 +1,33 @@ + + */ +final readonly class CreateBookHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private BookmarkRepository $bookmarkRepository + ) { + } + + public function __invoke(CreateBookmark $command): void + { + $user = $this->userRepository->getById($command->userId); + $bookmark = Bookmark::create($user, $command->name, $command->description); + + $this->bookmarkRepository->add($bookmark); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/DeleteBookmarkHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/DeleteBookmarkHandler.php new file mode 100644 index 0000000..c2aa8a7 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/DeleteBookmarkHandler.php @@ -0,0 +1,33 @@ + + */ +final readonly class DeleteBookmarkHandler implements CommandHandler +{ + public function __construct( + private BookmarkRepository $bookmarkRepository, + ) { + } + + public function __invoke(DeleteBookmark $command): void + { + $bookmark = $this->bookmarkRepository->getById($command->bookmarkId); + if ($bookmark->user->id !== $command->userId) { + throw PermissionNotGranted::withReason('feed_management.exceptions.cannot_delete_bookmark'); + } + + $this->bookmarkRepository->remove($bookmark); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/FollowSourceHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/FollowSourceHandler.php new file mode 100644 index 0000000..2e5aa7c --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/FollowSourceHandler.php @@ -0,0 +1,46 @@ + + */ +final readonly class FollowSourceHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private SourceRepository $sourceRepository, + private FollowedSourceRepository $followedSourceRepository + ) { + } + + public function __invoke(FollowSource $command): void + { + $followedSource = $this->followedSourceRepository->getByUserId( + $command->userId, + $command->sourceId + ); + + if ($followedSource instanceof FollowedSource) { + throw SourceAlreadyFollowed::with($command->userId, $command->sourceId); + } + + $user = $this->userRepository->getById($command->userId); + $source = $this->sourceRepository->getById($command->sourceId); + + $followedSource = new FollowedSource($source, $user); + $this->followedSourceRepository->add($followedSource); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveArticleFromBookmarkHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveArticleFromBookmarkHandler.php new file mode 100644 index 0000000..b695336 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveArticleFromBookmarkHandler.php @@ -0,0 +1,38 @@ + + */ +final readonly class RemoveArticleFromBookmarkHandler implements CommandHandler +{ + public function __construct( + private BookmarkRepository $bookmarkRepository, + private ArticleRepository $articleRepository + ) { + } + + public function __invoke(RemoveArticleFromBookmark $command): void + { + $bookmark = $this->bookmarkRepository->getById($command->bookmarkId); + if ($bookmark->user->id !== $command->userId) { + throw PermissionNotGranted::withReason('feed_management.exceptions.cannot_remove_article_from_bookmark'); + } + + $article = $this->articleRepository->getById($command->articleId); + $bookmark->removeArticle($article); + + $this->bookmarkRepository->add($bookmark); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveCommentFromArticleHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveCommentFromArticleHandler.php new file mode 100644 index 0000000..88090ab --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/RemoveCommentFromArticleHandler.php @@ -0,0 +1,33 @@ + + */ +final readonly class RemoveCommentFromArticleHandler implements CommandHandler +{ + public function __construct( + private CommentRepository $commentRepository, + ) { + } + + public function __invoke(RemoveCommentFromArticle $command): void + { + $comment = $this->commentRepository->getById($command->commentId); + if ($command->userId !== $comment->user->id) { + throw PermissionNotGranted::withReason('feed_management.exceptions.cannot_delete_comment'); + } + + $this->commentRepository->remove($comment); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UnfollowSourceHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UnfollowSourceHandler.php new file mode 100644 index 0000000..e69dfba --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UnfollowSourceHandler.php @@ -0,0 +1,38 @@ + + */ +final readonly class UnfollowSourceHandler implements CommandHandler +{ + public function __construct( + private FollowedSourceRepository $followedSourceRepository + ) { + } + + public function __invoke(UnfollowSource $command): void + { + $followedSource = $this->followedSourceRepository->getByUserId( + $command->userId, + $command->sourceId + ); + + if (! $followedSource instanceof FollowedSource) { + throw FollowedSourceNotFound::with($command->userId, $command->sourceId); + } + + $this->followedSourceRepository->remove($followedSource); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UpdateBookmarkHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UpdateBookmarkHandler.php new file mode 100644 index 0000000..6bed58e --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/CommandHandler/UpdateBookmarkHandler.php @@ -0,0 +1,39 @@ + + */ +final readonly class UpdateBookmarkHandler implements CommandHandler +{ + public function __construct( + private BookmarkRepository $bookmarkRepository + ) { + } + + public function __invoke(UpdateBookmark $command): void + { + $bookmark = $this->bookmarkRepository->getById($command->bookmarkId); + if ($bookmark->user->id !== $command->userId) { + throw PermissionNotGranted::withReason('feed_management.exceptions.cannot_update_bookmark'); + } + + $bookmark = match ($command->isPublic) { + true => $bookmark->markAsPublic(), + false => $bookmark->markAsPrivate(), + }; + $bookmark->updateInfos($command->name, $command->description); + + $this->bookmarkRepository->add($bookmark); + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleCommentList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleCommentList.php new file mode 100644 index 0000000..8451d55 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleCommentList.php @@ -0,0 +1,22 @@ + + */ +final readonly class GetArticleCommentList +{ + public function __construct( + public ArticleId $articleId, + public Page $page = new Page(), + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleDetails.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleDetails.php new file mode 100644 index 0000000..5814bb3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleDetails.php @@ -0,0 +1,22 @@ + + */ +final readonly class GetArticleDetails +{ + public function __construct( + public ArticleId $id, + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleOverviewList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleOverviewList.php new file mode 100644 index 0000000..07406fc --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetArticleOverviewList.php @@ -0,0 +1,24 @@ + + */ +final readonly class GetArticleOverviewList +{ + public function __construct( + public UserId $userId, + public Page $page = new Page(), + public ArticleFilters $filters = new ArticleFilters(), + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkList.php new file mode 100644 index 0000000..31916d0 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkList.php @@ -0,0 +1,22 @@ + + */ +final readonly class GetBookmarkList +{ + public function __construct( + public UserId $userId, + public Page $page = new Page(), + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkedArticleList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkedArticleList.php new file mode 100644 index 0000000..9044044 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetBookmarkedArticleList.php @@ -0,0 +1,26 @@ + + */ +final readonly class GetBookmarkedArticleList +{ + public function __construct( + public UserId $userId, + public BookmarkId $bookmarkId, + public Page $page = new Page(), + public ArticleFilters $filters = new ArticleFilters() + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceArticleOverviewList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceArticleOverviewList.php new file mode 100644 index 0000000..dfbf4a2 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceArticleOverviewList.php @@ -0,0 +1,26 @@ + + */ +final readonly class GetSourceArticleOverviewList +{ + public function __construct( + public SourceId $sourceId, + public UserId $userId, + public Page $page = new Page(), + public ArticleFilters $filters = new ArticleFilters(), + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceDetails.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceDetails.php new file mode 100644 index 0000000..04a86b8 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceDetails.php @@ -0,0 +1,22 @@ + + */ +final readonly class GetSourceDetails +{ + public function __construct( + public SourceId $sourceId, + public UserId $userId, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceOverviewList.php b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceOverviewList.php new file mode 100644 index 0000000..afc0722 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/Query/GetSourceOverviewList.php @@ -0,0 +1,22 @@ + + */ +final readonly class GetSourceOverviewList +{ + public function __construct( + public UserId $userId, + public Page $page = new Page(), + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleCommentListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleCommentListHandler.php new file mode 100644 index 0000000..6e85d27 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleCommentListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetArticleCommentListHandler extends QueryHandler +{ + public function __invoke(GetArticleCommentList $query): CommentList; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleDetailsHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleDetailsHandler.php new file mode 100644 index 0000000..f3173bb --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleDetailsHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetArticleDetailsHandler extends QueryHandler +{ + public function __invoke(GetArticleDetails $query): ArticleDetails; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleOverviewListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleOverviewListHandler.php new file mode 100644 index 0000000..663eefb --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetArticleOverviewListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetArticleOverviewListHandler extends QueryHandler +{ + public function __invoke(GetArticleOverviewList $query): ArticleOverviewList; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkListHandler.php new file mode 100644 index 0000000..b99b593 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetBookmarkListHandler extends QueryHandler +{ + public function __invoke(GetBookmarkList $query): BookmarkList; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkedArticleListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkedArticleListHandler.php new file mode 100644 index 0000000..5ec96fc --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetBookmarkedArticleListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetBookmarkedArticleListHandler extends QueryHandler +{ + public function __invoke(GetBookmarkedArticleList $query): ArticleOverviewList; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceArticleOverviewListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceArticleOverviewListHandler.php new file mode 100644 index 0000000..0307044 --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceArticleOverviewListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetSourceArticleOverviewListHandler extends QueryHandler +{ + public function __invoke(GetSourceArticleOverviewList $query): ArticleOverviewList; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceDetailsHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceDetailsHandler.php new file mode 100644 index 0000000..a843d9a --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceDetailsHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetSourceDetailsHandler extends QueryHandler +{ + public function __invoke(GetSourceDetails $query): SourceDetails; +} diff --git a/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceOverviewListHandler.php b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceOverviewListHandler.php new file mode 100644 index 0000000..766714a --- /dev/null +++ b/projects/backend/src/FeedManagement/Application/UseCase/QueryHandler/GetSourceOverviewListHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetSourceOverviewListHandler extends QueryHandler +{ + public function __invoke(GetSourceOverviewList $query): SourceOverviewList; +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/ArticleAlreadyBookmarked.php b/projects/backend/src/FeedManagement/Domain/Exception/ArticleAlreadyBookmarked.php new file mode 100644 index 0000000..a7bce7a --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/ArticleAlreadyBookmarked.php @@ -0,0 +1,37 @@ + + */ +final class ArticleAlreadyBookmarked extends \DomainException implements UserFacingError +{ + public static function with(ArticleId $articleId, BookmarkId $bookmarkId): self + { + return new self(sprintf('Article %s already bookmarked in bookmark %s', $articleId->toString(), $bookmarkId->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.article_already_bookmarked'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/BookmarkNotFound.php b/projects/backend/src/FeedManagement/Domain/Exception/BookmarkNotFound.php new file mode 100644 index 0000000..6b64945 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/BookmarkNotFound.php @@ -0,0 +1,36 @@ + + */ +final class BookmarkNotFound extends \DomainException implements UserFacingError +{ + public static function withId(BookmarkId $id): self + { + return new self(sprintf('Bookmark with id "%s" not found.', $id->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.bookmark_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/BookmarkedArticleNotFound.php b/projects/backend/src/FeedManagement/Domain/Exception/BookmarkedArticleNotFound.php new file mode 100644 index 0000000..f6f2f76 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/BookmarkedArticleNotFound.php @@ -0,0 +1,37 @@ + + */ +final class BookmarkedArticleNotFound extends \DomainException implements UserFacingError +{ + public static function with(ArticleId $articleId, BookmarkId $bookmarkId): self + { + return new self(sprintf('Article %s not found in bookmark %s', $articleId->toString(), $bookmarkId->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.bookmarked_article_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/CommentNotFound.php b/projects/backend/src/FeedManagement/Domain/Exception/CommentNotFound.php new file mode 100644 index 0000000..4324fe7 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/CommentNotFound.php @@ -0,0 +1,36 @@ + + */ +final class CommentNotFound extends \DomainException implements UserFacingError +{ + public static function withId(CommentId $id): self + { + return new self(sprintf('Comment with id "%s" not found.', $id->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.comment_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/FollowedSourceNotFound.php b/projects/backend/src/FeedManagement/Domain/Exception/FollowedSourceNotFound.php new file mode 100644 index 0000000..b4e07a4 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/FollowedSourceNotFound.php @@ -0,0 +1,37 @@ + + */ +final class FollowedSourceNotFound extends \DomainException implements UserFacingError +{ + public static function with(UserId $userId, SourceId $sourceId): self + { + return new self(sprintf('User %s does not follow source %s', $userId->toString(), $sourceId->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.followed_source_not_found'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Exception/SourceAlreadyFollowed.php b/projects/backend/src/FeedManagement/Domain/Exception/SourceAlreadyFollowed.php new file mode 100644 index 0000000..94b83b1 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Exception/SourceAlreadyFollowed.php @@ -0,0 +1,37 @@ + + */ +final class SourceAlreadyFollowed extends \DomainException implements UserFacingError +{ + public static function with(UserId $userId, SourceId $sourceId): self + { + return new self(sprintf('User %s already follows source %s', $userId->toString(), $sourceId->toString())); + } + + public function translationId(): string + { + return 'feed_management.exceptions.source_already_followed'; + } + + public function translationParameters(): array + { + return []; + } + + public function translationDomain(): string + { + return 'feed_management'; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Entity/Bookmark.php b/projects/backend/src/FeedManagement/Domain/Model/Entity/Bookmark.php new file mode 100644 index 0000000..ad3a895 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Entity/Bookmark.php @@ -0,0 +1,90 @@ + + */ +class Bookmark +{ + public readonly BookmarkId $id; + + /** + * @var DataCollection + */ + private(set) public iterable $articles; + + private function __construct( + public readonly User $user, + private(set) string $name, + private(set) ?string $description, + private(set) bool $isPublic = false, + private(set) ?\DateTimeImmutable $updatedAt = null, + public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable() + ) { + $this->id = new BookmarkId(); + $this->articles = new DataCollection(); + } + + public static function create(User $user, string $name, ?string $description = null): self + { + return new self($user, $name, $description); + } + + public function updateInfos(string $name, ?string $description = null): self + { + $this->name = $name; + $this->description = $description; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function markAsPrivate(): self + { + $this->isPublic = false; + + return $this; + } + + public function markAsPublic(): self + { + $this->isPublic = true; + + return $this; + } + + public function addArticle(Article $article): self + { + if ($this->articles->contains($article)) { + throw ArticleAlreadyBookmarked::with($article->id, $this->id); + } + + $this->articles->add($article); + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function removeArticle(Article $article): self + { + if (! $this->articles->contains($article)) { + throw BookmarkedArticleNotFound::with($article->id, $this->id); + } + + $this->articles->removeElement($article); + + return $this; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Entity/Comment.php b/projects/backend/src/FeedManagement/Domain/Model/Entity/Comment.php new file mode 100644 index 0000000..e08153a --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Entity/Comment.php @@ -0,0 +1,50 @@ + + */ +class Comment +{ + public readonly CommentId $id; + + public function __construct( + public readonly User $user, + public readonly Article $article, + public readonly string $content, + private(set) bool $isSpam = false, + private(set) Sentiment $sentiment = Sentiment::NEUTRAL, + public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(), + ) { + $this->id = new CommentId(); + } + + public static function create(User $user, Article $article, string $content): self + { + return new self($user, $article, $content); + } + + public function defineSentiment(Sentiment $sentiment): self + { + $this->sentiment = $sentiment; + + return $this; + } + + public function markAsSpam(): self + { + $this->isSpam = true; + + return $this; + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Entity/FollowedSource.php b/projects/backend/src/FeedManagement/Domain/Model/Entity/FollowedSource.php new file mode 100644 index 0000000..e192bc7 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Entity/FollowedSource.php @@ -0,0 +1,27 @@ + + */ +readonly class FollowedSource +{ + public FollowedSourceId $id; + + public function __construct( + public Source $source, + public User $follower, + public \DateTimeImmutable $createdAt = new \DateTimeImmutable(), + ) { + $this->id = new FollowedSourceId(); + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Filters/ArticleFilters.php b/projects/backend/src/FeedManagement/Domain/Model/Filters/ArticleFilters.php new file mode 100644 index 0000000..8582daa --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Filters/ArticleFilters.php @@ -0,0 +1,24 @@ + + */ +final class ArticleFilters +{ + public function __construct( + public ?string $search = null, + public ?string $category = null, + public ?DateRange $dateRange = null, + public SortDirection $sortDirection = SortDirection::DESC, + ) { + } +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Identity/BookmarkId.php b/projects/backend/src/FeedManagement/Domain/Model/Identity/BookmarkId.php new file mode 100644 index 0000000..7e10af3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Identity/BookmarkId.php @@ -0,0 +1,16 @@ + + */ +final class BookmarkId extends UuidV7 +{ +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Identity/CommentId.php b/projects/backend/src/FeedManagement/Domain/Model/Identity/CommentId.php new file mode 100644 index 0000000..25f2364 --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Identity/CommentId.php @@ -0,0 +1,16 @@ + + */ +final class CommentId extends UuidV7 +{ +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Identity/FollowedSourceId.php b/projects/backend/src/FeedManagement/Domain/Model/Identity/FollowedSourceId.php new file mode 100644 index 0000000..1cb2d1d --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Identity/FollowedSourceId.php @@ -0,0 +1,16 @@ + + */ +final class FollowedSourceId extends UuidV7 +{ +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Repository/BookmarkRepository.php b/projects/backend/src/FeedManagement/Domain/Model/Repository/BookmarkRepository.php new file mode 100644 index 0000000..797dcfe --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Repository/BookmarkRepository.php @@ -0,0 +1,22 @@ + + */ +interface BookmarkRepository +{ + public function add(Bookmark $bookmark): void; + + public function remove(Bookmark $bookmark): void; + + public function getById(BookmarkId $bookmarkId): Bookmark; +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Repository/CommentRepository.php b/projects/backend/src/FeedManagement/Domain/Model/Repository/CommentRepository.php new file mode 100644 index 0000000..b6a92fd --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Repository/CommentRepository.php @@ -0,0 +1,22 @@ + + */ +interface CommentRepository +{ + public function add(Comment $comment): void; + + public function remove(Comment $comment): void; + + public function getById(CommentId $commentId): Comment; +} diff --git a/projects/backend/src/FeedManagement/Domain/Model/Repository/FollowedSourceRepository.php b/projects/backend/src/FeedManagement/Domain/Model/Repository/FollowedSourceRepository.php new file mode 100644 index 0000000..007c99d --- /dev/null +++ b/projects/backend/src/FeedManagement/Domain/Model/Repository/FollowedSourceRepository.php @@ -0,0 +1,23 @@ + + */ +interface FollowedSourceRepository +{ + public function add(FollowedSource $followedSource): void; + + public function remove(FollowedSource $followedSource): void; + + public function getByUserId(UserId $userId, SourceId $sourceId): ?FollowedSource; +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php new file mode 100644 index 0000000..9e3f55a --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php @@ -0,0 +1,57 @@ + + */ +final readonly class GetArticleCommentListDbalHandler implements GetArticleCommentListHandler +{ + use PaginationQuery; + + public function __construct( + private Connection $connection, + ) { + } + + public function __invoke(GetArticleCommentList $query): CommentList + { + $qb = $this->connection->createQueryBuilder() + ->select( + 'c.id as comment_id', + 'c.content as comment_content', + 'c.sentiment as comment_sentiment', + 'c.created_at as comment_created_at' + ) + ->addSelect('u.id as user_id, u.name as user_name') + ->from('comment', 'c') + ->innerJoin('c', 'user', 'u', 'c.user_id = u.id') + ->where('c.article_id = :articleId') + ->orderBy('c.created_at', 'DESC') + ->setParameter('articleId', $query->articleId->toBinary(), ParameterType::BINARY); + + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('c.id', 'c.created_at')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('comment_id', 'comment_created_at')); + return CommentList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php new file mode 100644 index 0000000..83d5ce7 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php @@ -0,0 +1,62 @@ + + */ +final readonly class GetArticleDetailsDbalHandler implements GetArticleDetailsHandler +{ + use BookmarkQuery; + use SourceQuery; + use ArticleQuery; + + public function __construct( + private Connection $connection, + ) { + } + + #[\Override] + public function __invoke(GetArticleDetails $query): ArticleDetails + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addArticleDetailsSelectQuery($qb); + $qb = $this->addSourceOverviewSelectQuery($qb); + $qb = $this->addArticleBookmarkedExistsQuery($qb); + + $qb->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->from('article', 'a') + ->where('a.id = :articleId') + ->setParameter('articleId', $query->id->toBinary(), ParameterType::BINARY) + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ; + + try { + /** @var array|false $data */ + $data = $qb->executeQuery()->fetchAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + if ($data === false) { + throw ArticleNotFound::withId($query->id); + } + + return ArticleDetails::create($data); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php new file mode 100644 index 0000000..08273e6 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php @@ -0,0 +1,62 @@ + + */ +final readonly class GetArticleOverviewListDbalHandler implements GetArticleOverviewListHandler +{ + use PaginationQuery; + use BookmarkQuery; + use ArticleQuery; + use SourceQuery; + + public function __construct( + private Connection $connection, + ) { + } + + #[\Override] + public function __invoke(GetArticleOverviewList $query): ArticleOverviewList + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addArticleOverviewSelectQuery($qb); + $qb = $this->addSourceOverviewSelectQuery($qb); + $qb = $this->addArticleBookmarkedExistsQuery($qb); + + $qb->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + //->orderBy('a.published_at', $query->filters->sortDirection->value) + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ; + + $qb = $this->applyArticleFilters($qb, $query->filters); + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('article_id', 'article_published_at')); + return ArticleOverviewList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php new file mode 100644 index 0000000..b44d1b6 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php @@ -0,0 +1,56 @@ + + */ +final readonly class GetBookmarkListDbalHandler implements GetBookmarkListHandler +{ + use PaginationQuery; + use BookmarkQuery; + + public function __construct( + private Connection $connection, + ) { + } + + public function __invoke(GetBookmarkList $query): BookmarkList + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addBookmarkSelectQuery($qb); + + $qb->from('bookmark', 'b') + ->leftJoin('b', 'bookmark_article', 'ba', 'ba.bookmark_id = b.id') + ->where('b.user_id = :userId') + ->groupBy('b.id') + ->orderBy('b.id', 'DESC') + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ; + + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('b.id')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('bookmark_id')); + return BookmarkList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php new file mode 100644 index 0000000..e2d74da --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php @@ -0,0 +1,63 @@ + + */ +final readonly class GetBookmarkedArticleListDbalHandler implements GetBookmarkedArticleListHandler +{ + use PaginationQuery; + use ArticleQuery; + use SourceQuery; + + public function __construct( + private Connection $connection, + ) { + } + + public function __invoke(GetBookmarkedArticleList $query): ArticleOverviewList + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addArticleOverviewSelectQuery($qb); + $qb = $this->addSourceOverviewSelectQuery($qb); + + $qb + ->addSelect('1 as article_is_bookmarked') + ->from('bookmark_article', 'ba') + ->innerJoin('ba', 'article', 'a', 'a.id = ba.article_id') + ->innerJoin('ba', 'bookmark', 'b', 'b.id = ba.bookmark_id AND b.user_id = :userId') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where('b.id = :bookmarkId') + ->setParameter('bookmarkId', $query->bookmarkId->toBinary(), ParameterType::BINARY) + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ; + + $qb = $this->applyArticleFilters($qb, $query->filters); + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('article_id', 'article_published_at')); + return ArticleOverviewList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php new file mode 100644 index 0000000..7e0e728 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php @@ -0,0 +1,64 @@ + + */ +final readonly class GetSourceArticleOverviewListDbalHandler implements GetSourceArticleOverviewListHandler +{ + use PaginationQuery; + use BookmarkQuery; + use ArticleQuery; + use SourceQuery; + + public function __construct( + private Connection $connection, + ) { + } + + #[\Override] + public function __invoke(GetSourceArticleOverviewList $query): ArticleOverviewList + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addArticleOverviewSelectQuery($qb); + $qb = $this->addSourceOverviewSelectQuery($qb); + $qb = $this->addArticleBookmarkedExistsQuery($qb); + + $qb->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where('s.id = :sourceId') + ->orderBy('a.published_at', $query->filters->sortDirection->value) + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ->setParameter('sourceId', $query->sourceId->toBinary(), ParameterType::BINARY) + ; + + $qb = $this->applyArticleFilters($qb, $query->filters); + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('article_id', 'article_published_at')); + return ArticleOverviewList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php new file mode 100644 index 0000000..e28f122 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php @@ -0,0 +1,163 @@ + + */ +final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHandler +{ + use SourceQuery; + + private const int PUBLICATION_GRAPH_DAYS = 180; + + public function __construct( + private Connection $connection, + private CacheInterface $cache + ) { + } + + #[\Override] + public function __invoke(GetSourceDetails $query): SourceDetails + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addSourceDetailsSelectQuery($qb); + $qb = $this->addFollowedSourceExistsQuery($qb); + + $qb->from('source', 's') + ->leftJoin('s', 'article', 'a', 'a.source_id = s.id') + ->where('s.id = :sourceId') + ->setParameter('sourceId', $query->sourceId->toBinary(), ParameterType::BINARY) + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY); + + try { + $data = $qb->executeQuery()->fetchAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + if (empty($data['source_id'])) { + throw SourceNotFound::withId($query->sourceId); + } + + return SourceDetails::create( + $data, + $this->getPublicationGraph($query), + $this->getCategoryShares($query) + ); + } + + public function getPublicationGraph(GetSourceDetails $query): PublicationGraph + { + $cacheKey = SourceCacheAttributes::PUBLICATIONS->withId($query->sourceId->toString()); + $dateRange = DateRange::backward(days: self::PUBLICATION_GRAPH_DAYS); + + $qb = $this->connection->createQueryBuilder() + ->select('DATE(a.published_at) AS day, COUNT(a.id) AS count') + ->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where(' s.id = :sourceId') + ->andWhere('a.published_at BETWEEN FROM_UNIXTIME(:start) AND FROM_UNIXTIME(:end)') + ->groupBy('day') + ->orderBy('day', 'ASC') + ->setParameter('sourceId', $query->sourceId->toBinary(), ParameterType::BINARY) + ->setParameter('start', $dateRange->start, ParameterType::INTEGER) + ->setParameter('end', $dateRange->end, ParameterType::INTEGER) + ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); + + try { + /** @var list $data */ + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + return $this->cache->get($cacheKey, function (ItemInterface $item) use ($data, $dateRange): PublicationGraph { + $item->expiresAfter(SourceCacheAttributes::CACHE_TTL); + + $countsByDate = []; + foreach ($data as $row) { + $countsByDate[$row['day']] = (int) $row['count']; + } + + $start = \DateTimeImmutable::createFromTimestamp($dateRange->start); + $end = \DateTimeImmutable::createFromTimestamp($dateRange->end); + $period = new \DatePeriod($start, new \DateInterval('P1D'), $end); + + $heatmap = []; + foreach ($period as $date) { + $day = $date->format('Y-m-d'); + $heatmap[] = new PublicationEntry($day, $countsByDate[$day] ?? 0); + } + + return new PublicationGraph($heatmap, count($heatmap)); + }); + } + + public function getCategoryShares(GetSourceDetails $query): CategoryShares + { + $cacheKey = SourceCacheAttributes::CATEGORIES->withId($query->sourceId->toString()); + $qb = $this->connection->createQueryBuilder() + ->select('a.categories') + ->from('article', 'a') + ->innerJoin('a', 'source', 's', 'a.source_id = s.id') + ->where('s.id = :sourceId') + ->setParameter('sourceId', $query->sourceId->toBinary(), ParameterType::BINARY) + ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); + + try { + /** @var array $categories */ + $categories = $qb->executeQuery()->fetchFirstColumn(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + return $this->cache->get($cacheKey, function (ItemInterface $item) use ($categories): CategoryShares { + $item->expiresAfter(SourceCacheAttributes::CACHE_TTL); + + $counts = []; + foreach ($categories as $row) { + foreach (array_filter(array_map('trim', explode(',', $row))) as $cat) { + $counts[$cat] = ($counts[$cat] ?? 0) + 1; + } + } + + $total = array_sum($counts); + + $shares = array_map( + fn (string $category, int $count): CategoryShare => new CategoryShare( + category: $category, + count: $count, + percentage: $total > 0 ? round(($count / $total) * 100, 2) : 0.0 + ), + array_keys($counts), + array_values($counts) + ); + usort($shares, fn (CategoryShare $a, CategoryShare $b): int => ($a->count <=> $b->count) * -1); + + return new CategoryShares($shares, count($counts)); + }); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php new file mode 100644 index 0000000..01e00ec --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php @@ -0,0 +1,55 @@ + + */ +final readonly class GetSourceOverviewListDbalHandler implements GetSourceOverviewListHandler +{ + use PaginationQuery; + use SourceQuery; + + public function __construct( + private Connection $connection, + ) { + } + + #[\Override] + public function __invoke(GetSourceOverviewList $query): SourceOverviewList + { + $qb = $this->connection->createQueryBuilder(); + $qb = $this->addSourceOverviewSelectQuery($qb); + $qb = $this->addFollowedSourceExistsQuery($qb); + + $qb->from('source', 's') + ->groupBy('s.name') + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY) + ; + + $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('s.id', 's.created_at')); + + try { + $data = $qb->executeQuery()->fetchAllAssociative(); + } catch (\Throwable $e) { + throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); + } + + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('source_id')); + return SourceOverviewList::create($data, $pagination); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/ArticleQuery.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/ArticleQuery.php new file mode 100644 index 0000000..a0c88d2 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/ArticleQuery.php @@ -0,0 +1,82 @@ + + */ +trait ArticleQuery +{ + private function addArticleOverviewSelectQuery(QueryBuilder $qb): QueryBuilder + { + return $qb->addSelect( + 'a.id as article_id', + 'a.title as article_title', + 'a.link as article_link', + 'a.categories as article_categories', + 'a.excerpt as article_excerpt', + 'a.published_at as article_published_at', + 'a.image as article_image', + 'a.reading_time as article_reading_time', + ); + } + + private function addArticleDetailsSelectQuery(QueryBuilder $qb): QueryBuilder + { + return $qb->addSelect( + 'a.id as article_id', + 'a.title as article_title', + 'a.link as article_link', + 'a.categories as article_categories', + 'a.body as article_body', + 'a.hash as article_hash', + 'a.published_at as article_published_at', + 'a.crawled_at as article_crawled_at', + 'a.updated_at as article_updated_at', + 'a.bias as article_bias', + 'a.reliability as article_reliability', + 'a.transparency as article_transparency', + 'a.sentiment as article_sentiment', + 'a.metadata as article_metadata', + 'a.reading_time as article_reading_time', + ); + } + + /** + * Applies filters to the provided QueryBuilder instance based on the given ArticleFilters. + * + * @param QueryBuilder $qb The query builder instance to which filters will be applied. + * @param ArticleFilters $filters The filters containing criteria for filtering articles. + * + * @return QueryBuilder The updated query builder with the applied filters. + */ + private function applyArticleFilters(QueryBuilder $qb, ArticleFilters $filters): QueryBuilder + { + if ($filters->category !== null) { + $qb->andWhere('a.categories LIKE :category') + ->setParameter('category', sprintf('%%%s%%', $filters->category)); + } + + if ($filters->search !== null) { + $qb->andWhere('a.title LIKE :search') + ->setParameter('search', sprintf('%%%s%%', $filters->search)); + } + + if ($filters->dateRange instanceof DateRange) { + $qb->andWhere('a.published_at BETWEEN FROM_UNIXTIME(:start) AND FROM_UNIXTIME(:end)') + ->setParameter('start', $filters->dateRange->start, ParameterType::INTEGER) + ->setParameter('end', $filters->dateRange->end, ParameterType::INTEGER); + } + + return $qb; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/BookmarkQuery.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/BookmarkQuery.php new file mode 100644 index 0000000..f3f2df3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/BookmarkQuery.php @@ -0,0 +1,41 @@ + + */ +trait BookmarkQuery +{ + private function addBookmarkSelectQuery(QueryBuilder $qb): QueryBuilder + { + return $qb->addSelect( + 'b.id AS bookmark_id', + 'b.name AS bookmark_name', + 'b.description AS bookmark_description', + 'b.created_at AS bookmark_created_at', + 'b.updated_at AS bookmark_updated_at', + 'COUNT(ba.article_id) AS bookmark_articles_count', + 'b.is_public AS bookmark_is_public' + ); + } + + private function addArticleBookmarkedExistsQuery(QueryBuilder $qb): QueryBuilder + { + $subQb = $this->connection->createQueryBuilder() + ->select('1') + ->from('bookmark_article', 'ba') + ->innerJoin('ba', 'bookmark', 'b', 'ba.bookmark_id = b.id') + ->where('ba.article_id = a.id') + ->andWhere('b.user_id = :userId'); + $query = sprintf('EXISTS (%s)', $subQb->getSQL()); + + return $qb->addSelect(sprintf('%s as article_is_bookmarked', $query)); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php new file mode 100644 index 0000000..e54c0d2 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php @@ -0,0 +1,58 @@ + + */ +trait SourceQuery +{ + private function addSourceOverviewSelectQuery(QueryBuilder $qb): QueryBuilder + { + return $qb + ->addSelect( + 's.id as source_id', + 's.display_name as source_display_name', + "CONCAT('https://devscast.org/images/sources/', s.name, '.png') as source_image", + 's.url as source_url', + 's.name as source_name', + ); + } + + private function addSourceDetailsSelectQuery(QueryBuilder $qb): QueryBuilder + { + return $qb->addSelect( + 's.id as source_id', + 's.name as source_name', + 's.description as source_description', + 's.url as source_url', + 's.updated_at as source_updated_at', + 's.display_name as source_display_name', + 's.bias as source_bias', + 's.reliability as source_reliability', + 's.transparency as source_transparency', + "CONCAT('https://devscast.org/images/sources/', s.name, '.png') as source_image", + 'COUNT(a.hash) AS articles_count', + 'MAX(a.crawled_at) AS source_crawled_at', + 'COUNT(CASE WHEN a.metadata IS NOT NULL THEN 1 ELSE NULL END) AS articles_metadata_available', + ); + } + + private function addFollowedSourceExistsQuery(QueryBuilder $qb): QueryBuilder + { + $subQb = $this->connection->createQueryBuilder() + ->select('1') + ->from('followed_source', 'f') + ->where('f.source_id = s.id') + ->andWhere('f.follower_id = :userId'); + $query = sprintf('EXISTS (%s)', $subQb->getSQL()); + + return $qb->addSelect(sprintf('%s as source_is_followed', $query)); + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/BookmarkIdType.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/BookmarkIdType.php new file mode 100644 index 0000000..393c28e --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/BookmarkIdType.php @@ -0,0 +1,26 @@ + + */ +final class BookmarkIdType extends AbstractUidType +{ + public function getName(): string + { + return 'bookmark_id'; + } + + protected function getUidClass(): string + { + return BookmarkId::class; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/CommentIdType.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/CommentIdType.php new file mode 100644 index 0000000..ed82480 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/CommentIdType.php @@ -0,0 +1,26 @@ + + */ +final class CommentIdType extends AbstractUidType +{ + public function getName(): string + { + return 'comment_id'; + } + + protected function getUidClass(): string + { + return CommentId::class; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/FollowedSourceIdType.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/FollowedSourceIdType.php new file mode 100644 index 0000000..2d7f530 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Types/FollowedSourceIdType.php @@ -0,0 +1,26 @@ + + */ +final class FollowedSourceIdType extends AbstractUidType +{ + public function getName(): string + { + return 'followed_source_id'; + } + + protected function getUidClass(): string + { + return FollowedSourceId::class; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/BookmarkOrmRepository.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/BookmarkOrmRepository.php new file mode 100644 index 0000000..9bfb0e0 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/BookmarkOrmRepository.php @@ -0,0 +1,52 @@ + + * + * @author bernard-ng + */ +final class BookmarkOrmRepository extends ServiceEntityRepository implements BookmarkRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Bookmark::class); + } + + public function add(Bookmark $bookmark): void + { + $this->getEntityManager()->persist($bookmark); + $this->getEntityManager()->flush(); + } + + public function remove(Bookmark $bookmark): void + { + $this->getEntityManager()->remove($bookmark); + $this->getEntityManager()->flush(); + } + + public function getById(BookmarkId $bookmarkId): Bookmark + { + $bookmark = $this->findOneBy([ + 'id' => $bookmarkId, + ]); + + if ($bookmark === null) { + throw BookmarkNotFound::withId($bookmarkId); + } + + return $bookmark; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/CommentOrmRepository.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/CommentOrmRepository.php new file mode 100644 index 0000000..36306f4 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/CommentOrmRepository.php @@ -0,0 +1,52 @@ + + * + * @author bernard-ng + */ +final class CommentOrmRepository extends ServiceEntityRepository implements CommentRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Comment::class); + } + + public function add(Comment $comment): void + { + $this->getEntityManager()->persist($comment); + $this->getEntityManager()->flush(); + } + + public function remove(Comment $comment): void + { + $this->getEntityManager()->remove($comment); + $this->getEntityManager()->flush(); + } + + public function getById(CommentId $commentId): Comment + { + $comment = $this->findOneBy([ + 'id' => $commentId, + ]); + + if (! $comment instanceof Comment) { + throw CommentNotFound::withId($commentId); + } + + return $comment; + } +} diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php new file mode 100644 index 0000000..42b6d84 --- /dev/null +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php @@ -0,0 +1,51 @@ + + * + * @author bernard-ng + */ +final class FollowedSourceOrmRepository extends ServiceEntityRepository implements FollowedSourceRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, FollowedSource::class); + } + + public function add(FollowedSource $followedSource): void + { + $this->getEntityManager()->persist($followedSource); + $this->getEntityManager()->flush(); + } + + public function remove(FollowedSource $followedSource): void + { + $this->getEntityManager()->remove($followedSource); + $this->getEntityManager()->flush(); + } + + public function getByUserId(UserId $userId, SourceId $sourceId): ?FollowedSource + { + return $this->createQueryBuilder('fs') + ->andWhere('fs.follower = :userId') + ->andWhere('fs.source = :sourceId') + ->setParameter('sourceId', $sourceId->toBinary(), ParameterType::BINARY) + ->setParameter('userId', $userId->toBinary(), ParameterType::BINARY) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddArticleToBookmarkController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddArticleToBookmarkController.php new file mode 100644 index 0000000..a97d07d --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddArticleToBookmarkController.php @@ -0,0 +1,39 @@ + + */ +final class AddArticleToBookmarkController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks/{bookmarkId}/articles/{articleId}', + name: 'feed_management_add_article_to_bookmark', + requirements: [ + 'bookmarkId' => Requirement::UUID_V7, + 'articleId' => Requirement::UUID_V7, + ], + methods: ['POST'] + )] + public function __invoke(BookmarkId $bookmarkId, ArticleId $articleId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new AddArticleToBookmark($securityUser->userId, $articleId, $bookmarkId)); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddCommentToArticleController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddCommentToArticleController.php new file mode 100644 index 0000000..93309f2 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/AddCommentToArticleController.php @@ -0,0 +1,39 @@ + + */ +final class AddCommentToArticleController extends AbstractController +{ + #[Route( + path: '/api/feed/articles/{articleId}/comments', + name: 'feed_management_add_comment_to_article', + requirements: [ + 'articleId' => Requirement::UUID_V7, + ], + methods: ['POST'] + )] + public function __invoke(ArticleId $articleId, #[MapRequestPayload] AddCommentToArticleModel $model): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new AddCommentToArticle($securityUser->userId, $articleId, $model->content)); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/CreateBookmarkController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/CreateBookmarkController.php new file mode 100644 index 0000000..eafeb5f --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/CreateBookmarkController.php @@ -0,0 +1,38 @@ + + */ +final class CreateBookmarkController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks', + name: 'feed_management_create_bookmark', + methods: ['POST'] + )] + public function __invoke(#[MapRequestPayload] CreateBookmarkModel $model): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new CreateBookmark( + $securityUser->userId, + $model->name, + $model->description + )); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/DeleteBookmarkController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/DeleteBookmarkController.php new file mode 100644 index 0000000..d810b93 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/DeleteBookmarkController.php @@ -0,0 +1,37 @@ + + */ +final class DeleteBookmarkController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks/{bookmarkId}', + name: 'feed_management_delete_bookmark', + requirements: [ + 'bookmarkId' => Requirement::UUID_V7, + ], + methods: ['DELETE'] + )] + public function __invoke(BookmarkId $bookmarkId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new DeleteBookmark($securityUser->userId, $bookmarkId)); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/FollowSourceController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/FollowSourceController.php new file mode 100644 index 0000000..5b1e7c3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/FollowSourceController.php @@ -0,0 +1,37 @@ + + */ +final class FollowSourceController extends AbstractController +{ + #[Route( + path: '/api/feed/sources/{sourceId}/follow', + name: 'feed_management_follow_source', + requirements: [ + 'sourceId' => Requirement::UUID_V7, + ], + methods: ['POST'] + )] + public function __invoke(SourceId $sourceId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new FollowSource($sourceId, $securityUser->userId)); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleCommentListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleCommentListController.php new file mode 100644 index 0000000..280a74d --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleCommentListController.php @@ -0,0 +1,39 @@ + + */ +final class GetArticleCommentListController extends AbstractController +{ + #[Route( + path: '/api/feed/articles/{articleId}/comments', + name: 'feed_management_article_comment_list', + requirements: [ + 'articleId' => Requirement::UUID_V7, + ], + methods: ['GET'] + )] + public function __invoke(ArticleId $articleId, #[MapQueryString] Page $page): JsonResponse + { + /** @var CommentList $data */ + $data = $this->handleQuery(new GetArticleCommentList($articleId, $page)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleDetailsController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleDetailsController.php new file mode 100644 index 0000000..8fef249 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleDetailsController.php @@ -0,0 +1,36 @@ + + */ +final class GetArticleDetailsController extends AbstractController +{ + #[Route( + path: 'api/feed/articles/{articleId}', + name: 'feed_management_article_details', + requirements: [ + 'articleId' => Requirement::UUID_V7, + ], + methods: ['GET'] + )] + public function __invoke(ArticleId $articleId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetArticleDetails($articleId, $securityUser->userId)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleOverviewListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleOverviewListController.php new file mode 100644 index 0000000..a984895 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetArticleOverviewListController.php @@ -0,0 +1,36 @@ + + */ +final class GetArticleOverviewListController extends AbstractController +{ + #[Route( + path: 'api/feed/articles', + name: 'feed_management_article_overview_list', + methods: ['GET'] + )] + public function __invoke( + #[MapQueryString] Page $page, + #[MapQueryString] ArticleFilters $filters + ): JsonResponse { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetArticleOverviewList($securityUser->userId, $page, $filters)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkListController.php new file mode 100644 index 0000000..0178783 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkListController.php @@ -0,0 +1,33 @@ + + */ +final class GetBookmarkListController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks', + name: 'feed_management_bookmark_list', + methods: ['GET'] + )] + public function __invoke(#[MapQueryString] Page $page): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetBookmarkList($securityUser->userId, $page)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkedArticleListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkedArticleListController.php new file mode 100644 index 0000000..0ba81d3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetBookmarkedArticleListController.php @@ -0,0 +1,38 @@ + + */ +final class GetBookmarkedArticleListController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks/{bookmarkId}/articles', + name: 'feed_management_bookmarked_article_list', + requirements: [ + 'bookmarkId' => Requirement::UUID_V7, + ], + methods: ['GET'] + )] + public function __invoke(BookmarkId $bookmarkId, #[MapQueryString] Page $page): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetBookmarkedArticleList($securityUser->userId, $bookmarkId, $page)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceArticleOverviewListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceArticleOverviewListController.php new file mode 100644 index 0000000..99e1201 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceArticleOverviewListController.php @@ -0,0 +1,42 @@ + + */ +final class GetSourceArticleOverviewListController extends AbstractController +{ + #[Route( + path: 'api/feed/sources/{sourceId}/articles', + name: 'feed_management_source_article_list', + requirements: [ + 'sourceId' => Requirement::UUID_V7, + ], + methods: ['GET'] + )] + public function __invoke( + SourceId $sourceId, + #[MapQueryString] Page $page, + #[MapQueryString] ArticleFilters $filters + ): JsonResponse { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetSourceArticleOverviewList($sourceId, $securityUser->userId, $page, $filters)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceDetailsController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceDetailsController.php new file mode 100644 index 0000000..b6430f9 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceDetailsController.php @@ -0,0 +1,36 @@ + + */ +final class GetSourceDetailsController extends AbstractController +{ + #[Route( + path: '/api/feed/sources/{sourceId}', + name: 'feed_management_source_details', + requirements: [ + 'sourceId' => Requirement::UUID_V7, + ], + methods: ['GET'] + )] + public function __invoke(SourceId $sourceId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetSourceDetails($sourceId, $securityUser->userId)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceOverviewListController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceOverviewListController.php new file mode 100644 index 0000000..b72718e --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/GetSourceOverviewListController.php @@ -0,0 +1,33 @@ + + */ +final class GetSourceOverviewListController extends AbstractController +{ + #[Route( + path: '/api/feed/sources', + name: 'feed_management_source_overview_list', + methods: ['GET'] + )] + public function __invoke(#[MapQueryString] Page $page): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $data = $this->handleQuery(new GetSourceOverviewList($securityUser->userId, $page)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveArticleFromBookmarkController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveArticleFromBookmarkController.php new file mode 100644 index 0000000..64880b3 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveArticleFromBookmarkController.php @@ -0,0 +1,39 @@ + + */ +final class RemoveArticleFromBookmarkController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks/{bookmarkId}/articles/{articleId}', + name: 'feed_management_remove_article_from_bookmark', + requirements: [ + 'bookmarkId' => Requirement::UUID_V7, + 'articleId' => Requirement::UUID_V7, + ], + methods: ['DELETE'] + )] + public function __invoke(BookmarkId $bookmarkId, ArticleId $articleId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new RemoveArticleFromBookmark($securityUser->userId, $articleId, $bookmarkId)); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveCommentFromArticleController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveCommentFromArticleController.php new file mode 100644 index 0000000..f44673f --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/RemoveCommentFromArticleController.php @@ -0,0 +1,38 @@ + + */ +final class RemoveCommentFromArticleController extends AbstractController +{ + #[Route( + path: '/api/feed/articles/{articleId}/comments/{commentId}', + name: 'feed_management_remove_comment_from_article', + requirements: [ + 'articleId' => Requirement::UUID_V7, + 'commentId' => Requirement::UUID_V7, + ], + methods: ['DELETE'] + )] + public function __invoke(CommentId $commentId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new RemoveCommentFromArticle($securityUser->userId, $commentId)); + + return new JsonResponse(status: Response::HTTP_OK); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/UnfollowSourceController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/UnfollowSourceController.php new file mode 100644 index 0000000..0d98974 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/UnfollowSourceController.php @@ -0,0 +1,37 @@ + + */ +final class UnfollowSourceController extends AbstractController +{ + #[Route( + path: '/api/feed/sources/{sourceId}/unfollow', + name: 'feed_management_unfollow_source', + requirements: [ + 'sourceId' => Requirement::UUID_V7, + ], + methods: ['DELETE'] + )] + public function __invoke(SourceId $sourceId): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new UnfollowSource($sourceId, $securityUser->userId)); + + return new JsonResponse(status: Response::HTTP_OK); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/Web/Controller/UpdateBookmarkController.php b/projects/backend/src/FeedManagement/Presentation/Web/Controller/UpdateBookmarkController.php new file mode 100644 index 0000000..8d9a5e8 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/Web/Controller/UpdateBookmarkController.php @@ -0,0 +1,45 @@ + + */ +final class UpdateBookmarkController extends AbstractController +{ + #[Route( + path: '/api/feed/bookmarks/{bookmarkId}', + name: 'feed_management_update_bookmark', + requirements: [ + 'bookmarkId' => Requirement::UUID_V7, + ], + methods: ['PUT'] + )] + public function __invoke(BookmarkId $bookmarkId, #[MapRequestPayload] UpdateBookmarkModel $model): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new UpdateBookmark( + $securityUser->userId, + $bookmarkId, + $model->name, + $model->description, + $model->isPublic + )); + + return new JsonResponse(status: Response::HTTP_CREATED); + } +} diff --git a/projects/backend/src/FeedManagement/Presentation/WriteModel/AddCommentToArticleModel.php b/projects/backend/src/FeedManagement/Presentation/WriteModel/AddCommentToArticleModel.php new file mode 100644 index 0000000..0c972b5 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/WriteModel/AddCommentToArticleModel.php @@ -0,0 +1,19 @@ + + */ +final class AddCommentToArticleModel +{ + #[Assert\NotBlank] + #[Assert\Length(min: 5, max: 512)] + public string $content; +} diff --git a/projects/backend/src/FeedManagement/Presentation/WriteModel/CreateBookmarkModel.php b/projects/backend/src/FeedManagement/Presentation/WriteModel/CreateBookmarkModel.php new file mode 100644 index 0000000..8bd535f --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/WriteModel/CreateBookmarkModel.php @@ -0,0 +1,24 @@ + + */ +final class CreateBookmarkModel +{ + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + public string $name; + + #[Assert\Length(max: 512)] + public ?string $description = null; + + public bool $isPublic = false; +} diff --git a/projects/backend/src/FeedManagement/Presentation/WriteModel/UpdateBookmarkModel.php b/projects/backend/src/FeedManagement/Presentation/WriteModel/UpdateBookmarkModel.php new file mode 100644 index 0000000..9dcb155 --- /dev/null +++ b/projects/backend/src/FeedManagement/Presentation/WriteModel/UpdateBookmarkModel.php @@ -0,0 +1,24 @@ + + */ +final class UpdateBookmarkModel +{ + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + public string $name; + + #[Assert\Length(max: 512)] + public ?string $description = null; + + public bool $isPublic = false; +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountConfirmedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountConfirmedListener.php new file mode 100644 index 0000000..6e93abe --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountConfirmedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class AccountConfirmedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(AccountConfirmed $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new AccountConfirmedEmail($user->email, false, null); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountLockedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountLockedListener.php new file mode 100644 index 0000000..3c1b0cf --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountLockedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class AccountLockedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(AccountLocked $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new AccountLockedEmail($user->email, $event->token); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountUnlockedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountUnlockedListener.php new file mode 100644 index 0000000..ec0b924 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/AccountUnlockedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class AccountUnlockedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(AccountUnlocked $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new AccountUnlockedEmail($user->email); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/ConfirmationRequestedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/ConfirmationRequestedListener.php new file mode 100644 index 0000000..59973ae --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/ConfirmationRequestedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class ConfirmationRequestedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(ConfirmationRequested $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new ConfirmationRequestedEmail($user->email, $user->name, $event->token); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/LoginProfileChangedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/LoginProfileChangedListener.php new file mode 100644 index 0000000..1874400 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/LoginProfileChangedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class LoginProfileChangedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(LoginProfileChanged $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new LoginProfileChangedEmail($user->email, $event->device, $event->location); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordCreatedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordCreatedListener.php new file mode 100644 index 0000000..3a3f62f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordCreatedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class PasswordCreatedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(PasswordCreated $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new PasswordCreatedEmail($user->email, $event->password); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordForgottenListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordForgottenListener.php new file mode 100644 index 0000000..02ed5ef --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordForgottenListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class PasswordForgottenListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(PasswordForgotten $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new PasswordForgottenEmail($user->email, $event->token); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordResetListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordResetListener.php new file mode 100644 index 0000000..a165c5e --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordResetListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class PasswordResetListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(PasswordReset $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new PasswordResetEmail($user->email); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordUpdatedListener.php b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordUpdatedListener.php new file mode 100644 index 0000000..32c7d91 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/EventListener/PasswordUpdatedListener.php @@ -0,0 +1,33 @@ + + */ +final readonly class PasswordUpdatedListener implements EventListener +{ + public function __construct( + private Mailer $mailer, + private UserRepository $userRepository + ) { + } + + public function __invoke(PasswordUpdated $event): void + { + $user = $this->userRepository->getById($event->userId); + $email = new PasswordUpdatedEmail($user->email); + + $this->mailer->send($email); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountConfirmedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountConfirmedEmail.php new file mode 100644 index 0000000..51da50e --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountConfirmedEmail.php @@ -0,0 +1,68 @@ + + */ +final readonly class AccountConfirmedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private bool $isSocialLogin, + private ?string $socialLoginService + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.account_confirmed'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/account_confirmed'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'is_social_login' => $this->isSocialLogin, + 'social_login_service' => $this->socialLoginService, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountLockedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountLockedEmail.php new file mode 100644 index 0000000..4279332 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountLockedEmail.php @@ -0,0 +1,67 @@ + + */ +final readonly class AccountLockedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private GeneratedToken $token + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.account_locked'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/account_locked'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'token' => $this->token, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountUnlockedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountUnlockedEmail.php new file mode 100644 index 0000000..41c5cba --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/AccountUnlockedEmail.php @@ -0,0 +1,63 @@ + + */ +final readonly class AccountUnlockedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.account_unlocked'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/account_unlocked'; + } + + #[\Override] + public function templateVariables(): array + { + return []; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/ConfirmationRequestedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/ConfirmationRequestedEmail.php new file mode 100644 index 0000000..19589b4 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/ConfirmationRequestedEmail.php @@ -0,0 +1,69 @@ + + */ +final readonly class ConfirmationRequestedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private string $name, + private GeneratedToken $token + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.user_registered'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/user_registered'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'name' => $this->name, + 'token' => $this->token, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/LoginProfileChangedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/LoginProfileChangedEmail.php new file mode 100644 index 0000000..42a9bfd --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/LoginProfileChangedEmail.php @@ -0,0 +1,70 @@ + + */ +final readonly class LoginProfileChangedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private Device $device, + private GeoLocation $location + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.login_profile_changed'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/login_profile_changed'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'device' => $this->device, + 'location' => $this->location, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordCreatedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordCreatedEmail.php new file mode 100644 index 0000000..d90c877 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordCreatedEmail.php @@ -0,0 +1,67 @@ + + */ +final readonly class PasswordCreatedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private GeneratedCode $code + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.password_created'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/password_created'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'code' => $this->code, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordForgottenEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordForgottenEmail.php new file mode 100644 index 0000000..5c57b6e --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordForgottenEmail.php @@ -0,0 +1,67 @@ + + */ +final readonly class PasswordForgottenEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + private GeneratedToken $token + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.password_forgotten'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/password_forgotten'; + } + + #[\Override] + public function templateVariables(): array + { + return [ + 'token' => $this->token, + ]; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordResetEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordResetEmail.php new file mode 100644 index 0000000..64f29d7 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordResetEmail.php @@ -0,0 +1,63 @@ + + */ +final readonly class PasswordResetEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient, + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.password_reset'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/password_reset'; + } + + #[\Override] + public function templateVariables(): array + { + return []; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordUpdatedEmail.php b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordUpdatedEmail.php new file mode 100644 index 0000000..38ced63 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/Mailing/PasswordUpdatedEmail.php @@ -0,0 +1,63 @@ + + */ +final readonly class PasswordUpdatedEmail implements EmailDefinition +{ + public function __construct( + private EmailAddress $recipient + ) { + } + + #[\Override] + public function recipient(): EmailAddress + { + return $this->recipient; + } + + #[\Override] + public function subject(): string + { + return 'identity_and_access.emails.subjects.password_updated'; + } + + #[\Override] + public function subjectVariables(): array + { + return []; + } + + #[\Override] + public function template(): string + { + return 'identity_and_access/password_updated'; + } + + #[\Override] + public function templateVariables(): array + { + return []; + } + + #[\Override] + public function locale(): string + { + return 'fr'; + } + + #[\Override] + public function getDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php b/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php new file mode 100644 index 0000000..bc3e182 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php @@ -0,0 +1,37 @@ + + */ +final readonly class UserProfile +{ + public function __construct( + public UserId $id, + public string $name, + public EmailAddress $email, + public \DateTimeImmutable $createdAt, + public ?\DateTimeImmutable $updatedAt = null, + ) { + } + + public static function create(array $item): self + { + return new self( + UserId::fromBinary($item['user_id']), + DataMapping::string($item, 'user_name'), + EmailAddress::from(DataMapping::string($item, 'user_email')), + DataMapping::dateTime($item, 'user_created_at'), + DataMapping::nullableDateTime($item, 'user_updated_at'), + ); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ConfirmAccount.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ConfirmAccount.php new file mode 100644 index 0000000..9d71f97 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ConfirmAccount.php @@ -0,0 +1,20 @@ + + */ +final readonly class ConfirmAccount +{ + public function __construct( + public GeneratedToken $token + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/LockAccount.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/LockAccount.php new file mode 100644 index 0000000..b1f161d --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/LockAccount.php @@ -0,0 +1,20 @@ + + */ +final readonly class LockAccount +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/Register.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/Register.php new file mode 100644 index 0000000..98fafd6 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/Register.php @@ -0,0 +1,24 @@ + + */ +final readonly class Register +{ + public function __construct( + public string $name, + public EmailAddress $email, + public ?string $password, + public Roles $roles = new Roles() + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginAttempt.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginAttempt.php new file mode 100644 index 0000000..02628e5 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginAttempt.php @@ -0,0 +1,20 @@ + + */ +final readonly class RegisterLoginAttempt +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginSuccess.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginSuccess.php new file mode 100644 index 0000000..a1703f3 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RegisterLoginSuccess.php @@ -0,0 +1,22 @@ + + */ +final readonly class RegisterLoginSuccess +{ + public function __construct( + public UserId $userId, + public ClientProfile $profile + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RequestPassword.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RequestPassword.php new file mode 100644 index 0000000..4c6d41b --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/RequestPassword.php @@ -0,0 +1,20 @@ + + */ +final readonly class RequestPassword +{ + public function __construct( + public EmailAddress $email + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ResetPassword.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ResetPassword.php new file mode 100644 index 0000000..31a70fd --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/ResetPassword.php @@ -0,0 +1,21 @@ + + */ +final readonly class ResetPassword +{ + public function __construct( + public GeneratedToken $token, + public string $password + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UnlockAccount.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UnlockAccount.php new file mode 100644 index 0000000..3dc3618 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UnlockAccount.php @@ -0,0 +1,20 @@ + + */ +final readonly class UnlockAccount +{ + public function __construct( + public GeneratedToken $token, + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UpdatePassword.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UpdatePassword.php new file mode 100644 index 0000000..e3fd8c1 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Command/UpdatePassword.php @@ -0,0 +1,22 @@ + + */ +final readonly class UpdatePassword +{ + public function __construct( + public UserId $userId, + public string $current, + public string $password, + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ConfirmAccountHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ConfirmAccountHandler.php new file mode 100644 index 0000000..0094365 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ConfirmAccountHandler.php @@ -0,0 +1,41 @@ + + */ +final readonly class ConfirmAccountHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(ConfirmAccount $command): void + { + $token = $this->verificationTokenRepository->getByToken( + $command->token, + TokenPurpose::CONFIRM_ACCOUNT + ); + + $user = $this->userRepository->getById($token->user->id); + $user->confirmAccount(); + + $this->verificationTokenRepository->remove($token); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/LockAccountHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/LockAccountHandler.php new file mode 100644 index 0000000..d673831 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/LockAccountHandler.php @@ -0,0 +1,48 @@ + + */ +final readonly class LockAccountHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private SecretGenerator $secretGenerator, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(LockAccount $command): void + { + $user = $this->userRepository->getById($command->userId); + $token = $this->createVerificationToken($user); + $user->lockAccount($token); + + $this->userRepository->add($user); + $this->verificationTokenRepository->add($token); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } + + private function createVerificationToken(User $user): VerificationToken + { + $secret = $this->secretGenerator->generateToken(); + return VerificationToken::create($user, $secret, TokenPurpose::UNLOCK_ACCOUNT); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterHandler.php new file mode 100644 index 0000000..a1bf2f2 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterHandler.php @@ -0,0 +1,60 @@ + + */ +final readonly class RegisterHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private PasswordHasher $passwordHasher, + private SecretGenerator $secretGenerator, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(Register $command): void + { + $user = $this->userRepository->getByEmail($command->email); + if ($user instanceof User) { + throw EmailAlreadyUsed::with($command->email); + } + + $user = User::register($command->name, $command->email, $command->roles); + $password = $command->password ?? $this->secretGenerator->generateCode(); + $token = $this->createVerificationToken($user); + + $user + ->definePassword($password, $this->passwordHasher) + ->requestAccountConfirmation($token); + + $this->userRepository->add($user); + $this->verificationTokenRepository->add($token); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } + + private function createVerificationToken(User $user): VerificationToken + { + $secret = $this->secretGenerator->generateToken(); + return VerificationToken::create($user, $secret, TokenPurpose::CONFIRM_ACCOUNT); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginAttemptHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginAttemptHandler.php new file mode 100644 index 0000000..293a694 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginAttemptHandler.php @@ -0,0 +1,42 @@ + + */ +final readonly class RegisterLoginAttemptHandler implements CommandHandler +{ + private const int ATTEMPTS_LIMIT = 3; + + public function __construct( + private UserRepository $userRepository, + private LoginAttemptRepository $loginAttemptRepository, + private CommandBus $commandBus + ) { + } + + public function __invoke(RegisterLoginAttempt $command): void + { + $user = $this->userRepository->getById($command->userId); + $attempts = $this->loginAttemptRepository->countBy($user); + + if ($attempts < self::ATTEMPTS_LIMIT) { + $this->loginAttemptRepository->add(LoginAttempt::create($user)); + } elseif ($user->isLocked === false) { + $this->commandBus->handle(new LockAccount($user->id)); + } + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginSuccessHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginSuccessHandler.php new file mode 100644 index 0000000..5b6964c --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RegisterLoginSuccessHandler.php @@ -0,0 +1,48 @@ + + */ +final readonly class RegisterLoginSuccessHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private LoginHistoryRepository $loginHistoryRepository, + private LoginAttemptRepository $loginAttemptRepository, + private ClientProfiler $clientProfiler, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(RegisterLoginSuccess $command): void + { + $user = $this->userRepository->getById($command->userId); + $device = $this->clientProfiler->detect($command->profile); + $location = $this->clientProfiler->locate($command->profile); + + $current = LoginHistory::create($user, $command->profile->userIp, $device, $location); + $previous = $this->loginHistoryRepository->getLastBy($user); + if ($previous instanceof LoginHistory) { + $current->matchPreviousProfile($previous); + } + + $this->loginHistoryRepository->add($current); + $this->loginAttemptRepository->deleteBy($user); + $this->eventDispatcher->dispatch($current->releaseEvents()); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RequestPasswordHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RequestPasswordHandler.php new file mode 100644 index 0000000..9eb0b9a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/RequestPasswordHandler.php @@ -0,0 +1,53 @@ + + */ +final readonly class RequestPasswordHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private SecretGenerator $secretGenerator, + private EventDispatcher $eventDispatcher, + ) { + } + + public function __invoke(RequestPassword $command): void + { + $user = $this->userRepository->getByEmail($command->email); + if (! $user instanceof User) { + throw UserNotFound::withEmail($command->email); + } + + $token = $this->createVerificationToken($user); + $user->requestPasswordReset($token); + + $this->userRepository->add($user); + $this->verificationTokenRepository->add($token); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } + + private function createVerificationToken(User $user): VerificationToken + { + $secret = $this->secretGenerator->generateToken(); + return VerificationToken::create($user, $secret, TokenPurpose::PASSWORD_RESET); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ResetPasswordHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ResetPasswordHandler.php new file mode 100644 index 0000000..03045ea --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/ResetPasswordHandler.php @@ -0,0 +1,44 @@ + + */ +final readonly class ResetPasswordHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private PasswordHasher $passwordHasher, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(ResetPassword $command): void + { + $token = $this->verificationTokenRepository->getByToken( + $command->token, + TokenPurpose::PASSWORD_RESET + ); + + $user = $this->userRepository->getById($token->user->id); + $user->resetPassword($command->password, $this->passwordHasher); + + $this->userRepository->add($user); + $this->verificationTokenRepository->remove($token); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UnlockAccountHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UnlockAccountHandler.php new file mode 100644 index 0000000..19fce79 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UnlockAccountHandler.php @@ -0,0 +1,45 @@ + + */ +final readonly class UnlockAccountHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private VerificationTokenRepository $verificationTokenRepository, + private LoginAttemptRepository $loginAttemptRepository, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(UnlockAccount $command): void + { + $token = $this->verificationTokenRepository->getByToken( + $command->token, + TokenPurpose::UNLOCK_ACCOUNT + ); + + $user = $this->userRepository->getById($token->user->id); + $user->unlockAccount(); + + $this->userRepository->add($user); + $this->verificationTokenRepository->remove($token); + $this->loginAttemptRepository->deleteBy($user); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UpdatePasswordHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UpdatePasswordHandler.php new file mode 100644 index 0000000..0a99230 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/CommandHandler/UpdatePasswordHandler.php @@ -0,0 +1,35 @@ + + */ +final readonly class UpdatePasswordHandler implements CommandHandler +{ + public function __construct( + private UserRepository $userRepository, + private PasswordHasher $passwordHasher, + private EventDispatcher $eventDispatcher + ) { + } + + public function __invoke(UpdatePassword $command): void + { + $user = $this->userRepository->getById($command->userId); + $user->updatePassword($command->current, $command->password, $this->passwordHasher); + + $this->userRepository->add($user); + $this->eventDispatcher->dispatch($user->releaseEvents()); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/Query/GetUserProfile.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/Query/GetUserProfile.php new file mode 100644 index 0000000..b44cc3e --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/Query/GetUserProfile.php @@ -0,0 +1,20 @@ + + */ +final readonly class GetUserProfile +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Application/UseCase/QueryHandler/GetUserProfileHandler.php b/projects/backend/src/IdentityAndAccess/Application/UseCase/QueryHandler/GetUserProfileHandler.php new file mode 100644 index 0000000..920029f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Application/UseCase/QueryHandler/GetUserProfileHandler.php @@ -0,0 +1,19 @@ + + */ +interface GetUserProfileHandler extends QueryHandler +{ + public function __invoke(GetUserProfile $query): UserProfile; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/AccountConfirmed.php b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountConfirmed.php new file mode 100644 index 0000000..568bba7 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountConfirmed.php @@ -0,0 +1,20 @@ + + */ +final readonly class AccountConfirmed +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/AccountLocked.php b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountLocked.php new file mode 100644 index 0000000..afb1371 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountLocked.php @@ -0,0 +1,22 @@ + + */ +final readonly class AccountLocked +{ + public function __construct( + public UserId $userId, + public GeneratedToken $token + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/AccountUnlocked.php b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountUnlocked.php new file mode 100644 index 0000000..afa7510 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/AccountUnlocked.php @@ -0,0 +1,20 @@ + + */ +final readonly class AccountUnlocked +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/ConfirmationRequested.php b/projects/backend/src/IdentityAndAccess/Domain/Event/ConfirmationRequested.php new file mode 100644 index 0000000..65e20bc --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/ConfirmationRequested.php @@ -0,0 +1,22 @@ + + */ +final readonly class ConfirmationRequested +{ + public function __construct( + public UserId $userId, + public GeneratedToken $token + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/EmailUpdated.php b/projects/backend/src/IdentityAndAccess/Domain/Event/EmailUpdated.php new file mode 100644 index 0000000..cee3936 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/EmailUpdated.php @@ -0,0 +1,23 @@ + + */ +final readonly class EmailUpdated +{ + public function __construct( + public UserId $userId, + public EmailAddress $previous, + public EmailAddress $current + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/LoginProfileChanged.php b/projects/backend/src/IdentityAndAccess/Domain/Event/LoginProfileChanged.php new file mode 100644 index 0000000..3ae0805 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/LoginProfileChanged.php @@ -0,0 +1,24 @@ + + */ +final readonly class LoginProfileChanged +{ + public function __construct( + public UserId $userId, + public Device $device, + public GeoLocation $location + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordCreated.php b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordCreated.php new file mode 100644 index 0000000..d5b2c6a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordCreated.php @@ -0,0 +1,22 @@ + + */ +final readonly class PasswordCreated +{ + public function __construct( + public UserId $userId, + #[\SensitiveParameter] public GeneratedCode $password + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordForgotten.php b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordForgotten.php new file mode 100644 index 0000000..3ae3fc2 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordForgotten.php @@ -0,0 +1,22 @@ + + */ +final readonly class PasswordForgotten +{ + public function __construct( + public UserId $userId, + public GeneratedToken $token, + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordReset.php b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordReset.php new file mode 100644 index 0000000..0f7c395 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordReset.php @@ -0,0 +1,20 @@ + + */ +final readonly class PasswordReset +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordUpdated.php b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordUpdated.php new file mode 100644 index 0000000..c722ef6 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Event/PasswordUpdated.php @@ -0,0 +1,20 @@ + + */ +final readonly class PasswordUpdated +{ + public function __construct( + public UserId $userId + ) { + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountIsLocked.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountIsLocked.php new file mode 100644 index 0000000..e6f740b --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountIsLocked.php @@ -0,0 +1,33 @@ + + */ +final class AccountIsLocked extends \DomainException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.account_is_locked'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountNotConfirmed.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountNotConfirmed.php new file mode 100644 index 0000000..4ff7158 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/AccountNotConfirmed.php @@ -0,0 +1,33 @@ + + */ +final class AccountNotConfirmed extends \DomainException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.account_not_confirmed'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/EmailAlreadyUsed.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/EmailAlreadyUsed.php new file mode 100644 index 0000000..cc18c1b --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/EmailAlreadyUsed.php @@ -0,0 +1,39 @@ + + */ +final class EmailAlreadyUsed extends \DomainException implements UserFacingError +{ + public static function with(EmailAddress $email): self + { + return new self(sprintf('the %s email is already used by another user', $email->value)); + } + + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.email_already_used'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidCurrentPassword.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidCurrentPassword.php new file mode 100644 index 0000000..707db97 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidCurrentPassword.php @@ -0,0 +1,33 @@ + + */ +final class InvalidCurrentPassword extends \DomainException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.invalid_current_password'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidVerificationToken.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidVerificationToken.php new file mode 100644 index 0000000..e493297 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/InvalidVerificationToken.php @@ -0,0 +1,33 @@ + + */ +final class InvalidVerificationToken extends \DomainException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.invalid_verification_token'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/PasswordAlreadyDefined.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/PasswordAlreadyDefined.php new file mode 100644 index 0000000..c81947d --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/PasswordAlreadyDefined.php @@ -0,0 +1,33 @@ + + */ +final class PasswordAlreadyDefined extends \DomainException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.password_already_defined'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/PermissionNotGranted.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/PermissionNotGranted.php new file mode 100644 index 0000000..84a4d94 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/PermissionNotGranted.php @@ -0,0 +1,37 @@ + + */ +final class PermissionNotGranted extends \DomainException implements UserFacingError +{ + public static function withReason(string $message): self + { + return new self($message); + } + + public function translationId(): string + { + return 'identity_and_access.exceptions.permission_not_granted'; + } + + public function translationParameters(): array + { + return [ + '{reason}' => $this->message, + ]; + } + + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Exception/UserNotFound.php b/projects/backend/src/IdentityAndAccess/Domain/Exception/UserNotFound.php new file mode 100644 index 0000000..32c7381 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Exception/UserNotFound.php @@ -0,0 +1,45 @@ + + */ +final class UserNotFound extends \DomainException implements UserFacingError +{ + public static function withEmail(EmailAddress $email): self + { + return new self(\sprintf('User with email %s was not found', $email->value)); + } + + public static function withId(UserId $userId): self + { + return new self(\sprintf('User with id %s was not found', $userId->toString())); + } + + #[\Override] + public function translationId(): string + { + return 'identity_and_access.exceptions.user_not_found'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'identity_and_access'; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginAttempt.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginAttempt.php new file mode 100644 index 0000000..c7de765 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginAttempt.php @@ -0,0 +1,29 @@ + + */ +readonly class LoginAttempt +{ + public LoginAttemptId $id; + + private function __construct( + public User $user, + public \DateTimeImmutable $createdAt = new \DateTimeImmutable() + ) { + $this->id = new LoginAttemptId(); + } + + public static function create(User $user): self + { + return new self($user); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginHistory.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginHistory.php new file mode 100644 index 0000000..4c781db --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/LoginHistory.php @@ -0,0 +1,52 @@ + + */ +class LoginHistory +{ + use EventEmitterTrait; + + public readonly LoginHistoryId $id; + + private function __construct( + public readonly User $user, + public readonly ?string $ipAddress, + public readonly Device $device, + public readonly GeoLocation $location, + public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable() + ) { + $this->id = new LoginHistoryId(); + } + + public static function create(User $user, ?string $userIp, Device $device, GeoLocation $location): self + { + return new self($user, $userIp, $device, $location); + } + + public function matchPreviousProfile(self $previous): self + { + if ( + $this->ipAddress !== $previous->ipAddress || + $this->location->city !== $previous->location->city || + $this->location->country !== $previous->location->country || + $this->device->operatingSystem !== $previous->device->operatingSystem + ) { + $this->emitEvent(new LoginProfileChanged($this->user->id, $this->device, $this->location)); + } + + return $this; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/RefreshToken.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/RefreshToken.php new file mode 100644 index 0000000..532e904 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/RefreshToken.php @@ -0,0 +1,11 @@ + + */ +class User +{ + use EventEmitterTrait; + + public readonly UserId $id; + + private function __construct( + private(set) string $name, + private(set) EmailAddress $email, + private(set) Roles $roles, + private(set) ?string $password = null, + private(set) bool $isLocked = false, + private(set) bool $isConfirmed = false, + private(set) ?\DateTimeImmutable $updatedAt = null, + public readonly ?\DateTimeImmutable $createdAt = new \DateTimeImmutable(), + ) { + $this->id = new UserId(); + } + + public function lockAccount(VerificationToken $verificationToken): self + { + $this->isLocked = true; + $this->emitEvent(new AccountLocked($this->id, $verificationToken->token)); + + return $this; + } + + public function unlockAccount(): self + { + $this->isLocked = false; + $this->emitEvent(new AccountUnlocked($this->id)); + + return $this; + } + + public function confirmAccount(): self + { + $this->isConfirmed = true; + $this->emitEvent(new AccountConfirmed($this->id)); + + return $this; + } + + public static function register(string $name, EmailAddress $email, ?Roles $roles): self + { + return new self($name, $email, $roles ?? Roles::user()); + } + + public function updateProfile(string $name, Roles $roles): static + { + $this->name = $name; + $this->roles = $roles; + $this->updatedAt = new \DateTimeImmutable(); + + return $this; + } + + public function updateEmail(EmailAddress $email): self + { + $previous = $this->email; + $this->email = $email; + $this->emitEvent(new EmailUpdated($this->id, $previous, $email)); + + return $this; + } + + public function resetPassword(string $password, PasswordHasher $passwordHasher): void + { + $this->password = $passwordHasher->hash($this, $password); + $this->emitEvent(new PasswordReset($this->id)); + } + + public function updatePassword(string $current, string $new, PasswordHasher $passwordHasher): self + { + if ($this->password === null || ! $passwordHasher->verify($this, $current)) { + throw new InvalidCurrentPassword(); + } + + $this->password = $passwordHasher->hash($this, $new); + $this->emitEvent(new PasswordUpdated($this->id)); + + return $this; + } + + public function definePassword(GeneratedCode|string $password, PasswordHasher $passwordHasher): self + { + if ($this->password !== null) { + throw new PasswordAlreadyDefined(); + } + + $this->password = $passwordHasher->hash($this, (string) $password); + $this->updatedAt = new \DateTimeImmutable(); + + if ($password instanceof GeneratedCode) { + $this->emitEvent(new PasswordCreated($this->id, $password)); + } + + return $this; + } + + public function requestPasswordReset(VerificationToken $verificationToken): self + { + $this->emitEvent(new PasswordForgotten($this->id, $verificationToken->token)); + + return $this; + } + + public function requestAccountConfirmation(VerificationToken $verificationToken): self + { + $this->emitEvent(new ConfirmationRequested($this->id, $verificationToken->token)); + + return $this; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/VerificationToken.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/VerificationToken.php new file mode 100644 index 0000000..b9e2643 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Entity/VerificationToken.php @@ -0,0 +1,52 @@ + + */ +readonly class VerificationToken +{ + public const string DEFAULT_VALIDITY = 'PT2H'; + + public VerificationTokenId $id; + + public function __construct( + public User $user, + public GeneratedToken $token, + public TokenPurpose $purpose, + public \DateTimeImmutable $createdAt = new \DateTimeImmutable() + ) { + $this->id = new VerificationTokenId(); + } + + public static function create(User $user, GeneratedToken $token, TokenPurpose $purpose): self + { + return new self($user, $token, $purpose); + } + + public function isExpired(): bool + { + $now = new \DateTimeImmutable(); + $validUntil = (\DateTime::createFromImmutable($this->createdAt)) + ->add(new \DateInterval(self::DEFAULT_VALIDITY)); + + return $now > $validUntil; + } + + public function assertValid(): void + { + if ($this->isExpired()) { + throw new InvalidVerificationToken(); + } + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginAttemptId.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginAttemptId.php new file mode 100644 index 0000000..65d6c4a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginAttemptId.php @@ -0,0 +1,16 @@ + + */ +final class LoginAttemptId extends UuidV7 +{ +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginHistoryId.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginHistoryId.php new file mode 100644 index 0000000..7aeda4b --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/LoginHistoryId.php @@ -0,0 +1,16 @@ + + */ +final class LoginHistoryId extends UuidV7 +{ +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/UserId.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/UserId.php new file mode 100644 index 0000000..1c1fd79 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/UserId.php @@ -0,0 +1,16 @@ + + */ +final class UserId extends UuidV7 +{ +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/VerificationTokenId.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/VerificationTokenId.php new file mode 100644 index 0000000..ba0c1fd --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Identity/VerificationTokenId.php @@ -0,0 +1,16 @@ + + */ +final class VerificationTokenId extends UuidV7 +{ +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginAttemptRepository.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginAttemptRepository.php new file mode 100644 index 0000000..343bc8f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginAttemptRepository.php @@ -0,0 +1,24 @@ + + */ +interface LoginAttemptRepository +{ + public function add(LoginAttempt $loginAttempt): void; + + public function remove(LoginAttempt $loginAttempt): void; + + public function countBy(User $user): int; + + public function deleteBy(User $user): void; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginHistoryRepository.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginHistoryRepository.php new file mode 100644 index 0000000..93f697d --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/LoginHistoryRepository.php @@ -0,0 +1,22 @@ + + */ +interface LoginHistoryRepository +{ + public function add(LoginHistory $loginHistory): void; + + public function remove(LoginHistory $loginHistory): void; + + public function getLastBy(User $user): ?LoginHistory; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/UserRepository.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/UserRepository.php new file mode 100644 index 0000000..7a9966f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/UserRepository.php @@ -0,0 +1,25 @@ + + */ +interface UserRepository +{ + public function add(User $user): void; + + public function remove(User $user): void; + + public function getById(UserId $userId): User; + + public function getByEmail(EmailAddress $email): ?User; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/VerificationTokenRepository.php b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/VerificationTokenRepository.php new file mode 100644 index 0000000..8316e70 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/Repository/VerificationTokenRepository.php @@ -0,0 +1,23 @@ + + */ +interface VerificationTokenRepository +{ + public function add(VerificationToken $verificationToken): void; + + public function remove(VerificationToken $verificationToken): void; + + public function getByToken(GeneratedToken $token, TokenPurpose $purpose): VerificationToken; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Role.php b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Role.php new file mode 100644 index 0000000..f1b6b77 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Role.php @@ -0,0 +1,16 @@ + + */ +enum Role: string +{ + case USER = 'ROLE_USER'; + case ADMIN = 'ROLE_ADMIN'; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Roles.php b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Roles.php new file mode 100644 index 0000000..cbf6688 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Roles.php @@ -0,0 +1,55 @@ + + */ +readonly class Roles implements \Stringable +{ + private array $roles; + + public function __construct(array $roles = [Role::USER]) + { + Assert::notEmpty($roles, 'identity_and_access.validations.empty_roles'); + Assert::allIsInstanceOf($roles, Role::class); + + $roles[] = Role::USER; + $this->roles = array_unique(\array_map(fn (Role $role) => $role->value, $roles)); + } + + #[\Override] + public function __toString(): string + { + return implode(',', $this->roles); + } + + public static function admin(): self + { + return new self([Role::USER, Role::ADMIN]); + } + + public static function user(): self + { + return new self(); + } + + public function toArray(): array + { + return $this->roles; + } + + public static function fromArray(array $roles): self + { + return new self($roles); + } + + public function contains(string $role): bool + { + return in_array($role, $this->roles, true); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedCode.php b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedCode.php new file mode 100644 index 0000000..ec84c4d --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedCode.php @@ -0,0 +1,27 @@ + + */ +final readonly class GeneratedCode implements \Stringable +{ + public function __construct( + public string $code + ) { + Assert::notEmpty($this->code); + } + + #[\Override] + public function __toString(): string + { + return $this->code; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedToken.php b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedToken.php new file mode 100644 index 0000000..5e80938 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/Secret/GeneratedToken.php @@ -0,0 +1,32 @@ + + */ +final readonly class GeneratedToken implements \Stringable +{ + public function __construct( + public string $token, + ) { + Assert::notEmpty($this->token); + } + + #[\Override] + public function __toString(): string + { + return $this->token; + } + + public function isEqualTo(self $token): bool + { + return $this->token === $token->token; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/TokenPurpose.php b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/TokenPurpose.php new file mode 100644 index 0000000..1d58972 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Model/ValueObject/TokenPurpose.php @@ -0,0 +1,18 @@ + + */ +enum TokenPurpose: string +{ + case CONFIRM_ACCOUNT = 'confirm_account'; + case PASSWORD_RESET = 'password_reset'; + case UNLOCK_ACCOUNT = 'unlock_account'; + case DELETE_ACCOUNT = 'delete_account'; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Service/PasswordHasher.php b/projects/backend/src/IdentityAndAccess/Domain/Service/PasswordHasher.php new file mode 100644 index 0000000..06ab025 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Service/PasswordHasher.php @@ -0,0 +1,19 @@ + + */ +interface PasswordHasher +{ + public function hash(User $user, string $password): string; + + public function verify(User $user, string $password): bool; +} diff --git a/projects/backend/src/IdentityAndAccess/Domain/Service/SecretGenerator.php b/projects/backend/src/IdentityAndAccess/Domain/Service/SecretGenerator.php new file mode 100644 index 0000000..926c2d0 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Domain/Service/SecretGenerator.php @@ -0,0 +1,20 @@ + + */ +interface SecretGenerator +{ + public function generateToken(int $length = 60): GeneratedToken; + + public function generateCode(int $length = 6): GeneratedCode; +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginFailureListener.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginFailureListener.php new file mode 100644 index 0000000..d3890a8 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginFailureListener.php @@ -0,0 +1,36 @@ + + */ +#[AsEventListener(LoginFailureEvent::class)] +final readonly class LoginFailureListener +{ + public function __construct( + private CommandBus $commandBus + ) { + } + + public function __invoke(LoginFailureEvent $event): void + { + /** @var SecurityUser|null $user */ + $user = $event->getPassport()?->getUser(); + + if ($user && $event->getException() instanceof BadCredentialsException) { + $this->commandBus->handle(new RegisterLoginAttempt($user->userId)); + } + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginSuccessListener.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginSuccessListener.php new file mode 100644 index 0000000..46799d1 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/EventListener/LoginSuccessListener.php @@ -0,0 +1,43 @@ + + */ +#[AsEventListener(InteractiveLoginEvent::class)] +final readonly class LoginSuccessListener +{ + public function __construct( + private CommandBus $commandBus, + ) { + } + + public function __invoke(InteractiveLoginEvent $event): void + { + /** @var SecurityUser|null $user */ + $user = $event->getAuthenticationToken()->getUser(); + + if ($user !== null) { + $profile = new ClientProfile( + IpUtils::anonymize((string) $event->getRequest()->getClientIp(), 1), + $event->getRequest()->headers->get('User-Agent'), + $event->getRequest()->server->all() + ); + + $this->commandBus->handle(new RegisterLoginSuccess($user->userId, $profile)); + } + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUser.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUser.php new file mode 100644 index 0000000..0ce63ba --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUser.php @@ -0,0 +1,78 @@ + + */ +final readonly class SecurityUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface +{ + public function __construct( + public UserId $userId, + public EmailAddress $email, + public ?string $password, + public array $roles, + public bool $isLocked, + public bool $isConfirmed + ) { + } + + public static function create(User $user): self + { + return new self( + $user->id, + $user->email, + (string) $user->password, + $user->roles->toArray(), + $user->isLocked, + $user->isConfirmed + ); + } + + #[\Override] + public function getPassword(): ?string + { + return $this->password; + } + + #[\Override] + public function getRoles(): array + { + return $this->roles; + } + + #[\Override] + public function eraseCredentials(): void + { + } + + #[\Override] + public function getUserIdentifier(): string + { + /** @var non-empty-string $email */ + $email = $this->email->value; + + return $email; + } + + #[\Override] + public function isEqualTo(UserInterface $user): bool + { + if (! $user instanceof self) { + return false; + } + + return $this->userId->equals($user->userId); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUserProvider.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUserProvider.php new file mode 100644 index 0000000..ffcd34f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/SecurityUserProvider.php @@ -0,0 +1,50 @@ + + * + * @author bernard-ng + */ +final readonly class SecurityUserProvider implements UserProviderInterface +{ + public function __construct( + private UserRepository $userRepository, + ) { + } + + #[\Override] + public function refreshUser(UserInterface $user): UserInterface + { + return $this->loadUserByIdentifier($user->getUserIdentifier()); + } + + #[\Override] + public function loadUserByIdentifier(string $identifier): UserInterface + { + $user = $this->userRepository->getByEmail(EmailAddress::from($identifier)); + if (! $user instanceof User) { + throw new UserNotFoundException(); + } + + return SecurityUser::create($user); + } + + #[\Override] + public function supportsClass(string $class): bool + { + return $class === SecurityUser::class; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserChecker.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserChecker.php new file mode 100644 index 0000000..5d5a5bc --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserChecker.php @@ -0,0 +1,34 @@ + + */ +final readonly class UserChecker implements UserCheckerInterface +{ + #[\Override] + public function checkPreAuth(UserInterface $user): void + { + if ($user instanceof SecurityUser && $user->isLocked) { + throw new AccountIsLocked(); + } + } + + #[\Override] + public function checkPostAuth(UserInterface $user): void + { + if ($user instanceof SecurityUser && $user->isConfirmed === false) { + throw new AccountNotConfirmed(); + } + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserPasswordHasher.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserPasswordHasher.php new file mode 100644 index 0000000..8d30d85 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Framework/Symfony/Security/UserPasswordHasher.php @@ -0,0 +1,36 @@ + + */ +final readonly class UserPasswordHasher implements PasswordHasher +{ + public function __construct( + private UserPasswordHasherInterface $passwordHasher + ) { + } + + #[\Override] + public function hash(User $user, string $password): string + { + $securityUser = SecurityUser::create($user); + return $this->passwordHasher->hashPassword($securityUser, $password); + } + + #[\Override] + public function verify(User $user, string $password): bool + { + $securityUser = SecurityUser::create($user); + return $this->passwordHasher->isPasswordValid($securityUser, $password); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php new file mode 100644 index 0000000..982671b --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php @@ -0,0 +1,49 @@ + + */ +final readonly class GetUserProfileDbalHandler implements GetUserProfileHandler +{ + public function __construct( + private Connection $connection + ) { + } + + public function __invoke(GetUserProfile $query): UserProfile + { + $qb = $this->connection->createQueryBuilder() + ->select( + 'u.id as user_id', + 'u.name as user_name', + 'u.email as user_email', + 'u.created_at as user_created_at', + 'u.updated_at as user_updated_at' + ) + ->from('user', 'u') + ->where('u.id = :userId') + ->setParameter('userId', $query->userId->toBinary(), ParameterType::BINARY); + + /** @var array|false $data */ + $data = $qb->executeQuery()->fetchAssociative(); + + if ($data === false) { + throw UserNotFound::withId($query->userId); + } + + return UserProfile::create($data); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginAttemptIdType.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginAttemptIdType.php new file mode 100644 index 0000000..2641b3a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginAttemptIdType.php @@ -0,0 +1,28 @@ + + */ +final class LoginAttemptIdType extends AbstractUidType +{ + #[\Override] + public function getName(): string + { + return 'login_attempt_id'; + } + + #[\Override] + protected function getUidClass(): string + { + return LoginAttemptId::class; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginHistoryIdType.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginHistoryIdType.php new file mode 100644 index 0000000..ca2f984 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/LoginHistoryIdType.php @@ -0,0 +1,28 @@ + + */ +final class LoginHistoryIdType extends AbstractUidType +{ + #[\Override] + public function getName(): string + { + return 'login_history_id'; + } + + #[\Override] + protected function getUidClass(): string + { + return LoginHistoryId::class; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/UserIdType.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/UserIdType.php new file mode 100644 index 0000000..ad4a10f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/UserIdType.php @@ -0,0 +1,28 @@ + + */ +final class UserIdType extends AbstractUidType +{ + #[\Override] + public function getName(): string + { + return 'user_id'; + } + + #[\Override] + protected function getUidClass(): string + { + return UserId::class; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/VerificationTokenIdType.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/VerificationTokenIdType.php new file mode 100644 index 0000000..930e43a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/Types/VerificationTokenIdType.php @@ -0,0 +1,28 @@ + + */ +final class VerificationTokenIdType extends AbstractUidType +{ + #[\Override] + public function getName(): string + { + return 'verification_token_id'; + } + + #[\Override] + protected function getUidClass(): string + { + return VerificationTokenId::class; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginAttemptOrmRepository.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginAttemptOrmRepository.php new file mode 100644 index 0000000..80ce64d --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginAttemptOrmRepository.php @@ -0,0 +1,59 @@ + + * + * @author bernard-ng + */ +final class LoginAttemptOrmRepository extends ServiceEntityRepository implements LoginAttemptRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, LoginAttempt::class); + } + + #[\Override] + public function add(LoginAttempt $loginAttempt): void + { + $this->getEntityManager()->persist($loginAttempt); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function remove(LoginAttempt $loginAttempt): void + { + $this->getEntityManager()->remove($loginAttempt); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function countBy(User $user): int + { + return $this->count([ + 'user' => $user, + ]); + } + + #[\Override] + public function deleteBy(User $user): void + { + $this->createQueryBuilder('la') + ->delete(LoginAttempt::class, 'la') + ->where('la.user = :user') + ->setParameter('user', $user->id->toBinary()) + ->getQuery() + ->execute(); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginHistoryOrmRepository.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginHistoryOrmRepository.php new file mode 100644 index 0000000..a0a8ce2 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/LoginHistoryOrmRepository.php @@ -0,0 +1,55 @@ + + * + * @author bernard-ng + */ +final class LoginHistoryOrmRepository extends ServiceEntityRepository implements LoginHistoryRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, LoginHistory::class); + } + + #[\Override] + public function add(LoginHistory $loginHistory): void + { + $this->getEntityManager()->persist($loginHistory); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function remove(LoginHistory $loginHistory): void + { + $this->getEntityManager()->remove($loginHistory); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function getLastBy(User $user): ?LoginHistory + { + /** @var LoginHistory|null $loginHistory */ + $loginHistory = $this->createQueryBuilder('lh') + ->andWhere('lh.user = :user') + ->setParameter('user', $user) + ->orderBy('lh.createdAt', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + return $loginHistory; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/UserOrmRepository.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/UserOrmRepository.php new file mode 100644 index 0000000..c7a02f3 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/UserOrmRepository.php @@ -0,0 +1,64 @@ + + * + * @author bernard-ng + */ +final class UserOrmRepository extends ServiceEntityRepository implements UserRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + #[\Override] + public function add(User $user): void + { + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function remove(User $user): void + { + $this->getEntityManager()->remove($user); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function getById(UserId $userId): User + { + $user = $this->findOneBy([ + 'id' => $userId, + ]); + + if ($user === null) { + throw UserNotFound::withId($userId); + } + + return $user; + } + + #[\Override] + public function getByEmail(EmailAddress $email): ?User + { + return $this->findOneBy([ + 'email' => $email, + ]); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/VerificationTokenOrmRepository.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/VerificationTokenOrmRepository.php new file mode 100644 index 0000000..6319efd --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/ORM/VerificationTokenOrmRepository.php @@ -0,0 +1,57 @@ + + * + * @author bernard-ng + */ +final class VerificationTokenOrmRepository extends ServiceEntityRepository implements VerificationTokenRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, VerificationToken::class); + } + + #[\Override] + public function add(VerificationToken $verificationToken): void + { + $this->getEntityManager()->persist($verificationToken); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function remove(VerificationToken $verificationToken): void + { + $this->getEntityManager()->remove($verificationToken); + $this->getEntityManager()->flush(); + } + + #[\Override] + public function getByToken(GeneratedToken $token, TokenPurpose $purpose): VerificationToken + { + $token = $this->findOneBy([ + 'token.token' => $token->token, + 'purpose' => $purpose->value, + ]); + + if ($token === null) { + throw new InvalidVerificationToken(); + } + + return $token; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Secret/RandomizerSecretGenerator.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Secret/RandomizerSecretGenerator.php new file mode 100644 index 0000000..3cf3237 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Secret/RandomizerSecretGenerator.php @@ -0,0 +1,41 @@ + + */ +final readonly class RandomizerSecretGenerator implements SecretGenerator +{ + private const string ALLOWED_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + #[\Override] + public function generateToken(int $length = 60): GeneratedToken + { + $value = new Randomizer() + ->getBytesFromString(self::ALLOWED_CHARACTERS, $length); + + return new GeneratedToken($value); + } + + #[\Override] + public function generateCode(int $length = 6): GeneratedCode + { + $min = 10 ** ($length - 1); + $max = 10 ** $length - 1; + + $value = new Randomizer() + ->getInt($min, $max); + + return new GeneratedCode((string) $value); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Console/RegisterConsole.php b/projects/backend/src/IdentityAndAccess/Presentation/Console/RegisterConsole.php new file mode 100644 index 0000000..991c26f --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Console/RegisterConsole.php @@ -0,0 +1,92 @@ + + */ +#[AsCommand('app:user-register', 'register a new user')] +final class RegisterConsole extends Command +{ + use AskArgumentFeature; + + private SymfonyStyle $io; + + public function __construct( + private readonly CommandBus $commandBus, + ) { + parent::__construct(); + } + + #[\Override] + protected function configure(): void + { + $this + ->setDescription('Creates users and stores them in the database') + ->addArgument('name', InputArgument::OPTIONAL, 'The name of the new user') + ->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user') + ->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user') + ->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator'); + } + + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + #[\Override] + protected function interact(InputInterface $input, OutputInterface $output): void + { + if ( + $input->getArgument('name') !== null && + $input->getArgument('email') !== null && + $input->getArgument('password') !== null + ) { + return; + } + + $this->askArgument($input, 'name'); + $this->askArgument($input, 'email'); + $this->askArgument($input, 'password', true); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $name */ + $name = $input->getArgument('name'); + + /** @var string $email */ + $email = $input->getArgument('email'); + + /** @var string $password */ + $password = $input->getArgument('password'); + + /** @var bool $admin */ + $admin = $input->getOption('admin'); + + $command = new Register($name, EmailAddress::from($email), $password, $admin ? Roles::admin() : Roles::user()); + $this->commandBus->handle($command); + $this->io->success(\sprintf('%s was created: %s', $admin ? 'ADMIN' : 'USER', $email)); + + return Command::SUCCESS; + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ConfirmAccountController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ConfirmAccountController.php new file mode 100644 index 0000000..3ed1950 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ConfirmAccountController.php @@ -0,0 +1,36 @@ + + */ +final class ConfirmAccountController extends AbstractController +{ + #[Route( + path: '/api/account/confirm/{token}', + name: 'identity_and_access_confirm_account', + requirements: [ + 'token' => Requirement::ASCII_SLUG, + ], + methods: ['GET'] + )] + public function __invoke(string $token): JsonResponse + { + $token = new GeneratedToken($token); + $this->handleCommand(new ConfirmAccount($token)); + + return new JsonResponse(status: 200); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/GetUserProfileController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/GetUserProfileController.php new file mode 100644 index 0000000..dedf01a --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/GetUserProfileController.php @@ -0,0 +1,31 @@ + + */ +final class GetUserProfileController extends AbstractController +{ + #[Route( + path: '/api/me', + name: 'identity_and_access_me', + methods: ['GET'] + )] + public function __invoke(): JsonResponse + { + $security = $this->getSecurityUser(); + $data = $this->handleQuery(new GetUserProfile($security->userId)); + + return JsonResponse::fromJsonString($this->serialize($data)); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RegisterController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RegisterController.php new file mode 100644 index 0000000..344d8fd --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RegisterController.php @@ -0,0 +1,37 @@ + + */ +final class RegisterController extends AbstractController +{ + #[Route( + path: '/api/register', + name: 'identity_and_access_register', + methods: ['POST'] + )] + public function __invoke(#[MapRequestPayload] RegisterModel $model): JsonResponse + { + $this->handleCommand(new Register( + $model->name, + EmailAddress::from($model->email), + $model->password + )); + + return new JsonResponse(status: 201); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RequestPasswordController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RequestPasswordController.php new file mode 100644 index 0000000..fcdcdc5 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/RequestPasswordController.php @@ -0,0 +1,34 @@ + + */ +final class RequestPasswordController extends AbstractController +{ + #[Route( + path: '/api/password/request', + name: 'identity_and_access_request_password', + methods: ['POST'] + )] + public function __invoke(#[MapRequestPayload] RequestPasswordModel $model): JsonResponse + { + $email = EmailAddress::from($model->email); + $this->handleCommand(new RequestPassword($email)); + + return new JsonResponse(status: 200); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ResetPasswordController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ResetPasswordController.php new file mode 100644 index 0000000..ffdbf45 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/ResetPasswordController.php @@ -0,0 +1,38 @@ + + */ +final class ResetPasswordController extends AbstractController +{ + #[Route( + path: '/api/password/reset/{token}', + name: 'identity_and_access_reset_password', + requirements: [ + 'token' => Requirement::ASCII_SLUG, + ], + methods: ['POST'] + )] + public function __invoke(#[MapRequestPayload] ResetPasswordModel $model, string $token): JsonResponse + { + $token = new GeneratedToken($token); + $this->handleCommand(new ResetPassword($token, $model->password)); + + return new JsonResponse(status: 200); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UnlockAccountController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UnlockAccountController.php new file mode 100644 index 0000000..17bc56c --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UnlockAccountController.php @@ -0,0 +1,36 @@ + + */ +final class UnlockAccountController extends AbstractController +{ + #[Route( + path: '/api/account/unlock/{token}', + name: 'identity_and_access_unlock_account', + requirements: [ + 'token' => Requirement::ASCII_SLUG, + ], + methods: ['GET'] + )] + public function __invoke(string $token): JsonResponse + { + $token = new GeneratedToken($token); + $this->handleCommand(new UnlockAccount($token)); + + return new JsonResponse(status: 200); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UpdatePasswordController.php b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UpdatePasswordController.php new file mode 100644 index 0000000..4130ca5 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/Web/Controller/UpdatePasswordController.php @@ -0,0 +1,37 @@ + + */ +final class UpdatePasswordController extends AbstractController +{ + #[Route( + path: '/api/password/update', + name: 'identity_and_access_update_password', + methods: ['POST'] + )] + public function __invoke(#[MapRequestPayload] UpdatePasswordModel $model): JsonResponse + { + $securityUser = $this->getSecurityUser(); + $this->handleCommand(new UpdatePassword( + $securityUser->userId, + $model->current, + $model->password + )); + + return new JsonResponse(status: 200); + } +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RegisterModel.php b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RegisterModel.php new file mode 100644 index 0000000..98b15e3 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RegisterModel.php @@ -0,0 +1,27 @@ + + */ +final class RegisterModel +{ + #[Assert\NotBlank] + #[Assert\Length(min: 3, max: 255)] + public string $name; + + #[Assert\NotBlank] + #[Assert\Email] + public string $email; + + #[Assert\NotBlank] + #[Assert\Length(max: 512)] + public string $password; +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RequestPasswordModel.php b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RequestPasswordModel.php new file mode 100644 index 0000000..7ff5f79 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/RequestPasswordModel.php @@ -0,0 +1,20 @@ + + */ +final class RequestPasswordModel +{ + #[Assert\NotBlank] + #[Assert\Email] + #[Assert\Length(max: 255)] + public string $email; +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/ResetPasswordModel.php b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/ResetPasswordModel.php new file mode 100644 index 0000000..7aeff90 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/ResetPasswordModel.php @@ -0,0 +1,26 @@ + + */ +final class ResetPasswordModel +{ + #[Assert\NotBlank] + #[Assert\Length(max: 512)] + #[Assert\PasswordStrength] + public string $password; + + #[Assert\EqualTo( + propertyPath: 'password', + message: 'identity_and_access.exceptions.passwords_do_not_match', + )] + public string $confirm; +} diff --git a/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/UpdatePasswordModel.php b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/UpdatePasswordModel.php new file mode 100644 index 0000000..aa38401 --- /dev/null +++ b/projects/backend/src/IdentityAndAccess/Presentation/WriteModel/UpdatePasswordModel.php @@ -0,0 +1,29 @@ + + */ +final class UpdatePasswordModel +{ + #[Assert\NotBlank] + public string $current; + + #[Assert\NotBlank] + #[Assert\Length(max: 512)] + #[Assert\PasswordStrength] + public string $password; + + #[Assert\EqualTo( + propertyPath: 'password', + message: 'identity_and_access.exceptions.passwords_do_not_match', + )] + public string $confirm; +} diff --git a/projects/backend/src/SharedKernel/Application/Asset/AssetType.php b/projects/backend/src/SharedKernel/Application/Asset/AssetType.php new file mode 100644 index 0000000..25963b2 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Asset/AssetType.php @@ -0,0 +1,15 @@ + + */ +enum AssetType +{ + case SOURCE_PROFILE_IMAGE; +} diff --git a/projects/backend/src/SharedKernel/Application/Asset/AssetUrlProvider.php b/projects/backend/src/SharedKernel/Application/Asset/AssetUrlProvider.php new file mode 100644 index 0000000..0f8cddc --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Asset/AssetUrlProvider.php @@ -0,0 +1,15 @@ + + */ +interface AssetUrlProvider +{ + public function getUrl(string $id, AssetType $type): ?string; +} diff --git a/projects/backend/src/SharedKernel/Application/Mailing/EmailDefinition.php b/projects/backend/src/SharedKernel/Application/Mailing/EmailDefinition.php new file mode 100644 index 0000000..79d6941 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Mailing/EmailDefinition.php @@ -0,0 +1,29 @@ + + */ +interface EmailDefinition +{ + public function recipient(): EmailAddress; + + public function subject(): string; + + public function subjectVariables(): array; + + public function template(): string; + + public function templateVariables(): array; + + public function locale(): string; + + public function getDomain(): string; +} diff --git a/projects/backend/src/SharedKernel/Application/Mailing/Mailer.php b/projects/backend/src/SharedKernel/Application/Mailing/Mailer.php new file mode 100644 index 0000000..f87d0e0 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Mailing/Mailer.php @@ -0,0 +1,15 @@ + + */ +interface Mailer +{ + public function send(EmailDefinition $email): void; +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/AsyncMessage.php b/projects/backend/src/SharedKernel/Application/Messaging/AsyncMessage.php new file mode 100644 index 0000000..009291a --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/AsyncMessage.php @@ -0,0 +1,14 @@ + + */ +interface AsyncMessage +{ +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/CommandBus.php b/projects/backend/src/SharedKernel/Application/Messaging/CommandBus.php new file mode 100644 index 0000000..8121f35 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/CommandBus.php @@ -0,0 +1,15 @@ + + */ +interface CommandBus +{ + public function handle(object $message): mixed; +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/CommandHandler.php b/projects/backend/src/SharedKernel/Application/Messaging/CommandHandler.php new file mode 100644 index 0000000..a9cd9ea --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/CommandHandler.php @@ -0,0 +1,14 @@ + + */ +interface CommandHandler +{ +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/MessageBus.php b/projects/backend/src/SharedKernel/Application/Messaging/MessageBus.php new file mode 100644 index 0000000..81051af --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/MessageBus.php @@ -0,0 +1,15 @@ + + */ +interface MessageBus +{ + public function dispatch(AsyncMessage $message): void; +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/MessageHandler.php b/projects/backend/src/SharedKernel/Application/Messaging/MessageHandler.php new file mode 100644 index 0000000..3e3e446 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/MessageHandler.php @@ -0,0 +1,14 @@ + + */ +interface MessageHandler +{ +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/QueryBus.php b/projects/backend/src/SharedKernel/Application/Messaging/QueryBus.php new file mode 100644 index 0000000..1987e89 --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/QueryBus.php @@ -0,0 +1,15 @@ + + */ +interface QueryBus +{ + public function handle(object $message): mixed; +} diff --git a/projects/backend/src/SharedKernel/Application/Messaging/QueryHandler.php b/projects/backend/src/SharedKernel/Application/Messaging/QueryHandler.php new file mode 100644 index 0000000..e88190c --- /dev/null +++ b/projects/backend/src/SharedKernel/Application/Messaging/QueryHandler.php @@ -0,0 +1,14 @@ + + */ +interface QueryHandler +{ +} diff --git a/projects/backend/src/SharedKernel/Domain/Application.php b/projects/backend/src/SharedKernel/Domain/Application.php new file mode 100644 index 0000000..2f2b1b3 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Application.php @@ -0,0 +1,33 @@ + + */ +final class Application +{ + public string $name = 'DRC News Corpus'; + + public string $website = 'https://research.devscast.org/drc-news-corpus'; + + public string $emailAddress = 'contact@devscast.tech'; + + public string $infoAddress = 'contact@devscast.tech'; + + public string $emailName = 'DRC News Corpus'; + + public string $legalName = 'Devscast Software SàSu'; + + public string $legalRegistrationCode = ''; + + public string $legalVatCode = ''; + + public string $legalAddress = '10465, Avenue Lac kipopo, Lubumbashi, Haut-Katanga, RDC'; + + public string $legalPhone = '+243892530482'; +} diff --git a/projects/backend/src/SharedKernel/Domain/Assert.php b/projects/backend/src/SharedKernel/Domain/Assert.php new file mode 100644 index 0000000..265829d --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Assert.php @@ -0,0 +1,21 @@ + + */ +final class Assert extends \Webmozart\Assert\Assert +{ + #[\Override] + protected static function reportInvalidArgument($message): void + { + throw new InvalidArgument($message); + } +} diff --git a/projects/backend/src/SharedKernel/Domain/DataTransfert/DataExporter.php b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataExporter.php new file mode 100644 index 0000000..cbcee8a --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataExporter.php @@ -0,0 +1,15 @@ + + */ +interface DataExporter +{ + public function export(iterable $data, TransfertSetting $setting = new TransfertSetting()): \SplFileObject; +} diff --git a/projects/backend/src/SharedKernel/Domain/DataTransfert/DataImporter.php b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataImporter.php new file mode 100644 index 0000000..1a0b798 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataImporter.php @@ -0,0 +1,15 @@ + + */ +interface DataImporter +{ + public function import(\SplFileObject $file, TransfertSetting $setting = new TransfertSetting()): iterable; +} diff --git a/projects/backend/src/SharedKernel/Domain/DataTransfert/DataMapping.php b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataMapping.php new file mode 100644 index 0000000..521f3a5 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/DataTransfert/DataMapping.php @@ -0,0 +1,180 @@ + + */ +abstract class DataMapping +{ + /** + * @param array $data + * @param non-empty-string $key + */ + public static function uuid(array $data, string $key): UuidV7 + { + Assert::keyExists($data, $key); + return UuidV7::fromString($data[$key]); + } + + /** + * @template T of \BackedEnum + * @param array $data + * @param non-empty-string $key + * @param class-string $class + * @phpstan-return T + */ + public static function enum(array $data, string $key, string $class): \BackedEnum + { + Assert::keyExists($data, $key); + return $class::from($data[$key]); + } + + /** + * @template T of \BackedEnum + * @param array $data + * @param non-empty-string $key + * @param class-string $class + * @phpstan-return T + */ + public static function nullableEnum(array $data, string $key, string $class): ?\BackedEnum + { + Assert::keyExists($data, $key); + return $class::tryFrom($data[$key]); + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function string(array $data, string $key): string + { + if (! isset($data[$key]) || $data[$key] === '') { + return ''; + } + + return strval($data[$key]); + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function nullableString(array $data, string $key): ?string + { + if (! isset($data[$key]) || $data[$key] === '') { + return null; + } + + return strval($data[$key]); + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function boolean(array $data, string $key): bool + { + return isset($data[$key]) && (bool) $data[$key]; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function nullableBoolean(array $data, string $key): ?bool + { + if (! isset($data[$key])) { + return null; + } + + return (bool) $data[$key]; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function integer(array $data, string $key): int + { + return isset($data[$key]) ? (int) $data[$key] : 0; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function nullableInteger(array $data, string $key): ?int + { + if (! isset($data[$key])) { + return null; + } + + return (int) $data[$key]; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function float(array $data, string $key): float + { + return isset($data[$key]) ? (float) $data[$key] : 0.0; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function nullableFloat(array $data, string $key): ?float + { + if (! isset($data[$key])) { + return null; + } + + return (float) $data[$key]; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function datetime(array $data, string $key, string $format = 'Y-m-d H:i:s'): \DateTimeImmutable + { + Assert::keyExists($data, $key); + $datetime = \DateTimeImmutable::createFromFormat($format, $data[$key]); + + if ($datetime === false) { + throw new \InvalidArgumentException('Invalid datetime format'); + } + + return $datetime; + } + + /** + * @param array $data + * @param non-empty-string $key + */ + public static function nullableDatetime(array $data, string $key, string $format = 'Y-m-d H:i:s'): ?\DateTimeImmutable + { + if (! isset($data[$key])) { + return null; + } + + $datetime = \DateTimeImmutable::createFromFormat($format, $data[$key]); + + if ($datetime === false) { + throw new \InvalidArgumentException('Invalid datetime format'); + } + + return $datetime; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/DataTransfert/TransfertSetting.php b/projects/backend/src/SharedKernel/Domain/DataTransfert/TransfertSetting.php new file mode 100644 index 0000000..a88754a --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/DataTransfert/TransfertSetting.php @@ -0,0 +1,20 @@ + + */ +final readonly class TransfertSetting +{ + public function __construct( + public ?string $filename = null, + public ?string $type = null, + public string $format = 'csv' + ) { + } +} diff --git a/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventDispatcher.php b/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventDispatcher.php new file mode 100644 index 0000000..7285dec --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventDispatcher.php @@ -0,0 +1,18 @@ + + */ +interface EventDispatcher +{ + /** + * @param array $events + */ + public function dispatch(array $events): void; +} diff --git a/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventEmitterTrait.php b/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventEmitterTrait.php new file mode 100644 index 0000000..34948e0 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/EventDispatcher/EventEmitterTrait.php @@ -0,0 +1,31 @@ + + */ +trait EventEmitterTrait +{ + /** + * @var array + */ + private array $emittedEvents = []; + + public function emitEvent(object $event): void + { + $this->emittedEvents[] = $event; + } + + /** + * @return array + */ + public function releaseEvents(): array + { + return $this->emittedEvents; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/EventListener/EventListener.php b/projects/backend/src/SharedKernel/Domain/EventListener/EventListener.php new file mode 100644 index 0000000..5719b74 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/EventListener/EventListener.php @@ -0,0 +1,14 @@ + + */ +interface EventListener +{ +} diff --git a/projects/backend/src/SharedKernel/Domain/Exception/InvalidArgument.php b/projects/backend/src/SharedKernel/Domain/Exception/InvalidArgument.php new file mode 100644 index 0000000..b2c77c8 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Exception/InvalidArgument.php @@ -0,0 +1,31 @@ + + */ +final class InvalidArgument extends \RuntimeException implements UserFacingError +{ + #[\Override] + public function translationId(): string + { + return 'shared_kernel.exceptions.invalid_argument'; + } + + #[\Override] + public function translationParameters(): array + { + return []; + } + + #[\Override] + public function translationDomain(): string + { + return 'messages'; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Exception/InvalidEmailAddress.php b/projects/backend/src/SharedKernel/Domain/Exception/InvalidEmailAddress.php new file mode 100644 index 0000000..9320b00 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Exception/InvalidEmailAddress.php @@ -0,0 +1,44 @@ + + */ +final class InvalidEmailAddress extends \InvalidArgumentException implements UserFacingError +{ + public function __construct( + private readonly string $email + ) { + parent::__construct(sprintf('%s : Invalid email address provided', $this->email)); + } + + public static function withValue(string $value): self + { + return new self($value); + } + + #[\Override] + public function translationId(): string + { + return 'shared_kernel.exceptions.invalid_email_address'; + } + + #[\Override] + public function translationParameters(): array + { + return [ + '%email%' => $this->email, + ]; + } + + #[\Override] + public function translationDomain(): string + { + return 'messages'; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Exception/UserFacingError.php b/projects/backend/src/SharedKernel/Domain/Exception/UserFacingError.php new file mode 100644 index 0000000..894108e --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Exception/UserFacingError.php @@ -0,0 +1,19 @@ + + */ +interface UserFacingError extends \Throwable +{ + public function translationId(): string; + + public function translationParameters(): array; + + public function translationDomain(): string; +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Collection/Collection.php b/projects/backend/src/SharedKernel/Domain/Model/Collection/Collection.php new file mode 100644 index 0000000..59eec20 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Collection/Collection.php @@ -0,0 +1,20 @@ + + * + * @author bernard-ng + */ +interface Collection extends DoctrineCollection +{ +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Collection/DataCollection.php b/projects/backend/src/SharedKernel/Domain/Model/Collection/DataCollection.php new file mode 100644 index 0000000..944f792 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Collection/DataCollection.php @@ -0,0 +1,20 @@ + + * + * @author bernard-ng + */ +final class DataCollection extends DoctrineArrayCollection +{ +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Pagination/Page.php b/projects/backend/src/SharedKernel/Domain/Model/Pagination/Page.php new file mode 100644 index 0000000..2ff16c2 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Pagination/Page.php @@ -0,0 +1,48 @@ + + */ +final class Page +{ + public const int DEFAULT_PAGE = 1; + + public const int DEFAULT_LIMIT = 5; + + public const int MAX_LIMIT = 100; + + public function __construct( + public int $page = self::DEFAULT_PAGE, + public int $limit = self::DEFAULT_LIMIT, + public ?string $cursor = null, + public int $offset = 0, + ) { + Assert::greaterThanEq($this->page, self::DEFAULT_PAGE); + Assert::greaterThanEq($this->limit, self::DEFAULT_LIMIT); + Assert::lessThanEq($this->limit, self::MAX_LIMIT); + + $this->offset = intval(($this->page - 1) * $this->limit); + } + + public function next(): self + { + return new self($this->page + 1, $this->limit); + } + + public function previous(): self + { + if ($this->page === self::DEFAULT_PAGE) { + return $this; + } + + return new self($this->page - 1, $this->limit); + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php new file mode 100644 index 0000000..afe74ab --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php @@ -0,0 +1,75 @@ + + */ +final readonly class PaginationCursor +{ + public function __construct( + public UuidV7 $id, + public \DateTimeImmutable $date, + ) { + } + + /** + * Creates a new PaginationCursor from a DateTimeImmutable and a UuidV7. + * @throws \JsonException When JSON encoding fails + */ + public static function encode(array $item, PaginatorKeyset $keyset): string + { + $id = DataMapping::uuid($item, $keyset->id)->toString(); + + if ($keyset->date !== null) { + $date = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s'); + + return base64_encode( + json_encode([ + 'date' => $date, + 'id' => $id, + ], JSON_THROW_ON_ERROR) + ); + } + + return base64_encode( + json_encode([ + 'id' => $id, + ], JSON_THROW_ON_ERROR) + ); + + } + + /** + * Decodes a cursor string into a PaginationCursor object. + * Returns null if the cursor is invalid or cannot be decoded. + */ + public static function decode(?string $cursor): ?self + { + if ($cursor === null) { + return null; + } + + try { + $data = json_decode(base64_decode($cursor), true, 512, JSON_THROW_ON_ERROR); + + if (! is_array($data) || ! isset($data['date'], $data['id'])) { + throw new \InvalidArgumentException('Invalid cursor format'); + } + + return new self( + id: UuidV7::fromString($data['id']), + date: new \DateTimeImmutable($data['date']) + ); + } catch (\Throwable) { + return null; + } + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationInfo.php b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationInfo.php new file mode 100644 index 0000000..b0f0e71 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationInfo.php @@ -0,0 +1,26 @@ + + */ +final class PaginationInfo +{ + public function __construct( + public readonly int $current, + public readonly int $limit, + public ?string $cursor = null, + public bool $hasNext = false, + ) { + } + + public static function from(Page $page): self + { + return new self($page->page, $page->limit); + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginatorKeyset.php b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginatorKeyset.php new file mode 100644 index 0000000..47d588d --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginatorKeyset.php @@ -0,0 +1,23 @@ + + */ +final readonly class PaginatorKeyset +{ + /** + * @param non-empty-string $id + * @param non-empty-string|null $date + */ + public function __construct( + public string $id, + public ?string $date = null, + ) { + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/DateRange.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/DateRange.php new file mode 100644 index 0000000..5e13463 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/DateRange.php @@ -0,0 +1,80 @@ + + */ +final class DateRange implements \Stringable +{ + private function __construct( + public int $start, + public int $end + ) { + Assert::notEq($this->start, 0); + Assert::notEq($this->end, 0); + Assert::greaterThanEq($end, $start); + } + + #[\Override] + public function __toString(): string + { + return sprintf('%d:%d', $this->start, $this->end); + } + + public static function from(string $interval, string $format = 'Y-m-d', string $separator = ':'): self + { + if ($separator === '') { + throw new \InvalidArgumentException('Separator cannot be empty'); + } + + [$startDate, $endDate] = explode($separator, $interval); + + /** @var DateTime $start */ + $start = DateTime::createFromFormat($format, $startDate); + + /** @var DateTime $end */ + $end = DateTime::createFromFormat($format, $endDate); + + return new self((int) $start->format('U'), (int) $end->format('U')); + } + + public static function backward(\DateTimeImmutable $date = new \DateTimeImmutable(), ?int $days = null): self + { + $days ??= 30; + $start = $date->modify(sprintf('-%d days', $days)); + $end = $date->modify('+1 days'); + + return new self((int) $start->format('U'), (int) $end->format('U')); + } + + public static function forward(\DateTimeImmutable $date): self + { + $start = $date; + $end = new \DateTimeImmutable('now')->modify('+1 days'); + + return new self((int) $start->format('U'), (int) $end->format('U')); + } + + public function format(string $format = 'Y-m-d'): string + { + return sprintf('%s:%s', date($format, $this->start), date($format, $this->end)); + } + + public function inRange(int $timestamp): bool + { + return $timestamp >= $this->start && $timestamp <= $this->end; + } + + public function outRange(int $timestamp): bool + { + return $timestamp < $this->start || $timestamp > $this->end; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/EmailAddress.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/EmailAddress.php new file mode 100644 index 0000000..bb6f1e3 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/EmailAddress.php @@ -0,0 +1,54 @@ + + */ +final readonly class EmailAddress implements \Stringable, \JsonSerializable +{ + public string $value; + + public function __construct(string $value) + { + try { + Assert::notEmpty($value); + Assert::email($value); + } catch (\Throwable) { + throw InvalidEmailAddress::withValue($value); + } + + $this->value = $value; + } + + #[\Override] + public function __toString(): string + { + return $this->value; + } + + /** + * @throws InvalidEmailAddress + */ + public static function from(string $value): self + { + return new self($value); + } + + public function provider(): string + { + return substr($this->value, strpos($this->value, '@') + 1); + } + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/SortDirection.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/SortDirection.php new file mode 100644 index 0000000..a635462 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/SortDirection.php @@ -0,0 +1,24 @@ + + */ +enum SortDirection: string +{ + case ASC = 'asc'; + case DESC = 'desc'; + + public function opposite(): self + { + return match ($this) { + self::ASC => self::DESC, + self::DESC => self::ASC, + }; + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/ClientProfile.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/ClientProfile.php new file mode 100644 index 0000000..9eb55d9 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/ClientProfile.php @@ -0,0 +1,20 @@ + + */ +final readonly class ClientProfile +{ + public function __construct( + #[\SensitiveParameter] public ?string $userIp = null, + public ?string $userAgent = null, + public array $hints = [] + ) { + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/Device.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/Device.php new file mode 100644 index 0000000..39a4b3b --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/Device.php @@ -0,0 +1,26 @@ + + */ +final readonly class Device +{ + public function __construct( + public ?string $operatingSystem = null, + public ?string $client = null, + public ?string $device = null, + public bool $isBot = false, + ) { + } + + public static function empty(): self + { + return new self(); + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/GeoLocation.php b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/GeoLocation.php new file mode 100644 index 0000000..468bf44 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Model/ValueObject/Tracking/GeoLocation.php @@ -0,0 +1,49 @@ + + */ +final readonly class GeoLocation +{ + public function __construct( + public ?string $country = null, + public ?string $city = null, + public ?string $timeZone = null, + public ?float $longitude = null, + public ?float $latitude = null, + public ?int $accuracyRadius = null, + ) { + } + + public static function from(array $data): self + { + Assert::keyExists($data, 'country'); + Assert::keyExists($data, 'city'); + Assert::keyExists($data, 'time_zone'); + Assert::keyExists($data, 'longitude'); + Assert::keyExists($data, 'latitude'); + Assert::keyExists($data, 'accuracy_radius'); + + return new self( + country: $data['country'] ?? null, + city: $data['city'] ?? null, + timeZone: $data['time_zone'] ?? null, + longitude: $data['longitude'] ?? null, + latitude: $data['latitude'] ?? null, + accuracyRadius: $data['accuracy_radius'] ?? null, + ); + } + + public static function empty(): self + { + return new self(); + } +} diff --git a/projects/backend/src/SharedKernel/Domain/Tracking/ClientProfiler.php b/projects/backend/src/SharedKernel/Domain/Tracking/ClientProfiler.php new file mode 100644 index 0000000..c17ec73 --- /dev/null +++ b/projects/backend/src/SharedKernel/Domain/Tracking/ClientProfiler.php @@ -0,0 +1,23 @@ + + */ +interface ClientProfiler +{ + public function detect(ClientProfile $profile): Device; + + public function locate(ClientProfile $profile): GeoLocation; + + public function locateCountry(ClientProfile $profile): ?string; +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/EventDispatcher/SymfonyEventDispatcher.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/EventDispatcher/SymfonyEventDispatcher.php new file mode 100644 index 0000000..d1d8460 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/EventDispatcher/SymfonyEventDispatcher.php @@ -0,0 +1,33 @@ + + */ +final readonly class SymfonyEventDispatcher implements EventDispatcher +{ + public function __construct( + private EventDispatcherInterface $eventDispatcher + ) { + } + + /** + * @param array $events + */ + #[\Override] + public function dispatch(array $events): void + { + foreach ($events as $event) { + $this->eventDispatcher->dispatch($event, $event::class); + } + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Kernel.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Kernel.php new file mode 100644 index 0000000..f2b6873 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Kernel.php @@ -0,0 +1,18 @@ + + */ +class Kernel extends BaseKernel +{ + use MicroKernelTrait; +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/NormalizerFormatter.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/NormalizerFormatter.php new file mode 100644 index 0000000..a0ecf78 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/NormalizerFormatter.php @@ -0,0 +1,64 @@ + + */ +class NormalizerFormatter extends MonologNormalizerFormatter implements FormatterInterface +{ + public const string SIMPLE_DATE = 'Y-m-d\TH:i:sP'; + + protected string $dateFormat; + + protected int $maxNormalizeDepth = 9; + + protected int $maxNormalizeItemCount = 1000; + + protected string $basePath = ''; + + private int $jsonEncodeOptions = JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT; + + /** + * Setting a base path will hide the base path from exception and stack trace file names to shorten them + * + * @return $this + */ + #[\Override] + public function setBasePath(string $path = ''): self + { + if ($path !== '') { + $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } + + $this->basePath = $path; + + return $this; + } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @return string if encoding fails and ignoreErrors is true 'null' is returned + * @throws \RuntimeException if encoding fails and errors are not ignored + */ + #[\Override] + protected function toJson($data, bool $ignoreErrors = false): string + { + $json = Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors); + return <<< JSON +```json +{$json} +``` +JSON; + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/TelegramFormatter.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/TelegramFormatter.php new file mode 100644 index 0000000..a8ce365 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Logging/TelegramFormatter.php @@ -0,0 +1,198 @@ + + */ +class TelegramFormatter extends NormalizerFormatter +{ + public const int BATCH_MODE_JSON = 1; + + public const int BATCH_MODE_NEWLINES = 2; + + /** + * @param self::BATCH_MODE_* $batchMode + * + * @throws \RuntimeException If the function json_encode does not exist + */ + public function __construct( + protected int $batchMode = self::BATCH_MODE_JSON, + protected bool $appendNewline = true, + protected bool $ignoreEmptyContextAndExtra = false, + protected bool $includeStacktraces = false, + string $basePath = '' + ) { + $this->basePath = $basePath; + + parent::__construct(); + } + + /** + * The batch mode option configures the formatting style for + * multiple records. By default, multiple records will be + * formatted as a JSON-encoded array. However, for + * compatibility with some API endpoints, alternative styles + * are available. + */ + public function getBatchMode(): int + { + return $this->batchMode; + } + + /** + * True if newlines are appended to every formatted record + */ + public function isAppendingNewlines(): bool + { + return $this->appendNewline; + } + + #[\Override] + public function format(LogRecord $record): string + { + /** @var array $normalized */ + $normalized = parent::format($record); + + if (isset($normalized['context']) && $normalized['context'] === []) { + unset($normalized['context']); + } + + if (isset($normalized['extra']) && $normalized['extra'] === []) { + unset($normalized['extra']); + } + + return $this->toJson($normalized, true) . "\n\n"; + } + + #[\Override] + public function formatBatch(array $records): string + { + return match ($this->batchMode) { + static::BATCH_MODE_NEWLINES => $this->formatBatchNewlines($records), + default => $this->formatBatchJson($records), + }; + } + + /** + * @return $this + */ + public function includeStacktraces(bool $include = true): self + { + $this->includeStacktraces = $include; + + return $this; + } + + /** + * Return a JSON-encoded array of records. + * + * @phpstan-param LogRecord[] $records + */ + protected function formatBatchJson(array $records): string + { + return $this->toJson($this->normalize($records), true); + } + + /** + * Use new lines to separate records instead of a + * JSON-encoded array. + * + * @phpstan-param LogRecord[] $records + */ + protected function formatBatchNewlines(array $records): string + { + $oldNewline = $this->appendNewline; + $this->appendNewline = false; + $formatted = array_map(fn (LogRecord $record): string => $this->format($record), $records); + $this->appendNewline = $oldNewline; + + return implode("\n\n", $formatted); + } + + /** + * Normalizes given $data. + * + * @return array|bool|float|int|object|string|null + */ + #[\Override] + protected function normalize(mixed $data, int $depth = 0): array|bool|float|int|object|string|null + { + if ($depth > $this->maxNormalizeDepth) { + return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; + } + + if (\is_array($data)) { + $normalized = []; + + $count = 1; + foreach ($data as $key => $value) { + if ($count++ > $this->maxNormalizeItemCount) { + $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items (' . \count($data) . ' total), aborting normalization'; + break; + } + + $normalized[$key] = $this->normalize($value, $depth + 1); + } + + return $normalized; + } + + if (\is_object($data)) { + if ($data instanceof \DateTimeInterface) { + return $this->formatDate($data); + } + + if ($data instanceof \Throwable) { + /** @var array|float|object|bool|int|string|null $throwable */ + $throwable = $this->normalizeException($data, $depth); + return $throwable; + } + + // if the object has specific json serializability we want to make sure we skip the __toString treatment below + if ($data instanceof \JsonSerializable) { + return $data; + } + + if ($data instanceof \Stringable) { + return $data->__toString(); + } + + if ($data::class === '__PHP_Incomplete_Class') { + return new \ArrayObject($data); + } + + return $data; + } + + if (\is_resource($data)) { + return parent::normalize($data); + } + + /** @var array|float|object|bool|int|string|null $data */ + return $data; + } + + /** + * Normalizes given exception with or without its own stack trace based on + * `includeStacktraces` property. + * + * @inheritDoc + */ + #[\Override] + protected function normalizeException(\Throwable $e, int $depth = 0): array|float|object|bool|int|string|null + { + $data = parent::normalizeException($e, $depth); + if (! $this->includeStacktraces) { + unset($data['trace']); + } + + return $data; + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Mailing/SymfonyMailer.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Mailing/SymfonyMailer.php new file mode 100644 index 0000000..0539cff --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Mailing/SymfonyMailer.php @@ -0,0 +1,69 @@ + + */ +final readonly class SymfonyMailer implements Mailer +{ + public function __construct( + private MailerInterface $mailer, + private TranslatorInterface $translator, + private Application $application = new Application() + ) { + } + + /** + * @throws TransportExceptionInterface + */ + #[\Override] + public function send(EmailDefinition $email): void + { + $sender = new Address( + $this->application->emailAddress, + $this->application->emailName + ); + + $htmlTemplate = sprintf('emails/%s.html.twig', $email->template()); + $txtTemplate = sprintf('emails/%s.txt.twig', $email->template()); + + $message = new TemplatedEmail() + ->from($sender) + ->to($email->recipient()->value) + ->subject( + $this->translator->trans( + $email->subject(), + $email->subjectVariables(), + $email->getDomain(), + $email->locale() + ) + ) + ->htmlTemplate($htmlTemplate) + ->textTemplate($txtTemplate) + ->context(array_merge( + $email->templateVariables(), + [ + 'application' => new Application(), + 'locale' => $email->locale(), + 'domain' => $email->getDomain(), + ] + )) + ; + + $this->mailer->send($message); + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerCommandBus.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerCommandBus.php new file mode 100644 index 0000000..3d058ca --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerCommandBus.php @@ -0,0 +1,45 @@ + + */ +final class MessengerCommandBus implements CommandBus +{ + use HandleTrait { + HandleTrait::handle as messengerHandle; + } + + public function __construct(MessageBusInterface $commandBus) + { + $this->messageBus = $commandBus; + } + + /** + * @throws \Throwable + */ + #[\Override] + public function handle(object $command): mixed + { + try { + return $this->messengerHandle($command); + } catch (HandlerFailedException $e) { + while ($e instanceof HandlerFailedException) { + /** @var \Throwable $e */ + $e = $e->getPrevious(); + } + + throw $e; + } + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerMessageBus.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerMessageBus.php new file mode 100644 index 0000000..5ff2272 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerMessageBus.php @@ -0,0 +1,32 @@ + + */ +final readonly class MessengerMessageBus implements MessageBus +{ + public function __construct( + private MessageBusInterface $messageBus + ) { + } + + /** + * @throws ExceptionInterface + */ + #[\Override] + public function dispatch(AsyncMessage $message): void + { + $this->messageBus->dispatch($message); + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerQueryBus.php b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerQueryBus.php new file mode 100644 index 0000000..82b05ce --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Framework/Symfony/Messaging/MessengerQueryBus.php @@ -0,0 +1,45 @@ + + */ +final class MessengerQueryBus implements QueryBus +{ + use HandleTrait { + HandleTrait::handle as messengerHandle; + } + + public function __construct(MessageBusInterface $queryBus) + { + $this->messageBus = $queryBus; + } + + /** + * @throws \Throwable + */ + #[\Override] + public function handle(object $message): mixed + { + try { + return $this->messengerHandle($message); + } catch (HandlerFailedException $e) { + while ($e instanceof HandlerFailedException) { + /** @var \Throwable $e */ + $e = $e->getPrevious(); + } + + throw $e; + } + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php new file mode 100644 index 0000000..1d3ba05 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php @@ -0,0 +1,62 @@ + + */ +trait PaginationQuery +{ + public function createPaginationInfo(array $data, Page $page, PaginatorKeyset $keyset): PaginationInfo + { + $paginationInfo = PaginationInfo::from($page); + if ($data === []) { + return $paginationInfo; + } + + $paginationInfo->cursor = PaginationCursor::encode(array_pop($data), $keyset); + $paginationInfo->hasNext = count($data) > $page->limit; + + return $paginationInfo; + } + + public function applyCursorPagination(QueryBuilder $qb, Page $page, PaginatorKeyset $keyset): QueryBuilder + { + $cursor = PaginationCursor::decode($page->cursor); + if (! $cursor instanceof PaginationCursor) { + return $this->applyOffsetPagination($qb, $page); + } + + if ($keyset->date === null) { + $qb + ->andWhere(sprintf('%s <= :cursorLastId', $keyset->id)) + ->setParameter('cursorLastId', $cursor->id->toString(), ParameterType::BINARY); + } else { + $qb + ->andWhere(sprintf('(%s, %s) <= (:cursorLastDate, :cursorLastId)', $keyset->date, $keyset->id)) + ->setParameter('cursorLastDate', $cursor->id->toBinary(), ParameterType::BINARY) + ->setParameter('cursorLastId', $cursor->date->format('Y-m-d H:i:s')); + } + + return $qb->setMaxResults($page->limit + 1); + } + + public function applyOffsetPagination(QueryBuilder $qb, Page $page): QueryBuilder + { + return $qb + ->setFirstResult($page->offset) + ->setMaxResults($page->limit) + ; + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/NoResult.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/NoResult.php new file mode 100644 index 0000000..354623f --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/NoResult.php @@ -0,0 +1,21 @@ + + */ +final class NoResult extends \RuntimeException +{ + public static function forQuery(string $query, array $parameters, ?\Throwable $previous = null): self + { + return new self( + sprintf('%s - Query "%s" (parameters: %s) produced no results', $previous?->getMessage(), $query, json_encode($parameters)), + previous: $previous + ); + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Types/EmailType.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Types/EmailType.php new file mode 100644 index 0000000..aa4eb09 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Types/EmailType.php @@ -0,0 +1,74 @@ + + */ +final class EmailType extends Type +{ + public const string NAME = 'email'; + + #[\Override] + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $platform->getStringTypeDeclarationSQL([ + 'length' => 255, + ]); + } + + #[\Override] + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?EmailAddress + { + if ($value === null) { + return null; + } + + if (! \is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', EmailAddress::class]); + } + + try { + return EmailAddress::from($value); + } catch (\Throwable $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + #[\Override] + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if ($value instanceof EmailAddress) { + return (string) $value; + } + + if ($value === null || $value === '') { + return null; + } + + if (! \is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', EmailAddress::class]); + } + + try { + return (string) EmailAddress::from($value); + } catch (\Throwable $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + #[\Override] + public function getName(): string + { + return self::NAME; + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/Asset/AssetUrlProvider.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/Asset/AssetUrlProvider.php new file mode 100644 index 0000000..2d10ce0 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/Asset/AssetUrlProvider.php @@ -0,0 +1,40 @@ + + */ +final class AssetUrlProvider implements AssetUrlProviderInterface +{ + public function __construct( + #[Autowire(env: 'SERVER_ADDR')] public ?string $serverAddress = null, + #[Autowire(env: 'SERVER_PORT')] public ?string $serverPort = null, + #[Autowire(env: 'APP_ENV')] public string $env = 'dev' + ) { + $this->serverAddress ??= $_SERVER['SERVER_ADDR'] ?? null; + $this->serverPort ??= $_SERVER['SERVER_PORT'] ?? null; + } + + public function getUrl(string $id, AssetType $type): ?string + { + if ($this->serverAddress === null) { + return null; + } + + $path = match ($type) { + AssetType::SOURCE_PROFILE_IMAGE => sprintf('/images/sources/%s.png', $id), + }; + + $scheme = $this->env === 'prod' ? 'https' : 'http'; + return sprintf('%s://%s:%s/%s', $scheme, $this->serverAddress, $this->serverPort, $path); + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/DataTransfert.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/DataTransfert.php new file mode 100644 index 0000000..46f9e40 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Filesystem/DataTransfert.php @@ -0,0 +1,51 @@ + + */ +final readonly class DataTransfert implements DataImporter, DataExporter +{ + private const array SUPPORTED_FORMATS = ['csv']; + + public function __construct( + private SerializerInterface $serializer, + private Filesystem $filesystem, + ) { + } + + #[\Override] + public function export(iterable $data, TransfertSetting $setting = new TransfertSetting()): \SplFileObject + { + Assert::oneOf($setting->format, self::SUPPORTED_FORMATS); + + $data = $this->serializer->serialize($data, $setting->format); + $filename = $setting->filename ?? $this->filesystem->tempnam(sys_get_temp_dir(), 'export'); + $this->filesystem->dumpFile($filename, $data); + + return new \SplFileObject($filename); + } + + #[\Override] + public function import(\SplFileObject $file, TransfertSetting $setting = new TransfertSetting()): iterable + { + Assert::notNull($setting->type); + Assert::oneOf($setting->format, self::SUPPORTED_FORMATS); + + $data = $this->filesystem->readFile($file->getPathname()); + + return $this->serializer->deserialize($data, $setting->type, $setting->format); + } +} diff --git a/projects/backend/src/SharedKernel/Infrastructure/Tracking/ClientProfiler.php b/projects/backend/src/SharedKernel/Infrastructure/Tracking/ClientProfiler.php new file mode 100644 index 0000000..cd2e012 --- /dev/null +++ b/projects/backend/src/SharedKernel/Infrastructure/Tracking/ClientProfiler.php @@ -0,0 +1,134 @@ + + */ +final readonly class ClientProfiler implements ClientProfilerInterface +{ + private const string GEOIP_CITY_DATABASE = 'geoip_city.mmdb'; + + private const string GEOIP_COUNTRY_DATABASE = 'geoip_country.mmdb'; + + public function __construct( + private string $projectDir, + private LoggerInterface $logger + ) { + } + + #[\Override] + public function locate(ClientProfile $profile): GeoLocation + { + if ($this->shouldSkipIpLocalization($profile)) { + return GeoLocation::empty(); + } + + try { + $database = sprintf('%s/%s', $this->projectDir, self::GEOIP_CITY_DATABASE); + + Assert::notNull($profile->userIp); + $data = new Reader($database)->city($profile->userIp); + + return GeoLocation::from([ + 'country' => $data->country->isoCode, + 'city' => $data->city->name, + 'time_zone' => $data->location->timeZone, + 'longitude' => $data->location->longitude, + 'latitude' => $data->location->latitude, + 'accuracy_radius' => $data->location->accuracyRadius, + ]); + } catch (\Throwable $e) { + $this->logger->error('Unable to fetch location from IP address', [ + 'ip' => $profile->userIp, + 'exception' => $e, + ]); + + return GeoLocation::empty(); + } + } + + #[\Override] + public function locateCountry(ClientProfile $profile): ?string + { + if ($this->shouldSkipIpLocalization($profile)) { + return null; + } + + try { + $database = sprintf('%s/%s', $this->projectDir, self::GEOIP_COUNTRY_DATABASE); + + Assert::notNull($profile->userIp); + $data = new Reader($database)->country($profile->userIp); + + /** @var string|null $country */ + $country = $data->country->isoCode; + + return $country; + } catch (\Throwable $e) { + $this->logger->error('Unable to fetch country from IP address', [ + 'ip' => $profile->userIp, + 'exception' => $e, + ]); + + return null; + } + } + + #[\Override] + public function detect(ClientProfile $profile): Device + { + if ($profile->userAgent === null || $profile->hints === []) { + return Device::empty(); + } + + try { + $detector = new DeviceDetector($profile->userAgent, ClientHints::factory($profile->hints)); + $detector->parse(); + + $osLabel = is_string($detector->getOs('name')) ? $detector->getOs('name') : ''; + $clientLabel = is_string($detector->getClient('name')) ? $detector->getClient('name') : ''; + + return new Device( + operatingSystem: OperatingSystem::getOsFamily($osLabel), + client: match (true) { + $detector->isBrowser() => Browser::getBrowserFamily($clientLabel), + default => $clientLabel + }, + device: $detector->getDeviceName(), + isBot: $detector->isBot(), + ); + } catch (\Throwable $e) { + $this->logger->error('Unable to detect device', [ + 'user_agent' => $profile->userAgent, + 'exception' => $e, + ]); + + return Device::empty(); + } + } + + private function shouldSkipIpLocalization(ClientProfile $profile): bool + { + return \filter_var($profile->userIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) + || $profile->userIp === null + || IpUtils::isPrivateIp($profile->userIp); + } +} diff --git a/projects/backend/src/SharedKernel/Presentation/Console/AskArgumentFeature.php b/projects/backend/src/SharedKernel/Presentation/Console/AskArgumentFeature.php new file mode 100644 index 0000000..6441d46 --- /dev/null +++ b/projects/backend/src/SharedKernel/Presentation/Console/AskArgumentFeature.php @@ -0,0 +1,40 @@ + + */ +trait AskArgumentFeature +{ + private function askArgument(InputInterface $input, string $name, bool $hidden = false): void + { + $value = \strval($input->getArgument($name)); + if ($value !== '') { + $this->io->text(\sprintf(' > %s: %s', $name, $value)); + } else { + $value = match ($hidden) { + false => $this->io->ask(\strtoupper($name)), + default => $this->io->askHidden(\strtoupper($name)) + }; + $input->setArgument($name, $value); + } + } + + private function askOption(InputInterface $input, string $name): void + { + $value = \strval($input->getOption($name)); + if ($value !== '') { + $this->io->text(\sprintf(' > %s: %s', $name, $value)); + } else { + $value = $this->io->ask(\strtoupper($name)); + $input->setOption($name, $value); + } + } +} diff --git a/projects/backend/src/SharedKernel/Presentation/Web/Controller/AbstractController.php b/projects/backend/src/SharedKernel/Presentation/Web/Controller/AbstractController.php new file mode 100644 index 0000000..9332826 --- /dev/null +++ b/projects/backend/src/SharedKernel/Presentation/Web/Controller/AbstractController.php @@ -0,0 +1,91 @@ + + */ +abstract class AbstractController extends SymfonyController +{ + protected ?Response $response = null; + + #[\Override] + public static function getSubscribedServices(): array + { + $subscribedServices = parent::getSubscribedServices(); + + $subscribedServices[] = CommandBus::class; + $subscribedServices[] = QueryBus::class; + $subscribedServices[] = TranslatorInterface::class; + $subscribedServices[] = LoggerInterface::class; + $subscribedServices[] = SerializerInterface::class; + + return $subscribedServices; + } + + public function getSecurityUser(): SecurityUser + { + /** @var SecurityUser|null $user */ + $user = $this->getUser(); + + if ($user === null) { + throw $this->createAccessDeniedException( + 'You must be authenticated to access this resource.' + ); + } + + return $user; + } + + public function serialize(mixed $data, string $format = 'json', array $context = []): string + { + /** @var SerializerInterface $serializer */ + $serializer = $this->container->get(SerializerInterface::class); + return $serializer->serialize($data, $format, $context); + } + + #[\Override] + protected function render(string $view, array $parameters = [], ?Response $response = null): Response + { + return parent::render($view, $parameters, $response ?? $this->response); + } + + protected function handleCommand(object $command): mixed + { + /** @var CommandBus $commandBus */ + $commandBus = $this->container->get(CommandBus::class); + return $commandBus->handle($command); + } + + protected function handleQuery(object $query): mixed + { + /** @var QueryBus $queryBus */ + $queryBus = $this->container->get(QueryBus::class); + return $queryBus->handle($query); + } + + protected function trans(string $key, array $params = [], string $domain = 'messages'): string + { + /** @var TranslatorInterface $trans */ + $trans = $this->container->get(TranslatorInterface::class); + return $trans->trans($key, $params, $domain); + } + + protected function setStatus(int $status): void + { + $this->response = new Response(status: $status); + } +} diff --git a/projects/backend/src/SharedKernel/Presentation/Web/Controller/DefaultController.php b/projects/backend/src/SharedKernel/Presentation/Web/Controller/DefaultController.php new file mode 100644 index 0000000..c191fa1 --- /dev/null +++ b/projects/backend/src/SharedKernel/Presentation/Web/Controller/DefaultController.php @@ -0,0 +1,31 @@ + + */ +final class DefaultController extends AbstractController +{ + #[Route( + path: '', + name: 'default', + methods: ['GET'] + )] + public function __invoke(): JsonResponse + { + return $this->json([ + 'repository' => 'https://github.com/bernard-ng/drc-news-corpus', + 'title' => 'DRC News Corpus : Towards a scalable and intelligent system for Congolese News curation', + 'description' => 'The DRC News Corpus is a structured and scalable dataset of news articles sourced from major media outlets covering diverse aspects of the Democratic Republic of Congo (DRC). Designed for efficiency, this system enables the automated collection, processing, and organization of news stories spanning politics, economy, society, culture, environment, and international affairs.', + 'status' => 200, + ]); + } +} diff --git a/projects/backend/src/SharedKernel/Presentation/Web/EventListener/UserFacingErrorListener.php b/projects/backend/src/SharedKernel/Presentation/Web/EventListener/UserFacingErrorListener.php new file mode 100644 index 0000000..3792610 --- /dev/null +++ b/projects/backend/src/SharedKernel/Presentation/Web/EventListener/UserFacingErrorListener.php @@ -0,0 +1,86 @@ + + */ +#[AsEventListener(KernelEvents::EXCEPTION, priority: -1)] +final readonly class UserFacingErrorListener +{ + private const array NOT_FOUND_EXCEPTIONS = [ + ArticleNotFound::class, + SourceNotFound::class, + UserNotFound::class, + CommentNotFound::class, + BookmarkNotFound::class, + FollowedSourceNotFound::class, + BookmarkedArticleNotFound::class, + ]; + + private const array BAD_REQUEST_EXCEPTIONS = [ + InvalidArgument::class, + ]; + + private const array FORBIDDEN_EXCEPTIONS = [ + PermissionNotGranted::class, + ]; + + public function __construct( + private TranslatorInterface $translator + ) { + } + + public function __invoke(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + if ($exception instanceof UserFacingError) { + $message = $this->translator->trans( + $exception->translationId(), + $exception->translationParameters(), + $exception->translationDomain(), + ); + + $status = $this->getResponseStatus($exception); + $response = new JsonResponse([ + 'type' => 'https://symfony.com/errors/domain', + 'title' => $exception->translationId(), + 'detail' => $message, + 'status' => $status, + ], $status); + + $event->setResponse($response); + } + } + + public function getResponseStatus(UserFacingError $exception): int + { + return match (true) { + in_array($exception::class, self::NOT_FOUND_EXCEPTIONS) => Response::HTTP_NOT_FOUND, + in_array($exception::class, self::BAD_REQUEST_EXCEPTIONS) => Response::HTTP_BAD_REQUEST, + in_array($exception::class, self::FORBIDDEN_EXCEPTIONS) => Response::HTTP_FORBIDDEN, + default => Response::HTTP_UNPROCESSABLE_ENTITY + }; + } +} diff --git a/projects/backend/symfony.lock b/projects/backend/symfony.lock new file mode 100644 index 0000000..aa0e913 --- /dev/null +++ b/projects/backend/symfony.lock @@ -0,0 +1,294 @@ +{ + "doctrine/doctrine-bundle": { + "version": "2.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.12", + "ref": "7266981c201efbbe02ae53c87f8bb378e3f825ae" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "4.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, + "friends-of-behat/symfony-extension": { + "version": "2.6", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "1e012e04f573524ca83795cd19df9ea690adb604" + } + }, + "gesdinet/jwt-refresh-token-bundle": { + "version": "1.4", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "2390b4ed5c195e0b3f6dea45221f3b7c0af523a0" + }, + "files": [ + "config/packages/gesdinet_jwt_refresh_token.yaml", + "config/routes/gesdinet_jwt_refresh_token.yaml", + "src/Entity/RefreshToken.php" + ] + }, + "knplabs/knp-paginator-bundle": { + "version": "v6.7.0" + }, + "lexik/jwt-authentication-bundle": { + "version": "3.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, + "phpstan/phpstan": { + "version": "1.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + }, + "files": [ + "phpstan.dist.neon" + ] + }, + "phpunit/phpunit": { + "version": "10.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.6", + "ref": "7364a21d87e658eb363c5020c072ecfdc12e2326" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "sentry/sentry-symfony": { + "version": "5.2", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "5.0", + "ref": "f26c577142172082bb3aeef519af6b5944b86ac2" + }, + "files": [ + "config/packages/sentry.yaml" + ] + }, + "symfony/console": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/mailer": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "2bf89438209656b85b9a49238c4467bff1b1f939" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, + "symfony/maker-bundle": { + "version": "1.51", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/messenger": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, + "symfony/routing": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, + "symfony/translation": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.3", + "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, + "symfony/twig-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/uid": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" + } + }, + "symfony/validator": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, + "twig/extra-bundle": { + "version": "v3.13.0" + } +} diff --git a/projects/backend/templates/base.html.twig b/projects/backend/templates/base.html.twig new file mode 100644 index 0000000..1069c14 --- /dev/null +++ b/projects/backend/templates/base.html.twig @@ -0,0 +1,16 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {% endblock %} + + {% block javascripts %} + {% endblock %} + + + {% block body %}{% endblock %} + + diff --git a/projects/backend/templates/emails/aggregator/source_crawled.html.twig b/projects/backend/templates/emails/aggregator/source_crawled.html.twig new file mode 100644 index 0000000..e57ecd8 --- /dev/null +++ b/projects/backend/templates/emails/aggregator/source_crawled.html.twig @@ -0,0 +1,5 @@ +Hello There,

+ +The source {{ source }} has been fetched and is now available for download.

+ +performance : {{ event }} diff --git a/projects/backend/templates/emails/aggregator/source_crawled.txt.twig b/projects/backend/templates/emails/aggregator/source_crawled.txt.twig new file mode 100644 index 0000000..9b7b4ad --- /dev/null +++ b/projects/backend/templates/emails/aggregator/source_crawled.txt.twig @@ -0,0 +1,5 @@ +Hello There, + +The source {{ source }} has been fetched and is now available for download. + +performance : {{ event }} diff --git a/projects/backend/templates/emails/identity_and_access/account_confirmed.html.twig b/projects/backend/templates/emails/identity_and_access/account_confirmed.html.twig new file mode 100644 index 0000000..e268e62 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_confirmed.html.twig @@ -0,0 +1,2 @@ +Votre compte a été confirmé avec succès. +Bienvenue sur {{ application.name }}. diff --git a/projects/backend/templates/emails/identity_and_access/account_confirmed.txt.twig b/projects/backend/templates/emails/identity_and_access/account_confirmed.txt.twig new file mode 100644 index 0000000..e268e62 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_confirmed.txt.twig @@ -0,0 +1,2 @@ +Votre compte a été confirmé avec succès. +Bienvenue sur {{ application.name }}. diff --git a/projects/backend/templates/emails/identity_and_access/account_locked.html.twig b/projects/backend/templates/emails/identity_and_access/account_locked.html.twig new file mode 100644 index 0000000..18b95d4 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_locked.html.twig @@ -0,0 +1,5 @@ +Après plusieurs tentatives infructueuses de connexion, votre compte a été bloqué pour des raisons de sécurité.

+ +Utilisez le lien ci-dessous pour réinitialiser votre mot de passe et débloquer votre compte.

+ +{{ url('identity_and_access_unlock_account', {'token': token}) }} diff --git a/projects/backend/templates/emails/identity_and_access/account_locked.txt.twig b/projects/backend/templates/emails/identity_and_access/account_locked.txt.twig new file mode 100644 index 0000000..45eb472 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_locked.txt.twig @@ -0,0 +1,5 @@ +Après plusieurs tentatives infructueuses de connexion, votre compte a été bloqué pour des raisons de sécurité. + +Utilisez le lien ci-dessous pour réinitialiser votre mot de passe et débloquer votre compte. + +{{ url('identity_and_access_unlock_account', {'token': token}) }} diff --git a/projects/backend/templates/emails/identity_and_access/account_unlocked.html.twig b/projects/backend/templates/emails/identity_and_access/account_unlocked.html.twig new file mode 100644 index 0000000..1425553 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_unlocked.html.twig @@ -0,0 +1 @@ +Votre compte {{ application.name }} a été déverrouillé avec succès. diff --git a/projects/backend/templates/emails/identity_and_access/account_unlocked.txt.twig b/projects/backend/templates/emails/identity_and_access/account_unlocked.txt.twig new file mode 100644 index 0000000..1425553 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/account_unlocked.txt.twig @@ -0,0 +1 @@ +Votre compte {{ application.name }} a été déverrouillé avec succès. diff --git a/projects/backend/templates/emails/identity_and_access/password_forgotten.html.twig b/projects/backend/templates/emails/identity_and_access/password_forgotten.html.twig new file mode 100644 index 0000000..3fe85b4 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_forgotten.html.twig @@ -0,0 +1,4 @@ +Nous avons reçu une demande de réinitialisation de votre mot de passe {{ application.name }}.

+ +Ce lien ne sera plus valide après **2 heure**, +{{ url('identity_and_access_reset_password', {token: token}) }} diff --git a/projects/backend/templates/emails/identity_and_access/password_forgotten.txt.twig b/projects/backend/templates/emails/identity_and_access/password_forgotten.txt.twig new file mode 100644 index 0000000..ec8c819 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_forgotten.txt.twig @@ -0,0 +1,4 @@ +Nous avons reçu une demande de réinitialisation de votre mot de passe {{ application.name }}. + +Ce lien ne sera plus valide après **2 heure**, +{{ url('identity_and_access_reset_password', {token: token}) }} diff --git a/projects/backend/templates/emails/identity_and_access/password_reset.html.twig b/projects/backend/templates/emails/identity_and_access/password_reset.html.twig new file mode 100644 index 0000000..359efb8 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_reset.html.twig @@ -0,0 +1 @@ +Ceci est une confirmation que le mot de passe de votre compte {{ application.name }} vient d'être réinitialisé. diff --git a/projects/backend/templates/emails/identity_and_access/password_reset.txt.twig b/projects/backend/templates/emails/identity_and_access/password_reset.txt.twig new file mode 100644 index 0000000..359efb8 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_reset.txt.twig @@ -0,0 +1 @@ +Ceci est une confirmation que le mot de passe de votre compte {{ application.name }} vient d'être réinitialisé. diff --git a/projects/backend/templates/emails/identity_and_access/password_updated.html.twig b/projects/backend/templates/emails/identity_and_access/password_updated.html.twig new file mode 100644 index 0000000..0a01305 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_updated.html.twig @@ -0,0 +1 @@ +Ceci est une confirmation que le mot de passe de votre compte {{ application.name }} vient d'être mis à jour. diff --git a/projects/backend/templates/emails/identity_and_access/password_updated.txt.twig b/projects/backend/templates/emails/identity_and_access/password_updated.txt.twig new file mode 100644 index 0000000..0a01305 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/password_updated.txt.twig @@ -0,0 +1 @@ +Ceci est une confirmation que le mot de passe de votre compte {{ application.name }} vient d'être mis à jour. diff --git a/projects/backend/templates/emails/identity_and_access/user_registered.html.twig b/projects/backend/templates/emails/identity_and_access/user_registered.html.twig new file mode 100644 index 0000000..a444db6 --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/user_registered.html.twig @@ -0,0 +1,5 @@ +Merci pour l'intérêt que vous portez à {{ application.name }}.

+ +pour finaliser votre inscription, veuillez cliquer sur le lien ci-dessous :

+ +{{ url('identity_and_access_confirm_account', {token: token}) }} diff --git a/projects/backend/templates/emails/identity_and_access/user_registered.txt.twig b/projects/backend/templates/emails/identity_and_access/user_registered.txt.twig new file mode 100644 index 0000000..5f0e0ae --- /dev/null +++ b/projects/backend/templates/emails/identity_and_access/user_registered.txt.twig @@ -0,0 +1,5 @@ +Merci pour l'intérêt que vous portez à {{ application.name }}. + +pour finaliser votre inscription, veuillez cliquer sur le lien ci-dessous : + +{{ url('identity_and_access_confirm_account', {token: token}) }} diff --git a/projects/backend/tests/Behat/Context/AbstractContext.php b/projects/backend/tests/Behat/Context/AbstractContext.php new file mode 100644 index 0000000..ad7a689 --- /dev/null +++ b/projects/backend/tests/Behat/Context/AbstractContext.php @@ -0,0 +1,28 @@ + + */ +final class AbstractContext implements Context, ServiceSubscriberInterface +{ + use ServiceMethodsSubscriberTrait; + + #[\Override] + public static function getSubscribedServices(): array + { + return [CommandBus::class, QueryBus::class, SharedStorage::class]; + } +} diff --git a/projects/backend/tests/Behat/Hook/DatabasePurger.php b/projects/backend/tests/Behat/Hook/DatabasePurger.php new file mode 100644 index 0000000..7dc8800 --- /dev/null +++ b/projects/backend/tests/Behat/Hook/DatabasePurger.php @@ -0,0 +1,31 @@ +entityManager->getConnection() + ->getConfiguration() + ->setMiddlewares([]); + + $purger = new ORMPurger($this->entityManager); + $purger->purge(); + + $this->entityManager->clear(); + } +} diff --git a/projects/backend/tests/Behat/State/SharedStorage.php b/projects/backend/tests/Behat/State/SharedStorage.php new file mode 100644 index 0000000..bc1a209 --- /dev/null +++ b/projects/backend/tests/Behat/State/SharedStorage.php @@ -0,0 +1,37 @@ + + */ +final class SharedStorage +{ + private array $storage = []; + + public function get(string $key): mixed + { + return $this->storage[$key] ?? null; + } + + public function set(string $key, mixed $value): void + { + $this->storage[$key] = $value; + } + + public function has(string $key): bool + { + return isset($this->storage[$key]); + } + + public function remove(string $key): void + { + if ($this->has($key)) { + unset($this->storage[$key]); + } + } +} diff --git a/projects/backend/tests/Unit/Aggregator/Domain/Model/ValueObject/PageRangeTest.php b/projects/backend/tests/Unit/Aggregator/Domain/Model/ValueObject/PageRangeTest.php new file mode 100644 index 0000000..582e7c7 --- /dev/null +++ b/projects/backend/tests/Unit/Aggregator/Domain/Model/ValueObject/PageRangeTest.php @@ -0,0 +1,37 @@ + + */ +final class PageRangeTest extends TestCase +{ + public function testItShouldCreatePageRange(): void + { + $pageRange = PageRange::from('1:10'); + + $this->assertEquals(1, $pageRange->start); + $this->assertEquals(10, $pageRange->end); + } + + public function testEndPageShouldBeGreaterThanStartPage(): void + { + $this->expectException(InvalidArgument::class); + PageRange::from('10:1'); + } + + public function testNonNegativePages(): void + { + $this->expectException(InvalidArgument::class); + PageRange::from('-1:-10'); + } +} diff --git a/projects/backend/tests/Unit/Aggregator/Domain/Service/DateParserTest.php b/projects/backend/tests/Unit/Aggregator/Domain/Service/DateParserTest.php new file mode 100644 index 0000000..dcc04b3 --- /dev/null +++ b/projects/backend/tests/Unit/Aggregator/Domain/Service/DateParserTest.php @@ -0,0 +1,63 @@ + + */ +final class DateParserTest extends TestCase +{ + private DateParser $dateParser; + + #[\Override] + protected function setUp(): void + { + $this->dateParser = new DateParser(); + } + + #[DataProvider('validDateProvider')] + public function testCreateTimeStampWithValidDates( + string $date, + ?string $format, + ?string $pattern, + ?string $replacement, + string $expected + ): void { + $result = $this->dateParser->createTimeStamp($date, $format, $pattern, $replacement); + $this->assertEquals($expected, $result); + } + + #[DataProvider('invalidDateProvider')] + public function testCreateTimeStampWithInvalidDates( + string $date, + ?string $format, + ?string $pattern, + ?string $replacement + ): void { + $currentTimestamp = new \DateTime('midnight')->format('U'); + $result = $this->dateParser->createTimeStamp($date, $format, $pattern, $replacement); + $this->assertEquals($currentTimestamp, $result); + } + + public static function validDateProvider(): \Generator + { + yield ['2004-02-12T15:19:21', 'c', null, null, '1076599161']; + yield ['08/10/2024 - 00:00', 'Y-m-d H:i', '/(\d{2})\/(\d{2})\/(\d{4}) - (\d{2}:\d{2})/', '$3-$2-$1 $4', '1728345600']; + yield ['mar 08/10/2024 - 00:00', 'Y-m-d H:i', '/\w{3} (\d{2})\/(\d{2})\/(\d{4}) - (\d{2}:\d{2})/', '$3-$2-$1 $4', '1728345600']; + yield ['Mardi 8 octobre 2024 - 00:00', 'Y-m-d H:i', '/(\d{1}) (\d{1,2}) (\d{2}) (\d{4}) - (\d{2}:\d{2})/', '$4-$3-$2 $5', '1728345600']; + yield ['8.10.2024 00:00', 'd.m.Y H:i', null, null, '1728345600']; + } + + public static function invalidDateProvider(): \Generator + { + yield ['invalid date string', null, null, null]; + } +} diff --git a/projects/backend/tests/Unit/SharedKernel/Domain/Model/ValueObject/DateRangeTest.php b/projects/backend/tests/Unit/SharedKernel/Domain/Model/ValueObject/DateRangeTest.php new file mode 100644 index 0000000..6905bd8 --- /dev/null +++ b/projects/backend/tests/Unit/SharedKernel/Domain/Model/ValueObject/DateRangeTest.php @@ -0,0 +1,35 @@ + + */ +final class DateRangeTest extends TestCase +{ + public function testItShouldCreateDateRange(): void + { + $dateRange = DateRange::from( + '2021-10-01 00:00:00--2021-10-10 00:00:00', + 'Y-m-d H:i:s', + '--' + ); + + $this->assertEquals(1633046400, $dateRange->start); + $this->assertEquals(1633824000, $dateRange->end); + } + + public function testEndShouldBeGreaterThanStart(): void + { + $this->expectException(InvalidArgument::class); + DateRange::from('2021-10-10:2021-10-01'); + } +} diff --git a/projects/backend/tests/bootstrap.php b/projects/backend/tests/bootstrap.php new file mode 100644 index 0000000..8b7a9de --- /dev/null +++ b/projects/backend/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__) . '/.env'); +} diff --git a/projects/backend/tests/object-manager.php b/projects/backend/tests/object-manager.php new file mode 100644 index 0000000..451d163 --- /dev/null +++ b/projects/backend/tests/object-manager.php @@ -0,0 +1,18 @@ +bootEnv(__DIR__ . '/../.env'); + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); + +/** @var Doctrine\Bundle\DoctrineBundle\Registry $doctrine */ +$doctrine = $kernel->getContainer()->get('doctrine'); + +return $doctrine->getManager(); diff --git a/projects/backend/translations/aggregator.fr.yaml b/projects/backend/translations/aggregator.fr.yaml new file mode 100644 index 0000000..cf37989 --- /dev/null +++ b/projects/backend/translations/aggregator.fr.yaml @@ -0,0 +1,8 @@ +# Emails +aggregator.emails.source_crawled.subject: "[DRC News] source crawled successfully" + +# Exceptions +aggregator.exceptions.article_not_found: Désolé nous n'avons pas trouvé cet article dans nos enregistrements +aggregator.exceptions.duplicated_article: Cet article a déjà été enregistré +aggregator.exceptions.article_out_of_range: Article en déhors de l'intervalle de dates d'enregistrement +aggregator.exceptions.source_not_found: Désolé impossible d'obtenir des informations sur cette source diff --git a/projects/backend/translations/feed_management.fr.yaml b/projects/backend/translations/feed_management.fr.yaml new file mode 100644 index 0000000..35476f7 --- /dev/null +++ b/projects/backend/translations/feed_management.fr.yaml @@ -0,0 +1,12 @@ + +# Exception +feed_management.exceptions.article_already_bookmarked: L'article a déjà été ajouté au signet +feed_management.exceptions.bookmarked_article_not_found: L'article n'a pas été trouvé dans le signet +feed_management.exceptions.bookmark_not_found: Le signet n'a pas été trouvé +feed_management.exceptions.cannot_add_article_to_bookmark: Vous n'avez pas la permission d'ajouter cet article au signet +feed_management.exceptions.cannot_delete_bookmark: Vous n'avez pas la permission de supprimer ce signet +feed_management.exceptions.cannot_update_bookmark: Vous n'avez pas la permission de mettre à jour ce signet +feed_management.exceptions.followed_source_not_found: La source suivie n'a pas été trouvée +feed_management.exceptions.source_already_followed: La source est déjà suivie +feed_management.exceptions.cannot_delete_comment: Vous n'avez pas la permission de supprimer ce commentaire +feed_management.exceptions.comment_not_found: Le commentaire n'a pas été trouvé diff --git a/projects/backend/translations/identity_and_access.fr.yaml b/projects/backend/translations/identity_and_access.fr.yaml new file mode 100644 index 0000000..c047f72 --- /dev/null +++ b/projects/backend/translations/identity_and_access.fr.yaml @@ -0,0 +1,30 @@ +# Validations +identity_and_access.validations.password_must_match: Les deux mots de passe ne sont pas identiques ! +identity_and_access.validations.empty_role: Veuillez définir au moins un rôle +identity_and_access.validations.empty_username: Le nom d'utilisateur ne doit pas être vide +identity_and_access.validations.invalid_username_pattern: Le nom d'utilisateur ne peut contenir que des lettre et chiffre, (_) et (.), sans espace +identity_and_access.validations.long_username: Ce nom d'utilisateur est trop long (maximum 30 caractères) +identity_and_access.validations.short_username: Ce nom d'utilisateur est trop court (maximum 5 caractères) + +# Exceptions +identity_and_access.exceptions.user_not_found: Désolé nous n'avons pas trouvé d'utilisateur avec ces informations. +identity_and_access.exceptions.invalid_current_password: Le mot de passe actuel est incorrecte. +identity_and_access.exceptions.email_already_used: Impossible d'utiliser cette address email, essayez avec une autre. +identity_and_access.exceptions.invalid_verification_token: Désolé le jeton de sécurité n'est pas valide ! +identity_and_access.exceptions.password_already_defined: Désolé un mot de passe a déjà été défini ! +identity_and_access.exceptions.account_is_locked: Pour des raisons de sécurité, votre compte a été verrouillé. Suivez les instructions envoyées à votre adresse email pour le déverrouiller. +identity_and_access.exceptions.account_not_confirmed: Votre compte n'est pas encore confirmé. Veuillez suivre les instructions envoyées à votre adresse email pour le confirmer. +Invalid credentials.: Identifiants incorrects. +identity_and_access.exceptions.permission_not_granted: Impossible de continuer, {reason}. + +# Emails +identity_and_access.emails.subjects.password_created: "[DRC News] Connectez-vous à votre compte avec ce mot de passe par défaut" +identity_and_access.emails.subjects.password_forgotten: "[DRC News] Instruction de réinitialisation de mot de passe" +identity_and_access.emails.subjects.password_reset: "[DRC News] Votre mot de passe a été mis à jour" +identity_and_access.emails.subjects.password_updated: "[DRC News] Votre mot de passe a été mis à jour" +identity_and_access.emails.subjects.account_confirmed: "[DRC News] Votre compte a été confirmé" +identity_and_access.emails.subjects.account_locked: "[DRC News] Votre compte a été verrouillé" +identity_and_access.emails.subjects.account_unlocked: "[DRC News] Votre compte a été déverrouillé" +identity_and_access.emails.subjects.confirmation_requested: "[DRC News] Instruction de confirmation de compte" +identity_and_access.emails.subjects.login_profile_changed: "[DRC News] Nouvelle connexion à votre compte" +identity_and_access.emails.subjects.email_updated: "[DRC News] Votre adresse email a été mise à jour" diff --git a/projects/backend/translations/messages.fr.yaml b/projects/backend/translations/messages.fr.yaml new file mode 100644 index 0000000..96a4a7a --- /dev/null +++ b/projects/backend/translations/messages.fr.yaml @@ -0,0 +1,4 @@ +generic.form_error: Certains champs sont invalides +generic.error: Désolé une erreur est survenue, veuillez réessayer +generic.success: Action effectuée avec succès ! +generic.invalid_argument: Désolé certaines données fournis sont invalides. diff --git a/projects/backend/translations/shared_kernel.fr.yaml b/projects/backend/translations/shared_kernel.fr.yaml new file mode 100644 index 0000000..4ad30d9 --- /dev/null +++ b/projects/backend/translations/shared_kernel.fr.yaml @@ -0,0 +1,3 @@ +# Exceptions +shared_kernel.exceptions.invalid_email_address: Cet adresse email n'est pas valide ! +shared_kernel.exceptions.invalid_argument: Les informations fournis sont invalides ! diff --git a/projects/backend/translations/validators.fr.yaml b/projects/backend/translations/validators.fr.yaml new file mode 100644 index 0000000..557812e --- /dev/null +++ b/projects/backend/translations/validators.fr.yaml @@ -0,0 +1 @@ +identity_and_access.exceptions.passwords_do_not_match: Les mots de passe ne correspondent pas. \ No newline at end of file diff --git a/projects/crawler/.dockerignore b/projects/crawler/.dockerignore new file mode 100644 index 0000000..9bbcac5 --- /dev/null +++ b/projects/crawler/.dockerignore @@ -0,0 +1,23 @@ +# Ignore Python cache files +__pycache__/ +*.pyc + +# Ignore virtual environments +.venv/ + +# Ignore local environment files +.env.local +.env.*.local + +# Ignore logs +*.log + +# Ignore Docker-related files +Dockerfile +docker-compose.yml + +# Ignore other unnecessary files +*.swp +.idea/ +.vscode/ +.DS_Store diff --git a/projects/crawler/.env b/projects/crawler/.env new file mode 100644 index 0000000..139597f --- /dev/null +++ b/projects/crawler/.env @@ -0,0 +1,2 @@ + + diff --git a/projects/crawler/.gitignore b/projects/crawler/.gitignore new file mode 100644 index 0000000..6bcf335 --- /dev/null +++ b/projects/crawler/.gitignore @@ -0,0 +1,21 @@ +.idea/ +.vscode/ +.ipynb_checkpoints/ +*.pyc +.env.local +.env.*.local +var/ +.DS_Store + +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +data/ diff --git a/projects/crawler/.pre-commit-config.yaml b/projects/crawler/.pre-commit-config.yaml new file mode 100644 index 0000000..70d5c0b --- /dev/null +++ b/projects/crawler/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.12 + hooks: + - id: ruff-check + - id: ruff-format diff --git a/projects/crawler/.python-version b/projects/crawler/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/projects/crawler/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/projects/crawler/Dockerfile b/projects/crawler/Dockerfile new file mode 100644 index 0000000..1be8dc3 --- /dev/null +++ b/projects/crawler/Dockerfile @@ -0,0 +1,34 @@ +# Use the official Python image as a base +FROM python:3.13-slim + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Install the project into `/app` +WORKDIR /app + +# Enable bytecode compilation +ENV UV_COMPILE_BYTECODE=1 + +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy + +# Ensure installed tools can be executed out of the box +ENV UV_TOOL_BIN_DIR=/usr/local/bin + +# Install the project's dependencies using the lockfile and settings +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" + +# Reset the entrypoint, don't invoke `uv` +ENTRYPOINT [] diff --git a/projects/crawler/README.md b/projects/crawler/README.md new file mode 100644 index 0000000..bc4cfb9 --- /dev/null +++ b/projects/crawler/README.md @@ -0,0 +1,11 @@ +# Crawler + +[![Lint](https://github.com/bernard-ng/basango/actions/workflows/lint.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/lint.yml) +[![Lint](https://github.com/bernard-ng/basango/actions/workflows/test.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/test.yml) +[![Security](https://github.com/bernard-ng/basango/actions/workflows/security.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/security.yml) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit) + +--- + +### Get started diff --git a/projects/crawler/compose.yaml b/projects/crawler/compose.yaml new file mode 100644 index 0000000..5dc3b22 --- /dev/null +++ b/projects/crawler/compose.yaml @@ -0,0 +1,38 @@ +services: + basango: + build: . + container_name: basango-app + restart: unless-stopped + networks: + - basango-network + + redis: + image: redis:7-alpine + container_name: basango-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/var/redis + command: redis-server --appendonly yes + networks: + - basango-network + + redis-commander: + image: rediscommander/redis-commander:latest + container_name: basango-redis-commander + restart: unless-stopped + ports: + - "8081:8081" + environment: + - REDIS_HOSTS=local:redis:6379 + depends_on: + - redis + networks: + - basango-network + +networks: + basango-network: + +volumes: + redis_data: diff --git a/projects/crawler/config/pipeline.dev.yaml b/projects/crawler/config/pipeline.dev.yaml new file mode 100644 index 0000000..e3d35fa --- /dev/null +++ b/projects/crawler/config/pipeline.dev.yaml @@ -0,0 +1,97 @@ +# Fetching and crawling configuration +fetch: + client: + timeout: 20 + user_agent: Basango/0.1 (+https://github.com/bernard-ng/basango) + follow_redirects: true + verify_ssl: true + rotate_user_agent: true + max_retries: 3 + backoff_initial: 1.0 + backoff_multiplier: 2.0 + backoff_max: 30.0 + respect_retry_after: true + crawler: + notify: false + use_multi_threading: false + max_workers: 5 + +# Source configurations +sources: + html: + - source_id: radiookapi.net + source_url: https://www.radiookapi.net + source_date: + pattern: "/(\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + source_selectors: + articles: ".view-content > .views-row.content-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field-name-body" + article_date: ".views-field-created" + article_categories: ".views-field-field-cat-gorie a" + pagination: "ul.pagination > li a(:last-child)" + pagination_template: "/actualite?page={page}" + supports_categories: false + requires_details: false + requires_rate_limit: false + + - source_id: 7sur7.cd + source_url: https://7sur7.cd + source_date: + pattern: "/\\w{3} (\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + categories: [ "politique", "economie", "culture", "sport", "societe" ] + source_selectors: + articles: ".view-content > .row.views-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field.field--name-body" + article_date: ".views-field-created" + pagination: "ul.pagination > li a(:last-child)" + pagination_template: "/index.php/category/{category}?page={page}" + supports_categories: true + requires_details: false + requires_rate_limit: false + + - source_id: mediacongo.net + source_url: https://mediacongo.net + source_date: + format: "%d.%m.%Y %H:%M" + source_selectors: + articles: ".for_aitems > .article_other_item" + article_title: "img" + article_link: "a(:first-child)" + article_categories: "a.color_link" + article_body: ".article_ttext" + article_date: ".article_other_about" + pagination: ".nav > a(:last-child)" + pagination_template: "/articles.html?page={page}" + supports_categories: false + requires_details: true + requires_rate_limit: false + + - source_id: actualite.cd + source_url: https://actualite.cd + source_date: + pattern: "/(\\d{1}) (\\d{1,2}) (\\d{2}) (\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$4-$3-$2 $5" + source_selectors: + articles: "#views-bootstrap-taxonomy-term-page-2 > div > div" + article_title: "#actu-titre a" + article_link: "#actu-titre a" + article_categories: "#actu-cat a" + article_body: ".views-field.views-field-body" + article_date: "#p-date" + pagination_template: "/actualite?page={page}" + supports_categories: false + requires_details: true + requires_rate_limit: false + + wordpress: + - source_id: beto.cd + source_url: https://beto.cd + requires_rate_limit: true + - source_id: newscd.net + source_url: https://newscd.net diff --git a/projects/crawler/config/pipeline.prod.yaml b/projects/crawler/config/pipeline.prod.yaml new file mode 100644 index 0000000..86b09bc --- /dev/null +++ b/projects/crawler/config/pipeline.prod.yaml @@ -0,0 +1,160 @@ +# Fetching and crawling configuration +fetch: + client: + timeout: 20 + user_agent: Basango/0.1 (+https://github.com/bernard-ng/basango) + follow_redirects: true + verify_ssl: true + rotate_user_agent: true + max_retries: 3 + backoff_initial: 1.0 + backoff_multiplier: 2.0 + backoff_max: 30.0 + respect_retry_after: true + crawler: + notify: false + use_multi_threading: false + max_workers: 5 + +# Logging configuration +# Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) +logging: + level: "ERROR" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + file_logging: true # Enable logging to file + console_logging: true # Enable logging to console + log_file: "pipeline.log" # Log file name + max_log_size: 10485760 # Maximum size of log file before rotation (10MB) + backup_count: 5 # Number of backup log files to keep + +# Source configurations +sources: + html: + - source_id: radiookapi.net + source_url: https://www.radiookapi.net + source_date: + pattern: "/(\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + source_selectors: + articles: ".view-content > .views-row.content-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field-name-body" + article_date: ".views-field-created" + article_categories: ".views-field-field-cat-gorie a" + pagination: "ul.pagination > li a(:last-child)" + pagination_template: "/actualite?page={page}" + supports_categories: false + requires_details: false + requires_rate_limit: false + + - source_id: 7sur7.cd + source_url: https://7sur7.cd + source_date: + pattern: "/\\w{3} (\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + categories: [ "politique", "economie", "culture", "sport", "societe" ] + source_selectors: + articles: ".view-content > .row.views-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field.field--name-body" + article_date: ".views-field-created" + pagination: "ul.pagination > li a(:last-child)" + pagination_template: "/index.php/category/{category}?page={page}" + supports_categories: true + requires_details: false + requires_rate_limit: false + + - source_id: mediacongo.net + source_url: https://mediacongo.net + source_date: + format: "%d.%m.%Y %H:%M" + source_selectors: + articles: ".for_aitems > .article_other_item" + article_title: "img" + article_link: "a(:first-child)" + article_categories: "a.color_link" + article_body: ".article_ttext" + article_date: ".article_other_about" + pagination: ".nav > a(:last-child)" + pagination_template: "/articles.html?page={page}" + supports_categories: false + requires_details: true + requires_rate_limit: false + + - source_id: actualite.cd + source_url: https://actualite.cd + source_date: + pattern: "/(\\d{1}) (\\d{1,2}) (\\d{2}) (\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$4-$3-$2 $5" + source_selectors: + articles: "#views-bootstrap-taxonomy-term-page-2 > div > div" + article_title: "#actu-titre a" + article_link: "#actu-titre a" + article_categories: "#actu-cat a" + article_body: ".views-field.views-field-body" + article_date: "#p-date" + pagination_template: "/actualite?page={page}" + supports_categories: false + requires_details: true + requires_rate_limit: false + + wordpress: + - source_id: beto.cd + source_url: https://beto.cd + requires_rate_limit: true + - source_id: newscd.net + source_url: https://newscd.net + - source_id: africanewsrdc.net + source_url: https://www.africanewsrdc.net + - source_id: angazainstitute.ac.cd + source_url: https://angazainstitute.ac.cd + - source_id: b-onetv.cd + source_url: https://b-onetv.cd + - source_id: bukavufm.com + source_url: https://bukavufm.com + - source_id: changement7.net + source_url: https://changement7.net + - source_id: congoactu.net + source_url: https://congoactu.net + - source_id: congoindependant.com + source_url: https://www.congoindependant.com + - source_id: congoquotidien.com + source_url: https://www.congoquotidien.com + - source_id: cumulard.cd + source_url: https://www.cumulard.cd + - source_id: environews-rdc.net + source_url: https://environews-rdc.net + - source_id: freemediardc.info + source_url: https://www.freemediardc.info + - source_id: geopolismagazine.org + source_url: https://geopolismagazine.org + - source_id: habarirdc.net + source_url: https://habarirdc.net + - source_id: infordc.com + source_url: https://infordc.com + - source_id: kilalopress.net + source_url: https://kilalopress.net + - source_id: laprosperiteonline.net + source_url: https://laprosperiteonline.net + - source_id: laprunellerdc.cd + source_url: https://laprunellerdc.cd + - source_id: lesmedias.net + source_url: https://lesmedias.net + - source_id: lesvolcansnews.net + source_url: https://lesvolcansnews.net + - source_id: netic-news.net + source_url: https://www.netic-news.net + - source_id: objectif-infos.cd + source_url: https://objectif-infos.cd + - source_id: scooprdc.net + source_url: https://scooprdc.net + - source_id: journaldekinshasa.com + source_url: https://www.journaldekinshasa.com + - source_id: lepotentiel.cd + source_url: https://lepotentiel.cd + - source_id: acturdc.com + source_url: https://acturdc.com + - source_id: matininfos.net + source_url: https://matininfos.net diff --git a/projects/crawler/config/pipeline.yaml b/projects/crawler/config/pipeline.yaml new file mode 100644 index 0000000..a48b52b --- /dev/null +++ b/projects/crawler/config/pipeline.yaml @@ -0,0 +1,160 @@ +# Fetching and crawling configuration +fetch: + client: + timeout: 20 + user_agent: Basango/0.1 (+https://github.com/bernard-ng/basango) + follow_redirects: true + verify_ssl: true + rotate: true + max_retries: 3 + backoff_initial: 1.0 + backoff_multiplier: 2.0 + backoff_max: 30.0 + respect_retry_after: true + crawler: + notify: false + use_multi_threading: false + max_workers: 5 + +# Logging configuration +# Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) +logging: + level: "INFO" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + file_logging: true # Enable logging to file + console_logging: true # Enable logging to console + log_file: "pipeline.log" # Log file name + max_log_size: 10485760 # Maximum size of log file before rotation (10MB) + backup_count: 5 # Number of backup log files to keep + +# Source configurations +sources: + html: + - source_id: radiookapi.net + source_url: https://www.radiookapi.net + source_date: + pattern: "/(\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + source_selectors: + articles: ".view-content > .views-row.content-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field-name-body" + article_date: ".views-field-created" + article_categories: ".views-field-field-cat-gorie a" + pagination: "ul.pagination > li.pager-last > a" + pagination_template: "actualite" + supports_categories: false + requires_details: false + requires_rate_limit: false + + - source_id: 7sur7.cd + source_url: https://7sur7.cd + source_date: + pattern: "/\\w{3} (\\d{2})\/(\\d{2})\/(\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$3-$2-$1 $4" + categories: [ "politique", "economie", "culture", "sport", "societe" ] + source_selectors: + articles: ".view-content > .row.views-row" + article_title: ".views-field-title a" + article_link: ".views-field-title a" + article_body: ".field.field--name-body" + article_date: ".views-field-created" + pagination: "ul.pagination > li.pager__item.pager__item--last > a" + pagination_template: "index.php/category/{category}" + supports_categories: true + requires_details: false + requires_rate_limit: false + + - source_id: mediacongo.net + source_url: https://www.mediacongo.net + source_date: + format: "%d.%m.%Y %H:%M" + source_selectors: + articles: ".for_aitems > .article_other_item" + article_title: "img" + article_link: "a:first-child" + article_categories: "a.color_link" + article_body: ".article_ttext" + article_date: ".article_other_about" + pagination: "div.pagination > div > a:last-child" + pagination_template: "articles.html" + supports_categories: false + requires_details: true + requires_rate_limit: false + + - source_id: actualite.cd + source_url: https://actualite.cd + source_date: + pattern: "/(\\d{1}) (\\d{1,2}) (\\d{2}) (\\d{4}) - (\\d{2}:\\d{2})/" + replacement: "$4-$3-$2 $5" + source_selectors: + articles: "#views-bootstrap-taxonomy-term-page-2 > div > div" + article_title: "#actu-titre a" + article_link: "#actu-titre a" + article_categories: "#actu-cat a" + article_body: ".views-field.views-field-body" + article_date: "#p-date" + pagination_template: "actualite" + supports_categories: false + requires_details: true + requires_rate_limit: false + + wordpress: + - source_id: beto.cd + source_url: https://beto.cd + requires_rate_limit: true + - source_id: newscd.net + source_url: https://newscd.net + - source_id: africanewsrdc.net + source_url: https://www.africanewsrdc.net + - source_id: angazainstitute.ac.cd + source_url: https://angazainstitute.ac.cd + - source_id: b-onetv.cd + source_url: https://b-onetv.cd + - source_id: bukavufm.com + source_url: https://bukavufm.com + - source_id: changement7.net + source_url: https://changement7.net + - source_id: congoactu.net + source_url: https://congoactu.net + - source_id: congoindependant.com + source_url: https://www.congoindependant.com + - source_id: congoquotidien.com + source_url: https://www.congoquotidien.com + - source_id: cumulard.cd + source_url: https://www.cumulard.cd + - source_id: environews-rdc.net + source_url: https://environews-rdc.net + - source_id: freemediardc.info + source_url: https://www.freemediardc.info + - source_id: geopolismagazine.org + source_url: https://geopolismagazine.org + - source_id: habarirdc.net + source_url: https://habarirdc.net + - source_id: infordc.com + source_url: https://infordc.com + - source_id: kilalopress.net + source_url: https://kilalopress.net + - source_id: laprosperiteonline.net + source_url: https://laprosperiteonline.net + - source_id: laprunellerdc.cd + source_url: https://laprunellerdc.cd + - source_id: lesmedias.net + source_url: https://lesmedias.net + - source_id: lesvolcansnews.net + source_url: https://lesvolcansnews.net + - source_id: netic-news.net + source_url: https://www.netic-news.net + - source_id: objectif-infos.cd + source_url: https://objectif-infos.cd + - source_id: scooprdc.net + source_url: https://scooprdc.net + - source_id: journaldekinshasa.com + source_url: https://www.journaldekinshasa.com + - source_id: lepotentiel.cd + source_url: https://lepotentiel.cd + - source_id: acturdc.com + source_url: https://acturdc.com + - source_id: matininfos.net + source_url: https://matininfos.net diff --git a/projects/crawler/pyproject.toml b/projects/crawler/pyproject.toml new file mode 100644 index 0000000..594e064 --- /dev/null +++ b/projects/crawler/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "basango" +version = "0.1.0" +description = "Basango : Web Scrapper for DRC News" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "pydantic>=2.11.7", + "pydantic-settings>=2.10.1", + "rq>=2.5.0", + "typer>=0.16.1", + "uv-build>=0.8.12,<0.9.0", + "pyyaml>=6.0.2", + "httpx>=0.27.2", + "trafilatura>=1.7.0", + "selectolax>=0.3.20", + "markdownify>=0.13.1", + "readability-lxml>=0.8.1", + "beautifulsoup4>=4.13.5", +] + +[dependency-groups] +dev = [ + "bandit>=1.8.6", + "pyright>=1.1.404", + "pytest>=8.4.1", + "ruff>=0.12.9", +] + +[project.scripts] +basango = "basango:main" + +[build-system] +requires = ["uv_build>=0.8.12,<0.9.0"] +build-backend = "uv_build" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/projects/crawler/src/basango/__init__.py b/projects/crawler/src/basango/__init__.py new file mode 100644 index 0000000..9de631d --- /dev/null +++ b/projects/crawler/src/basango/__init__.py @@ -0,0 +1,9 @@ +def main() -> None: + # Lazy import to avoid importing CLI deps during package import + from basango.cli import app + + app() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/projects/crawler/src/basango/cli.py b/projects/crawler/src/basango/cli.py new file mode 100644 index 0000000..e76a5b5 --- /dev/null +++ b/projects/crawler/src/basango/cli.py @@ -0,0 +1,52 @@ +import typer + +from basango.core.config import CrawlerConfig +from basango.core.config_manager import ConfigManager +from basango.domain import PageRange, DateRange, UpdateDirection +from basango.services.crawler.html_crawler import HtmlCrawler +from basango.services.crawler.wordpress_crawler import WordpressCrawler + +app = typer.Typer(no_args_is_help=True, add_completion=False) + + +@app.command("crawl") +def crawl_cmd( + source_id: str = typer.Option( + ..., help="Source id to crawl (as defined in config)" + ), + page: str = typer.Option(None, "--page", "-p", help="Page range e.g. '1:10'"), + date: str = typer.Option( + None, "--date", "-d", help="Date range e.g. '2024-10-01:2024-10-31'" + ), + category: str = typer.Option(None, "--category", "-g", help="Optional category"), + notify: bool = typer.Option(False, "--notify", "-n", help="Enable notifications"), + env: str = typer.Option("development", "--env", "-c", help="Environment"), +) -> None: + """Crawl a single source based on CLI-provided settings.""" + manager = ConfigManager() + + pipeline = manager.get(env) + manager.ensure_directories(pipeline) + manager.setup_logging(pipeline) + + source = pipeline.sources.find(source_id) + assert source is not None, f"Source '{source_id}' not found in config" + + crawler_config = CrawlerConfig( + source=source, + page_range=PageRange.create(page) if page else None, + date_range=DateRange.create(date) if date else None, + category=category, + notify=notify, + direction=UpdateDirection.FORWARD, + ) + + crawlers = [ + HtmlCrawler(crawler_config, pipeline.fetch.client), + WordpressCrawler(crawler_config, pipeline.fetch.client), + ] + + for crawler in crawlers: + if crawler.supports(source.source_kind): + crawler.fetch() + break diff --git a/projects/crawler/src/basango/core/config/__init__.py b/projects/crawler/src/basango/core/config/__init__.py new file mode 100644 index 0000000..63414f5 --- /dev/null +++ b/projects/crawler/src/basango/core/config/__init__.py @@ -0,0 +1,19 @@ +from .fetch_config import ClientConfig, FetchConfig, CrawlerConfig +from .logging_config import LoggingConfig +from .pipeline_config import PipelineConfig +from .source_config import ( + WordPressSourceConfig, + HtmlSourceConfig, + SourcesConfig, +) + +__all__ = [ + "ClientConfig", + "FetchConfig", + "CrawlerConfig", + "LoggingConfig", + "PipelineConfig", + "WordPressSourceConfig", + "HtmlSourceConfig", + "SourcesConfig", +] diff --git a/projects/crawler/src/basango/core/config/fetch_config.py b/projects/crawler/src/basango/core/config/fetch_config.py new file mode 100644 index 0000000..02dac65 --- /dev/null +++ b/projects/crawler/src/basango/core/config/fetch_config.py @@ -0,0 +1,71 @@ +from typing import Optional, Union + +from pydantic import BaseModel, Field + +from basango.domain import PageRange, DateRange, UpdateDirection +from basango.core.config.source_config import HtmlSourceConfig, WordPressSourceConfig + + +class ClientConfig(BaseModel): + timeout: float = Field(default=20.0, description="Request timeout in seconds") + user_agent: str = Field( + default="Basango/0.1 (+https://github.com/bernard-ng/basango)" + ) + follow_redirects: bool = Field(default=True, description="Follow HTTP redirects") + verify_ssl: bool = Field(default=True, description="Verify SSL certificates") + rotate: bool = Field(default=True, description="Rotate User-Agent header") + max_retries: int = Field( + default=3, description="Maximum number of retries on failure" + ) + backoff_initial: float = Field( + default=1.0, description="Initial backoff delay in seconds" + ) + backoff_multiplier: float = Field(default=2.0, description="Backoff multiplier") + backoff_max: float = Field( + default=30.0, description="Maximum backoff delay in seconds" + ) + respect_retry_after: bool = Field( + default=True, description="Respect Retry-After header if present" + ) + + +class CrawlerConfig(BaseModel): + source: Optional[Union[HtmlSourceConfig, WordPressSourceConfig]] = Field( + default=None, description="Source configuration to crawl" + ) + page_range: Optional[PageRange] = Field( + default=None, description="Page range to crawl, e.g: 1:10" + ) + date_range: Optional[DateRange] = Field( + default=None, + description="Date range to filter articles, e.g: 2024-10-01:2024-10-31", + ) + category: Optional[str] = Field( + default=None, description="Optional category to filter articles" + ) + notify: bool = Field( + default=False, description="Enable notifications after crawling" + ) + + is_update: bool = Field( + default=False, + description="Whether this crawl is an update (True) or a full crawl (False)", + ) + use_multi_threading: bool = Field( + default=False, description="Enable multiprocessing for concurrent crawling" + ) + max_workers: int = Field( + default=5, description="Maximum number of concurrent crawling workers" + ) + direction: UpdateDirection = Field( + default=UpdateDirection.FORWARD, description="Crawling direction" + ) + + +class FetchConfig(BaseModel): + client: ClientConfig = Field( + default_factory=ClientConfig, description="Http client configuration" + ) + crawler: CrawlerConfig = Field( + default_factory=CrawlerConfig, description="Crawler configuration" + ) diff --git a/projects/crawler/src/basango/core/config/logging_config.py b/projects/crawler/src/basango/core/config/logging_config.py new file mode 100644 index 0000000..8fa08e1 --- /dev/null +++ b/projects/crawler/src/basango/core/config/logging_config.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class LoggingConfig(BaseModel): + level: str = "INFO" + format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + file_logging: bool = True + console_logging: bool = True + log_file: str = "pipeline.log" + max_log_size: int = 10 * 1024 * 1024 # 10MB + backup_count: int = 5 diff --git a/projects/crawler/src/basango/core/config/pipeline_config.py b/projects/crawler/src/basango/core/config/pipeline_config.py new file mode 100644 index 0000000..645f4f1 --- /dev/null +++ b/projects/crawler/src/basango/core/config/pipeline_config.py @@ -0,0 +1,25 @@ +from pathlib import Path +from pydantic import Field, BaseModel + +from basango.core.config.fetch_config import FetchConfig +from basango.core.config.logging_config import LoggingConfig +from basango.core.config.source_config import SourcesConfig +from basango.core.project_paths import ProjectPaths + + +def _default_project_paths() -> ProjectPaths: + """Create default project paths relative to the project root.""" + root = Path.cwd() + return ProjectPaths( + root=root, + configs=root / "config", + data=root / "data" / "dataset", + logs=root / "data" / "logs", + ) + + +class PipelineConfig(BaseModel): + paths: ProjectPaths = Field(default_factory=_default_project_paths, alias="paths") + logging: LoggingConfig = Field(default_factory=LoggingConfig) + fetch: FetchConfig = Field(default_factory=FetchConfig) + sources: SourcesConfig = Field(default_factory=SourcesConfig) diff --git a/projects/crawler/src/basango/core/config/source_config.py b/projects/crawler/src/basango/core/config/source_config.py new file mode 100644 index 0000000..bfaa204 --- /dev/null +++ b/projects/crawler/src/basango/core/config/source_config.py @@ -0,0 +1,66 @@ +from typing import Union + +from pydantic import BaseModel, Field, HttpUrl + +from basango.domain import SourceDate, SourceKind, SourceSelectors + + +class SourceConfigBase(BaseModel): + source_id: str = Field(..., description="Unique identifier for the source") + source_url: HttpUrl = Field(..., description="URL of the source") + source_date: SourceDate = Field( + default_factory=SourceDate, description="Date extraction schema" + ) + source_kind: SourceKind = Field( + ..., description="Type of the source, e.g., 'wordpress' or 'html'" + ) + categories: list[str] = Field( + default_factory=list, description="List of categories to filter articles" + ) + + supports_categories: bool = Field( + default=False, description="the source supports categories" + ) + requires_details: bool = Field( + default=False, description="detailed article is required to compute date range" + ) + requires_rate_limit: bool = Field( + default=False, description="requires rate limit to avoid being blocked" + ) + + +class WordPressSourceConfig(SourceConfigBase): + source_kind: SourceKind = Field( + default=SourceKind.WORDPRESS, description="Type of the source" + ) + source_date: SourceDate = SourceDate( + format="%Y-%m-%dT%H:%M:%S", pattern=None, replacement=None + ) + + +class HtmlSourceConfig(SourceConfigBase): + source_kind: SourceKind = Field( + default=SourceKind.HTML, description="Type of the source" + ) + source_selectors: SourceSelectors = Field( + default_factory=lambda: SourceSelectors(), + description="CSS selectors for extracting articles", + ) + pagination_template: str = Field( + ..., description="Template URL for pagination, e.g., '/actualite?page={page}'" + ) + + +class SourcesConfig(BaseModel): + html: list[HtmlSourceConfig] = Field( + default_factory=list, description="List of source configurations" + ) + wordpress: list[WordPressSourceConfig] = Field( + default_factory=list, description="List of source configurations" + ) + + def find(self, source_id: str) -> Union[HtmlSourceConfig, WordPressSourceConfig]: + for source in self.html + self.wordpress: + if source.source_id == source_id: + return source + raise ValueError(f"Source with id '{source_id}' not found") diff --git a/projects/crawler/src/basango/core/config_manager.py b/projects/crawler/src/basango/core/config_manager.py new file mode 100644 index 0000000..76c9c57 --- /dev/null +++ b/projects/crawler/src/basango/core/config_manager.py @@ -0,0 +1,134 @@ +import logging +from pathlib import Path +from typing import Optional, Union, Dict + +import yaml + +from basango.core.config import PipelineConfig +from basango.core.project_paths import ProjectPaths + + +class ConfigManager: + def __init__(self, config_path: Optional[Union[str, Path]] = None): + self.config_path = Path(config_path) if config_path else self._find_config() + self._config: Optional[PipelineConfig] = None + self._setup_paths() + + def get(self, env: Optional[str] = None) -> PipelineConfig: + if env: + path = self.config_path.parent / f"pipeline.{env}.yaml" + + if path.exists(): + base = self.load().model_dump() + self._override(base, self.load(path).model_dump()) + return PipelineConfig(**base) + + if self._config is None: + self._config = self.load() + return self._config + + def load(self, config_path: Optional[Path] = None) -> PipelineConfig: + """Load configuration from file""" + self.config_path = config_path if config_path else self._find_config() + + if not self.config_path.exists(): + logging.warning( + f"Config file not found: {self.config_path}. Using defaults." + ) + return self._create_default() + + try: + with open(self.config_path, "r") as f: + config_data = yaml.safe_load(f) + + if "paths" not in config_data: + config_data["paths"] = self.default_paths.model_dump() + + self._config = PipelineConfig(**config_data) + return self._config + + except Exception as e: + logging.error(f"Failed to load config from {self.config_path}: {e}") + return self._create_default() + + @classmethod + def ensure_directories(cls, cfg: PipelineConfig) -> None: + directories = [cfg.paths.data, cfg.paths.logs, cfg.paths.configs] + + for directory in directories: + Path(directory).mkdir(parents=True, exist_ok=True) + + logging.info("Ensured all required directories exist") + + @classmethod + def setup_logging(cls, cfg: PipelineConfig): + logs_path = cfg.paths.logs + logs_path.mkdir(parents=True, exist_ok=True) + + # Setup logging configuration + log_level = getattr(logging, cfg.logging.level.upper(), logging.INFO) + + # Create formatter + formatter = logging.Formatter(cfg.logging.format) + + # Setup root logger + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # Clear existing handlers + root_logger.handlers.clear() + + # Console handler + if cfg.logging.console_logging: + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # File handler + if cfg.logging.file_logging: + from logging.handlers import RotatingFileHandler + + log_file_path = logs_path / cfg.logging.log_file + file_handler = RotatingFileHandler( + log_file_path, + maxBytes=cfg.logging.max_log_size, + backupCount=cfg.logging.backup_count, + ) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + @classmethod + def _find_config(cls) -> Path: + possible_paths = [ + Path.cwd() / "config" / "pipeline.yaml", + Path.cwd() / "config" / "pipeline.yml", + Path.cwd() / "pipeline.yaml", + Path(__file__).parent.parent.parent.parent / "config" / "pipeline.yaml", + ] + + for path in possible_paths: + if path.exists(): + return path + + raise FileNotFoundError( + "No configuration file found in the expected locations." + ) + + def _setup_paths(self) -> None: + root = Path(__file__).parent.parent.parent.parent + self.default_paths = ProjectPaths( + root=root, + configs=root / "config", + data=root / "data" / "dataset", + logs=root / "data" / "logs", + ) + + def _create_default(self) -> PipelineConfig: + return PipelineConfig(paths=self.default_paths) + + def _override(self, base: Dict, update: Dict): + for key, value in update.items(): + if key in base and isinstance(base[key], dict) and isinstance(value, dict): + self._override(base[key], value) + else: + base[key] = value diff --git a/projects/crawler/src/basango/core/project_paths.py b/projects/crawler/src/basango/core/project_paths.py new file mode 100644 index 0000000..1793a81 --- /dev/null +++ b/projects/crawler/src/basango/core/project_paths.py @@ -0,0 +1,26 @@ +from pathlib import Path + +from pydantic import BaseModel, field_validator, ConfigDict + + +class ProjectPaths(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + root: Path + data: Path + logs: Path + configs: Path + + @classmethod + @field_validator("*", mode="before") + def convert_to_path(cls, v): + return Path(v) if not isinstance(v, Path) else v + + def get_data_path(self, filename: str) -> Path: + return self.data / filename + + def get_logs_path(self, filename: str) -> Path: + return self.logs / filename + + def get_config_path(self, filename: str) -> Path: + return self.configs / filename diff --git a/projects/crawler/src/basango/domain/__init__.py b/projects/crawler/src/basango/domain/__init__.py new file mode 100644 index 0000000..0c9acc4 --- /dev/null +++ b/projects/crawler/src/basango/domain/__init__.py @@ -0,0 +1,15 @@ +from .article import Article +from .date_range import DateRange +from .page_range import PageRange +from .source import SourceKind, SourceDate, SourceSelectors +from .update_direction import UpdateDirection + +__all__ = [ + "Article", + "DateRange", + "PageRange", + "SourceKind", + "SourceDate", + "SourceSelectors", + "UpdateDirection", +] diff --git a/projects/crawler/src/basango/domain/article.py b/projects/crawler/src/basango/domain/article.py new file mode 100644 index 0000000..439ae68 --- /dev/null +++ b/projects/crawler/src/basango/domain/article.py @@ -0,0 +1,14 @@ +from datetime import datetime +from typing import Any, Optional + +from pydantic import BaseModel, HttpUrl + + +class Article(BaseModel): + title: str + link: HttpUrl + body: str + categories: list[str] + source: str + timestamp: datetime + metadata: Optional[dict[str, Any]] = None diff --git a/projects/crawler/src/basango/domain/date_range.py b/projects/crawler/src/basango/domain/date_range.py new file mode 100644 index 0000000..47fc7ed --- /dev/null +++ b/projects/crawler/src/basango/domain/date_range.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass +from datetime import datetime, timezone, timedelta +from typing import Optional + + +def _ensure_utc(dt: datetime) -> datetime: + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt + + +@dataclass(frozen=True) +class DateRange: + start: int # Unix timestamp + end: int # Unix timestamp + + def __post_init__(self) -> None: + assert self.start != 0, "[DateRange] Start timestamp cannot be 0" + assert self.end != 0, "[DateRange] End timestamp cannot be 0" + assert self.end >= self.start, ( + "[DateRange] End must be greater than or equal to start" + ) + + def __str__(self) -> str: + return f"{self.start}:{self.end}" + + def in_range(self, ts: int) -> bool: + return self.start <= ts <= self.end + + def out_range(self, ts: int) -> bool: + return ts < self.start or ts > self.end + + def format(self, fmt: str = "%Y-%m-%d") -> str: + start = datetime.fromtimestamp(self.start, tz=timezone.utc).strftime(fmt) + end = datetime.fromtimestamp(self.end, tz=timezone.utc).strftime(fmt) + return f"{start}:{end}" + + @classmethod + def create( + cls, spec: str, fmt: str = "%Y-%m-%d", separator: str = ":" + ) -> "DateRange": + assert separator != "", "[DateRange] Separator cannot be empty" + assert separator in spec, f"[DateRange] {separator} must be in {spec}" + + parts = spec.split(separator) + assert len(parts) == 2, f"[DateRange] Invalid date interval: {spec}" + + start = _ensure_utc(datetime.strptime(parts[0], fmt)) + end = _ensure_utc(datetime.strptime(parts[1], fmt)) + return cls(int(start.timestamp()), int(end.timestamp())) + + @classmethod + def backward(cls, date: Optional[datetime] = None, days: int = 30) -> "DateRange": + base = _ensure_utc(date or datetime.now(timezone.utc)) + + start = base - timedelta(days=days) + end = base + timedelta(days=1) # in future to avoid timezone issues + return cls(int(start.timestamp()), int(end.timestamp())) + + @classmethod + def forward(cls, date: datetime) -> "DateRange": + start = _ensure_utc(date) + end = datetime.now(timezone.utc) + timedelta(days=1) + return cls(int(start.timestamp()), int(end.timestamp())) diff --git a/projects/crawler/src/basango/domain/exception.py b/projects/crawler/src/basango/domain/exception.py new file mode 100644 index 0000000..26b4ffb --- /dev/null +++ b/projects/crawler/src/basango/domain/exception.py @@ -0,0 +1,18 @@ +from basango.domain import DateRange + + +class ArticleNotFoundError(Exception): + pass + + +class ArticleOutOfRange(Exception): + def __init__(self, timestamp: str, date_range: DateRange): + self.timestamp = timestamp + self.date_range = date_range + super().__init__( + f"Article with timestamp {timestamp} is out of range {date_range}" + ) + + @classmethod + def create(cls, timestamp: str, date_range: DateRange) -> "ArticleOutOfRange": + return cls(timestamp, date_range) diff --git a/projects/crawler/src/basango/domain/page_range.py b/projects/crawler/src/basango/domain/page_range.py new file mode 100644 index 0000000..e756c90 --- /dev/null +++ b/projects/crawler/src/basango/domain/page_range.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PageRange: + start: int + end: int + + @staticmethod + def create(spec: str) -> "PageRange": + parts = spec.split(":") + assert len(parts) == 2, f"[PageRange] Invalid page range: {spec}" + + start, end = int(parts[0]), int(parts[1]) + assert start >= 0, f"[PageRange] Invalid page range: {spec}" + assert end >= start, f"[PageRange] Invalid page range: {spec}" + return PageRange(start=start, end=end) + + def __str__(self): + return f"{self.start}:{self.end}" diff --git a/projects/crawler/src/basango/domain/source.py b/projects/crawler/src/basango/domain/source.py new file mode 100644 index 0000000..f6258f7 --- /dev/null +++ b/projects/crawler/src/basango/domain/source.py @@ -0,0 +1,41 @@ +from enum import StrEnum +from typing import Optional + +from pydantic import BaseModel, Field + + +class SourceKind(StrEnum): + WORDPRESS = "wordpress" + HTML = "html" + + +class SourceDate(BaseModel): + format: str = "%Y-%m-%dT%H:%M:%S" + pattern: Optional[str] = None + replacement: Optional[str] = None + + +class SourceSelectors(BaseModel): + articles: Optional[str] = Field( + default=None, description="CSS selector for the list of articles within a page" + ) + article_title: Optional[str] = Field( + default=None, description="CSS selector for the article title" + ) + article_link: Optional[str] = Field( + default=None, description="CSS selector for the article link" + ) + article_body: Optional[str] = Field( + default=None, description="CSS selector for the article body/content" + ) + article_date: Optional[str] = Field( + default=None, description="CSS selector for the article date" + ) + article_categories: Optional[str] = Field( + default=None, description="CSS selector for the article categories" + ) + + pagination: str = Field( + default="ul.pagination > li a", + description="CSS selector for the pagination links", + ) diff --git a/projects/crawler/src/basango/domain/update_direction.py b/projects/crawler/src/basango/domain/update_direction.py new file mode 100644 index 0000000..f73c483 --- /dev/null +++ b/projects/crawler/src/basango/domain/update_direction.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class UpdateDirection(StrEnum): + FORWARD = "forward" + BACKWARD = "backward" diff --git a/projects/crawler/src/basango/services/__init__.py b/projects/crawler/src/basango/services/__init__.py new file mode 100644 index 0000000..22b0a04 --- /dev/null +++ b/projects/crawler/src/basango/services/__init__.py @@ -0,0 +1,11 @@ +from .date_parser import DateParser +from .http_client import HttpClient +from .open_graph import OpenGraphProvider +from .user_agents import UserAgentProvider + +__all__ = [ + "DateParser", + "HttpClient", + "OpenGraphProvider", + "UserAgentProvider", +] diff --git a/projects/crawler/src/basango/services/crawler/__init__.py b/projects/crawler/src/basango/services/crawler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/crawler/src/basango/services/crawler/base_crawler.py b/projects/crawler/src/basango/services/crawler/base_crawler.py new file mode 100644 index 0000000..697d614 --- /dev/null +++ b/projects/crawler/src/basango/services/crawler/base_crawler.py @@ -0,0 +1,62 @@ +import logging +from abc import ABC, abstractmethod +from typing import Optional + +from bs4 import BeautifulSoup + +from basango.core.config import CrawlerConfig, ClientConfig +from basango.domain import DateRange, SourceKind, PageRange +from basango.domain.exception import ArticleOutOfRange +from basango.services import HttpClient + + +class BaseCrawler(ABC): + def __init__( + self, crawler_config: CrawlerConfig, client_config: ClientConfig + ) -> None: + self.config = crawler_config + self.source = crawler_config.source + self.client = HttpClient(client_config=client_config) + + @abstractmethod + def fetch(self) -> None: + pass + + def crawl(self, url: str, page: Optional[int] = None) -> BeautifulSoup: + if page is not None: + logging.info(f"> Page {page}") + + response = self.client.get(url).text + return BeautifulSoup(response, "html.parser") + + @abstractmethod + def fetch_one(self, html: str, date_range: Optional[DateRange] = None) -> None: + pass + + @abstractmethod + def get_pagination(self) -> PageRange: + pass + + def get_last_page(self) -> int: + return 1 + + @abstractmethod + def supports(self, source_kind: SourceKind) -> bool: + pass + + @classmethod + def initialize(cls) -> None: + logging.info("Initializing Crawler") + + def completed(self, notify: bool = False) -> None: + logging.info("Crawling completed") + if notify: + logging.info("Sending notification about completion") + # Implement notification logic here + + @classmethod + def skip(cls, date_range: DateRange, timestamp: str, title: str, date: str) -> None: + if date_range.out_range(int(timestamp)): + raise ArticleOutOfRange.create(timestamp, date_range) + + logging.warning(f"> {title} [Skipped {date}]") diff --git a/projects/crawler/src/basango/services/crawler/html_crawler.py b/projects/crawler/src/basango/services/crawler/html_crawler.py new file mode 100644 index 0000000..755b529 --- /dev/null +++ b/projects/crawler/src/basango/services/crawler/html_crawler.py @@ -0,0 +1,72 @@ +import re +from typing import Optional, cast, override +from urllib.parse import urlparse, parse_qs + +from basango.core.config import CrawlerConfig, ClientConfig +from basango.core.config.source_config import HtmlSourceConfig +from basango.domain import PageRange, SourceKind, DateRange +from basango.services.crawler.base_crawler import BaseCrawler + + +class HtmlCrawler(BaseCrawler): + def __init__( + self, crawler_config: CrawlerConfig, client_config: ClientConfig + ) -> None: + super().__init__(crawler_config, client_config) + if not self.source or self.source.source_kind != SourceKind.HTML: + raise ValueError("HtmlCrawler requires a source of kind HTML") + + self.source = cast(HtmlSourceConfig, self.source) + + @override + def fetch(self) -> None: + self.initialize() + page = self.config.page_range or self.get_pagination() + print(page) + + @override + def fetch_one(self, html: str, date_range: Optional[DateRange] = None) -> None: + pass + + @override + def get_pagination(self) -> PageRange: + return PageRange.create(f"0:{self.get_last_page()}") + + @override + def get_last_page(self) -> int: + if not self.source: + return 1 + + if self.source.supports_categories and self.config.category: + path = self.source.pagination_template.replace( + "{category}", self.config.category + ) + else: + path = self.source.pagination_template + + links = self.crawl(f"{self.source.source_url}{path}").select( + self.source.source_selectors.pagination + ) + if not links: + return 1 + + href = links[-1].get("href") + if not href or not isinstance(href, str): + return 1 + + # Extract number from href using regex or url parsing + match = re.search(r"(\d+)", href) + if match: + return int(match.group(1)) + + queries = parse_qs(urlparse(href).query) + if "page" in queries and queries["page"]: + try: + return int(queries["page"][0]) + except ValueError: + return 1 + return 1 + + @override + def supports(self, source_kind: SourceKind) -> bool: + return source_kind == SourceKind.HTML diff --git a/projects/crawler/src/basango/services/crawler/wordpress_crawler.py b/projects/crawler/src/basango/services/crawler/wordpress_crawler.py new file mode 100644 index 0000000..e688017 --- /dev/null +++ b/projects/crawler/src/basango/services/crawler/wordpress_crawler.py @@ -0,0 +1,71 @@ +import logging +from typing import Optional, override, cast, Final + +from basango.core.config import WordPressSourceConfig, CrawlerConfig, ClientConfig +from basango.domain import SourceKind, PageRange, DateRange +from basango.services.crawler.base_crawler import BaseCrawler + + +class WordpressCrawler(BaseCrawler): + def __init__( + self, crawler_config: CrawlerConfig, client_config: ClientConfig + ) -> None: + super().__init__(crawler_config, client_config) + if not self.source or self.source.source_kind != SourceKind.WORDPRESS: + raise ValueError("WordpressCrawler requires a source of kind WORDPRESS") + + self.source = cast(WordPressSourceConfig, self.source) + + POST_QUERY: Final = "_fields=date,slug,link,title.rendered,content.rendered,categories&orderby=date&order=desc" + CATEGORY_QUERY: Final = ( + "_fields=id,slug,count&orderby=count&order=desc&per_page=100" + ) + TOTAL_PAGES_HEADER: Final = "x-wp-totalpages" + TOTAL_POSTS_HEADER: Final = "x-wp-total" + + category_map: dict[int, str] = {} + + @override + def fetch(self) -> None: + self.initialize() + page = self.config.page_range or self.get_pagination() + print(page) + + @override + def fetch_one(self, html: str, date_range: Optional[DateRange] = None) -> None: + pass + + @override + def get_pagination(self) -> PageRange: + response = self.client.get( + f"{self.source.source_url}wp-json/wp/v2/posts?_fields=id&per_page=100" + ) + pages = int(response.headers.get(self.TOTAL_PAGES_HEADER, "1")) + posts = int(response.headers.get(self.TOTAL_POSTS_HEADER, "0")) + + logging.info(f"WordPress Pagination {posts} posts in {pages} pages") + return PageRange.create(f"1:{pages}") + + def _fetch_categories(self) -> None: + response = self.client.get( + f"{self.source.source_url}wp-json/wp/v2/categories?{self.CATEGORY_QUERY}" + ) + for category in response.json(): + self.category_map[int(category["id"])] = category["slug"] + + def _map_categories(self, categories: list[int]) -> str: + if not self.category_map: + self._fetch_categories() + return ",".join( + self.category_map[category] + for category in sorted(categories) + if category in self.category_map + ) + + @override + def get_last_page(self) -> int: + return 1 + + @override + def supports(self, source_kind: SourceKind) -> bool: + return source_kind == SourceKind.WORDPRESS diff --git a/projects/crawler/src/basango/services/date_parser.py b/projects/crawler/src/basango/services/date_parser.py new file mode 100644 index 0000000..c6e6ee1 --- /dev/null +++ b/projects/crawler/src/basango/services/date_parser.py @@ -0,0 +1,82 @@ +import logging +import re +from datetime import datetime, timezone +from typing import Optional + + +class DateParser: + MONTHS = { + "janvier": "01", + "février": "02", + "mars": "03", + "avril": "04", + "mai": "05", + "juin": "06", + "juillet": "07", + "août": "08", + "septembre": "09", + "octobre": "10", + "novembre": "11", + "décembre": "12", + } + + DAYS = { + "dimanche": "0", + "lundi": "1", + "mardi": "2", + "mercredi": "3", + "jeudi": "4", + "vendredi": "5", + "samedi": "6", + } + + DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M" + + @classmethod + def _apply_substitution( + cls, date: str, pattern: Optional[str], replacement: Optional[str] + ) -> str: + if not pattern or replacement is None: + return date + + # Accept PHP-like patterns with leading/trailing slashes + if len(pattern) >= 2 and pattern[0] == "/" and pattern.rfind("/") > 0: + pattern = pattern[1 : pattern.rfind("/")] + + # Convert $1 to \1 for Python + replacement = re.sub(r"\$(\d+)", r"\\\1", replacement) + try: + return re.sub(pattern, replacement, date) + except re.error: + logging.error(f"[DateParser] Could not convert {pattern} to {replacement}") + return date + + def create_timestamp( + self, + date: str, + fmt: Optional[str] = None, + pattern: Optional[str] = None, + replacement: Optional[str] = None, + ) -> int: + # Normalize and translate French day/month words + date = date.lower() + for k, v in self.DAYS.items(): + date = date.replace(k, v) + for k, v in self.MONTHS.items(): + date = date.replace(k, v) + + # Optional regex transform + date = self._apply_substitution(date, pattern, replacement) + fmt = fmt or self.DEFAULT_DATE_FORMAT + + try: + dt = datetime.strptime(date, fmt).replace(tzinfo=timezone.utc) + return int(dt.timestamp()) + except Exception as e: + logging.error( + f"[DateParser] Could not parse date '{date}' with format '{fmt}': {e}" + ) + dt = datetime.now(timezone.utc).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + return int(dt.timestamp()) diff --git a/projects/crawler/src/basango/services/http_client.py b/projects/crawler/src/basango/services/http_client.py new file mode 100644 index 0000000..d1a9f87 --- /dev/null +++ b/projects/crawler/src/basango/services/http_client.py @@ -0,0 +1,152 @@ +import random +import time +from dataclasses import dataclass +from datetime import datetime, timezone +from email.utils import parsedate_to_datetime +from typing import Any, Optional, TypeAlias + +import httpx + +from basango.core.config import ClientConfig +from basango.services.user_agents import UserAgentProvider + +HttpHeaders: TypeAlias = dict[str, str] | None +HttpParams: TypeAlias = dict[str, Any] | None +HttpData: TypeAlias = Any | None + +TRANSIENT_STATUSES = (429, 500, 502, 503, 504) + + +@dataclass +class HttpClient: + client_config: ClientConfig + user_agent_provider: UserAgentProvider | None = None + default_headers: HttpHeaders = None + + def _compute_backoff(self, attempt: int) -> float: + base = min( + self.client_config.backoff_initial + * (self.client_config.backoff_multiplier**attempt), + self.client_config.backoff_max, + ) + jitter = random.uniform(0, base * 0.25) + return base + jitter + + def _retry_delay( + self, attempt: int, response: Optional[httpx.Response] = None + ) -> float: + delay = 0.0 + + if response is not None and self.client_config.respect_retry_after: + retry_after = ( + response.headers.get("Retry-After") if response.headers else None + ) + if retry_after: + try: + delay = max(0.0, float(int(retry_after))) + except ValueError: + try: + dt = parsedate_to_datetime(retry_after) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) + delay = max(0.0, (dt - now).total_seconds()) + except Exception: # noqa: BLE001 + pass + + if delay == 0.0: + delay = self._compute_backoff(attempt) + + return delay + + def __post_init__(self) -> None: + if self.user_agent_provider is not None: + user_agent = self.user_agent_provider.get() + self._user_agent = ( + user_agent if user_agent else self.client_config.user_agent + ) + else: + provider = UserAgentProvider( + rotate=self.client_config.rotate, + fallback=self.client_config.user_agent, + ) + user_agent = provider.get() + self._user_agent = ( + user_agent if user_agent else self.client_config.user_agent + ) + + headers = {"User-Agent": self._user_agent} + + if self.default_headers: + headers.update(self.default_headers) + + self._client = httpx.Client( + follow_redirects=self.client_config.follow_redirects, + max_redirects=5, + verify=self.client_config.verify_ssl, + timeout=self.client_config.timeout, + headers=headers, + ) + + # Context manager support ------------------------------------------------- + def __enter__(self) -> "HttpClient": # noqa: D401 + return self + + def __exit__(self, exc_type, exc, tb) -> None: # noqa: D401 + self.close() + + def close(self) -> None: + try: + self._client.close() + except Exception: # noqa: BLE001 + pass + + # Core request with retries ---------------------------------------------- + def _request( + self, + method: str, + url: str, + *, + headers: HttpHeaders = None, + params: HttpParams = None, + data: Any | None = None, + json: Any | None = None, + ) -> httpx.Response: + attempt = 0 + while True: + try: + response = self._client.request( + method, url, headers=headers, params=params, data=data, json=json + ) + if ( + response.status_code in TRANSIENT_STATUSES + ) and attempt < self.client_config.max_retries: + time.sleep(self._retry_delay(attempt, response)) + attempt += 1 + continue + response.raise_for_status() + return response + except httpx.HTTPStatusError as e: + status = e.response.status_code if e.response else 0 + if ( + status in TRANSIENT_STATUSES + ) and attempt < self.client_config.max_retries: + time.sleep(self._retry_delay(attempt, e.response)) + attempt += 1 + continue + raise + except httpx.RequestError: + if attempt < self.client_config.max_retries: + time.sleep(self._compute_backoff(attempt)) + attempt += 1 + continue + raise + + # Public helpers ---------------------------------------------------------- + def get(self, url: str) -> httpx.Response: + return self._request("GET", url) + + def post( + self, url: str, data: HttpData = None, json: HttpData = None + ) -> httpx.Response: + return self._request("POST", url, data=data, json=json) diff --git a/projects/crawler/src/basango/services/open_graph.py b/projects/crawler/src/basango/services/open_graph.py new file mode 100644 index 0000000..73cec46 --- /dev/null +++ b/projects/crawler/src/basango/services/open_graph.py @@ -0,0 +1,55 @@ +import logging +from dataclasses import dataclass +from typing import Optional + +import trafilatura + +from basango.core.config import ClientConfig +from basango.services.http_client import HttpClient +from basango.services.user_agents import UserAgentProvider + + +@dataclass +class OpenGraphObject: + title: Optional[str] = None + description: Optional[str] = None + image: Optional[str] = None + url: Optional[str] = None + + +class OpenGraphProvider: + def __init__( + self, user_agent_provider: UserAgentProvider = UserAgentProvider(rotate=False) + ) -> None: + self._user_agent = user_agent_provider.og() + self._http_client = HttpClient( + client_config=ClientConfig(), + default_headers={"User-Agent": self._user_agent}, + ) + + def consume_url(self, url: str) -> OpenGraphObject | None: + try: + logging.info(f"[OpenGraphProvider] Consuming url: {url}") + html = self._http_client.get(url).text + return self.consume_html(html, url) + except Exception as e: + logging.exception(f"[OpenGraphProvider] Failed to consume url: {e}") + return None + + @classmethod + def consume_html( + cls, html: str, url: Optional[str] = None + ) -> OpenGraphObject | None: + try: + meta = trafilatura.extract_metadata(html, default_url=url) + if not meta: + return None + return OpenGraphObject( + title=meta.title or None, + description=meta.description or None, + image=meta.image or None, + url=url, + ) + except Exception as e: + logging.error(f"[OpenGraphProvider] Failed to extract metadata: {e}") + return None diff --git a/projects/crawler/src/basango/services/user_agents.py b/projects/crawler/src/basango/services/user_agents.py new file mode 100644 index 0000000..e5d134d --- /dev/null +++ b/projects/crawler/src/basango/services/user_agents.py @@ -0,0 +1,28 @@ +import random +from dataclasses import dataclass + + +@dataclass +class UserAgentProvider: + USER_AGENTS = [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_4_8; like Mac OS X) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.3638.271 Mobile Safari/537.5", + "Mozilla/5.0 (Linux; U; Linux x86_64; en-US) Gecko/20130401 Firefox/52.7", + "Mozilla/5.0 (Linux; U; Android 5.0; SM-P815 Build/LRX22G) AppleWebKit/600.4 (KHTML, like Gecko) Chrome/48.0.1562.260 Mobile Safari/600.0", + "Mozilla/5.0 (Windows; U; Windows NT 6.3;) AppleWebKit/533.34 (KHTML, like Gecko) Chrome/51.0.1883.215 Safari/533", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.3; x64; en-US Trident/4.0)", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_10_3) Gecko/20100101 Firefox/63.4", + "Mozilla/5.0 (Linux; Linux x86_64; en-US) AppleWebKit/603.50 (KHTML, like Gecko) Chrome/55.0.2226.116 Safari/601", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 7_8_3; en-US) Gecko/20100101 Firefox/68.9", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_9_8; like Mac OS X) AppleWebKit/603.34 (KHTML, like Gecko) Chrome/47.0.1126.107 Mobile Safari/602.7", + "Mozilla/5.0 (iPod; CPU iPod OS 8_2_0; like Mac OS X) AppleWebKit/601.40 (KHTML, like Gecko) Chrome/47.0.1590.178 Mobile Safari/535.2", + ] + + rotate: bool = True + fallback: str = "Basango/0.1 (+https://github.com/bernard-ng/basango)" + + def get(self) -> str: + return random.choice(self.USER_AGENTS) if self.rotate else self.fallback + + @classmethod + def og(cls) -> str: + return "facebookexternalhit/1.1" diff --git a/projects/crawler/tests/basango/domain/test_date_range.py b/projects/crawler/tests/basango/domain/test_date_range.py new file mode 100644 index 0000000..d09d821 --- /dev/null +++ b/projects/crawler/tests/basango/domain/test_date_range.py @@ -0,0 +1,57 @@ +from datetime import datetime, timezone + +import pytest + +from basango.domain import DateRange + + +def ts(y: int, m: int, d: int, hh: int = 0, mm: int = 0, ss: int = 0) -> int: + return int(datetime(y, m, d, hh, mm, ss, tzinfo=timezone.utc).timestamp()) + + +def test_from_parses_two_dates_with_default_format() -> None: + dr = DateRange.create("2024-10-01:2024-10-08") + assert dr.start == ts(2024, 10, 1) + assert dr.end == ts(2024, 10, 8) + + +def test_str_and_format_roundtrip() -> None: + dr = DateRange.create("2024-10-01:2024-10-02") + assert str(dr) == f"{ts(2024, 10, 1)}:{ts(2024, 10, 2)}" + assert dr.format("%Y-%m-%d") == "2024-10-01:2024-10-02" + + +def test_in_range_out_range_inclusive_boundaries() -> None: + dr = DateRange.create("2024-10-01:2024-10-02") + start = ts(2024, 10, 1) + end = ts(2024, 10, 2) + before = start - 1 + after = end + 1 + midday_end = ts(2024, 10, 2, 12, 0, 0) + + assert dr.in_range(start) is True + assert dr.in_range(end) is True + assert dr.out_range(before) is True + # End is at 00:00 of end day; times later that day are outside + assert dr.out_range(midday_end) is True + assert dr.out_range(after) is True + + +def test_backward_uses_days_and_next_day_end() -> None: + base = datetime(2024, 10, 31, tzinfo=timezone.utc) + dr = DateRange.backward(date=base, days=10) + assert dr.start == ts(2024, 10, 21) + assert dr.end == ts(2024, 11, 1) + + +def test_from_raises_on_invalid_separator_or_spec() -> None: + with pytest.raises(AssertionError): + DateRange.create("2024-10-01:2024-10-08", separator="") + with pytest.raises(AssertionError): + DateRange.create("2024-10-01", separator=":") + + +def test_from_accepts_python_format_string() -> None: + dr = DateRange.create("2024/10/01|2024/10/02", fmt="%Y/%m/%d", separator="|") + assert dr.start == ts(2024, 10, 1) + assert dr.end == ts(2024, 10, 2) diff --git a/projects/crawler/tests/basango/domain/test_page_range.py b/projects/crawler/tests/basango/domain/test_page_range.py new file mode 100644 index 0000000..a089766 --- /dev/null +++ b/projects/crawler/tests/basango/domain/test_page_range.py @@ -0,0 +1,19 @@ +import pytest + +from basango.domain import PageRange + + +def test_it_should_create_page_range(): + pr = PageRange.create("1:10") + assert pr.start == 1 + assert pr.end == 10 + + +def test_end_page_should_be_greater_than_start_page(): + with pytest.raises(AssertionError): + PageRange.create("10:1") + + +def test_non_negative_pages(): + with pytest.raises(AssertionError): + PageRange.create("-1:-10") diff --git a/projects/crawler/tests/basango/services/crawler/test_html_crawler.py b/projects/crawler/tests/basango/services/crawler/test_html_crawler.py new file mode 100644 index 0000000..90a87ee --- /dev/null +++ b/projects/crawler/tests/basango/services/crawler/test_html_crawler.py @@ -0,0 +1,292 @@ +from unittest.mock import patch + +import pytest +from bs4 import BeautifulSoup +from pydantic import HttpUrl + +from basango.core.config import WordPressSourceConfig +from basango.core.config.fetch_config import CrawlerConfig, ClientConfig +from basango.core.config.source_config import HtmlSourceConfig, SourceSelectors +from basango.domain import SourceKind, PageRange +from basango.services.crawler.html_crawler import HtmlCrawler + + +class TestHtmlCrawler: + """Test suite for HtmlCrawler.""" + + @pytest.fixture + def mock_client_config(self): + return ClientConfig() + + @pytest.fixture + def mock_html_source_config(self): + return HtmlSourceConfig( + source_id="test_source", + source_url=HttpUrl("https://example.com"), + pagination_template="news", + source_selectors=SourceSelectors(pagination="ul.pagination > li a"), + supports_categories=True, + ) + + @pytest.fixture + def mock_crawler_config(self, mock_html_source_config): + return CrawlerConfig(source=mock_html_source_config, category="tech") + + @pytest.fixture + def html_crawler(self, mock_crawler_config, mock_client_config): + return HtmlCrawler(mock_crawler_config, mock_client_config) + + def test_with_valid_html_source(self, html_crawler): + """Test __init__ with valid HTML source config.""" + assert html_crawler.source.source_kind == SourceKind.HTML + assert isinstance(html_crawler.source, HtmlSourceConfig) + + def test_with_invalid_source_kind_raises_error(self, mock_client_config): + """Test __init__ raises ValueError when source kind is not HTML.""" + wordpress_source = WordPressSourceConfig( + source_id="test_wordpress", + source_url=HttpUrl("https://example.com"), + ) + config = CrawlerConfig(source=wordpress_source) + + with pytest.raises( + ValueError, match="HtmlCrawler requires a source of kind HTML" + ): + HtmlCrawler(config, mock_client_config) + + def test_with_no_source_raises_error(self, mock_client_config): + """Test __init__ raises ValueError when no source is provided.""" + config = CrawlerConfig(source=None) + + with pytest.raises( + ValueError, match="HtmlCrawler requires a source of kind HTML" + ): + HtmlCrawler(config, mock_client_config) + + def test_get_pagination_returns_valid_page_range(self, html_crawler): + """Test that get_pagination returns a valid PageRange.""" + with patch.object(html_crawler, "get_last_page", return_value=5): + result = html_crawler.get_pagination() + + assert isinstance(result, PageRange) + assert result.start == 0 + assert result.end == 5 + assert str(result) == "0:5" + + def test_get_last_page_with_valid_pagination_links(self, html_crawler): + """Test get_last_page extracts page number from pagination links.""" + # Mock HTML with pagination links + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 10 + + def test_get_last_page_with_no_pagination_links(self, html_crawler): + """Test get_last_page returns 1 when no pagination links found.""" + mock_html = "
No pagination here
" + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 1 + + def test_get_last_page_with_empty_href(self, html_crawler): + """Test get_last_page returns 1 when href is empty or None.""" + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 1 + + def test_get_last_page_with_regex_extraction(self, html_crawler): + """Test get_last_page extracts page number using regex.""" + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 25 + + def test_get_last_page_with_query_parameters(self, html_crawler): + """Test get_last_page extracts page number from query parameters.""" + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 15 + + def test_get_last_page_with_invalid_page_parameter(self, html_crawler): + """Test get_last_page returns 1 when page parameter is invalid.""" + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 1 + + def test_get_last_page_with_category_support(self, html_crawler): + """Test get_last_page uses category in URL when supported.""" + mock_html = """ +
    +
  • 8
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl") as mock_crawl: + mock_crawl.return_value = mock_soup + html_crawler.get_last_page() + + # The URL construction concatenates source_url with the path + # Since the template doesn't contain {category}, it should remain unchanged + expected_url = "https://example.com/news" + mock_crawl.assert_called_once_with(expected_url) + + def test_get_last_page_with_category_template(self, mock_client_config): + """Test get_last_page uses category replacement when template contains {category}.""" + source_config = HtmlSourceConfig( + source_id="test_source", + source_url=HttpUrl("https://example.com"), + pagination_template="news/{category}", + source_selectors=SourceSelectors(pagination="ul.pagination > li a"), + supports_categories=True, + ) + crawler_config = CrawlerConfig(source=source_config, category="tech") + crawler = HtmlCrawler(crawler_config, mock_client_config) + + mock_html = """ +
    +
  • 5
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(crawler, "crawl") as mock_crawl: + mock_crawl.return_value = mock_soup + crawler.get_last_page() + + expected_url = "https://example.com/news/tech" + mock_crawl.assert_called_once_with(expected_url) + + def test_get_last_page_without_category_support(self, html_crawler): + """Test get_last_page uses default template when categories not supported.""" + # Modify source to not support categories + html_crawler.source.supports_categories = False + + mock_html = """ +
    +
  • 5
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl") as mock_crawl: + mock_crawl.return_value = mock_soup + html_crawler.get_last_page() + + # Verify the URL was constructed without category replacement + expected_url = "https://example.com/news" + mock_crawl.assert_called_once_with(expected_url) + + def test_get_last_page_without_category_in_config( + self, mock_client_config, mock_html_source_config + ): + """Test get_last_page uses default template when no category in config.""" + config = CrawlerConfig(source=mock_html_source_config, category=None) + crawler = HtmlCrawler(config, mock_client_config) + + mock_html = """ +
    +
  • 3
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(crawler, "crawl") as mock_crawl: + mock_crawl.return_value = mock_soup + crawler.get_last_page() + + # Verify the URL was constructed without category replacement + expected_url = "https://example.com/news" + mock_crawl.assert_called_once_with(expected_url) + + def test_get_last_page_with_multiple_numbers_in_href(self, html_crawler): + """Test get_last_page extracts first number when multiple numbers present.""" + mock_html = """ + + """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + # Should extract the first number found (2024) + assert result == 2024 + + def test_supports_html_source_kind(self, html_crawler): + """Test that supports method returns True for HTML source kind.""" + assert html_crawler.supports(SourceKind.HTML) is True + assert html_crawler.supports(SourceKind.WORDPRESS) is False + + def test_get_pagination_integration(self, html_crawler): + """Integration test for get_pagination calling get_last_page.""" + mock_html = """ +
    +
  • 7
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_pagination() + + assert isinstance(result, PageRange) + assert result.start == 0 + assert result.end == 7 + + def test_get_last_page_with_non_string_href(self, html_crawler): + """Test get_last_page handles non-string href attributes.""" + # Create a mock element with href as a list (AttributeValueList) + mock_html = """ +
    +
  • 5
  • +
+ """ + mock_soup = BeautifulSoup(mock_html, "html.parser") + + # Modify the href to simulate a non-string type by removing it + pagination_link = mock_soup.select("ul.pagination > li a")[-1] + # Instead of setting href to a list, let's test with missing href + del pagination_link.attrs["href"] + + with patch.object(html_crawler, "crawl", return_value=mock_soup): + result = html_crawler.get_last_page() + assert result == 1 diff --git a/projects/crawler/tests/basango/services/crawler/test_wordpress_crawler.py b/projects/crawler/tests/basango/services/crawler/test_wordpress_crawler.py new file mode 100644 index 0000000..29206cc --- /dev/null +++ b/projects/crawler/tests/basango/services/crawler/test_wordpress_crawler.py @@ -0,0 +1,240 @@ +from unittest.mock import Mock, patch + +import pytest +from pydantic import HttpUrl + +from basango.core.config.fetch_config import CrawlerConfig, ClientConfig +from basango.core.config.source_config import ( + WordPressSourceConfig, + HtmlSourceConfig, + SourceSelectors, +) +from basango.domain import SourceKind, PageRange +from basango.services.crawler.wordpress_crawler import WordpressCrawler + + +class TestWordPressCrawler: + """Test suite for WordPressCrawler.""" + + @pytest.fixture + def mock_client_config(self): + return ClientConfig() + + @pytest.fixture + def mock_wordpress_source_config(self): + return WordPressSourceConfig( + source_id="test_wordpress_source", + source_url=HttpUrl("https://example.com/"), + supports_categories=True, + categories=["tech", "news"], + ) + + @pytest.fixture + def mock_crawler_config(self, mock_wordpress_source_config): + return CrawlerConfig(source=mock_wordpress_source_config, category="tech") + + @pytest.fixture + def wordpress_crawler(self, mock_crawler_config, mock_client_config): + return WordpressCrawler(mock_crawler_config, mock_client_config) + + @pytest.fixture + def mock_response_with_headers(self): + response = Mock() + response.headers = { + WordpressCrawler.TOTAL_PAGES_HEADER: "5", + WordpressCrawler.TOTAL_POSTS_HEADER: "47", + } + return response + + def test_with_valid_wordpress_source(self, wordpress_crawler): + """Test __init__ with valid WordPress source config.""" + assert wordpress_crawler.source.source_kind == SourceKind.WORDPRESS + assert isinstance(wordpress_crawler.source, WordPressSourceConfig) + + def test_with_invalid_source_kind_raises_error(self, mock_client_config): + """Test __init__ raises ValueError when source kind is not WORDPRESS.""" + html_source = HtmlSourceConfig( + source_id="test_html", + source_url=HttpUrl("https://example.com"), + pagination_template="news", + source_selectors=SourceSelectors(), + ) + config = CrawlerConfig(source=html_source) + + with pytest.raises( + ValueError, match="WordpressCrawler requires a source of kind WORDPRESS" + ): + WordpressCrawler(config, mock_client_config) + + def test_with_no_source_raises_error(self, mock_client_config): + """Test __init__ raises ValueError when source is None.""" + config = CrawlerConfig(source=None) + + with pytest.raises( + ValueError, match="WordpressCrawler requires a source of kind WORDPRESS" + ): + WordpressCrawler(config, mock_client_config) + + def test_get_pagination_returns_valid_page_range( + self, wordpress_crawler, mock_response_with_headers + ): + """Test get_pagination returns correct PageRange from WordPress API headers.""" + with patch.object( + wordpress_crawler.client, "get", return_value=mock_response_with_headers + ): + result = wordpress_crawler.get_pagination() + + assert isinstance(result, PageRange) + assert result.start == 1 + assert result.end == 5 + assert str(result) == "1:5" + + def test_get_pagination_with_default_headers(self, wordpress_crawler): + """Test get_pagination with default headers when WordPress headers are missing.""" + mock_response = Mock() + mock_response.headers = {} # No WordPress headers + + with patch.object(wordpress_crawler.client, "get", return_value=mock_response): + result = wordpress_crawler.get_pagination() + + assert isinstance(result, PageRange) + assert result.start == 1 + assert result.end == 1 # Default when no headers + + def test_get_pagination_makes_correct_api_call(self, wordpress_crawler): + """Test get_pagination makes the correct WordPress API call.""" + mock_response = Mock() + mock_response.headers = { + WordpressCrawler.TOTAL_PAGES_HEADER: "3", + WordpressCrawler.TOTAL_POSTS_HEADER: "25", + } + + with patch.object( + wordpress_crawler.client, "get", return_value=mock_response + ) as mock_get: + wordpress_crawler.get_pagination() + + expected_url = f"{wordpress_crawler.source.source_url}wp-json/wp/v2/posts?_fields=id&per_page=100" + mock_get.assert_called_once_with(expected_url) + + def test_fetch_categories_populates_category_map(self, wordpress_crawler): + """Test _fetch_categories populates the category_map correctly.""" + mock_categories_response = Mock() + mock_categories_response.json.return_value = [ + {"id": 1, "slug": "technology", "count": 15}, + {"id": 2, "slug": "business", "count": 10}, + {"id": 3, "slug": "sports", "count": 8}, + ] + + with patch.object( + wordpress_crawler.client, "get", return_value=mock_categories_response + ): + wordpress_crawler._fetch_categories() + + assert len(wordpress_crawler.category_map) == 3 + assert wordpress_crawler.category_map[1] == "technology" + assert wordpress_crawler.category_map[2] == "business" + assert wordpress_crawler.category_map[3] == "sports" + + def test_fetch_categories_makes_correct_api_call(self, wordpress_crawler): + """Test _fetch_categories makes the correct WordPress API call.""" + mock_response = Mock() + mock_response.json.return_value = [] + + with patch.object( + wordpress_crawler.client, "get", return_value=mock_response + ) as mock_get: + wordpress_crawler._fetch_categories() + + expected_url = f"{wordpress_crawler.source.source_url}wp-json/wp/v2/categories?{WordpressCrawler.CATEGORY_QUERY}" + mock_get.assert_called_once_with(expected_url) + + def test_map_categories_with_populated_category_map(self, wordpress_crawler): + """Test _map_categories returns correct comma-separated string.""" + + # Pre-populate category map + wordpress_crawler.category_map = { + 1: "technology", + 2: "business", + 3: "sports", + 4: "lifestyle", + } + + result = wordpress_crawler._map_categories([2, 1, 4]) + + # Should be sorted by category ID + assert result == "technology,business,lifestyle" + + def test_map_categories_with_empty_category_map_fetches_categories( + self, wordpress_crawler + ): + """Test _map_categories fetches categories when category_map is empty.""" + mock_categories_response = Mock() + mock_categories_response.json.return_value = [ + {"id": 1, "slug": "tech", "count": 15}, + {"id": 2, "slug": "news", "count": 10}, + ] + + wordpress_crawler.category_map = {} + with patch.object( + wordpress_crawler.client, "get", return_value=mock_categories_response + ): + result = wordpress_crawler._map_categories([1, 2]) + + assert result == "tech,news" + assert len(wordpress_crawler.category_map) == 2 + + def test_map_categories_filters_unknown_category_ids(self, wordpress_crawler): + """Test _map_categories filters out unknown category IDs.""" + wordpress_crawler.category_map = {1: "technology", 2: "business"} + + result = wordpress_crawler._map_categories([1, 99, 2, 100]) + + # Should only include known categories + assert result == "technology,business" + + def test_map_categories_with_empty_category_list(self, wordpress_crawler): + """Test _map_categories returns empty string for empty category list.""" + wordpress_crawler.category_map = {1: "tech", 2: "news"} + + result = wordpress_crawler._map_categories([]) + + assert result == "" + + def test_map_categories_sorts_by_category_id(self, wordpress_crawler): + """Test _map_categories sorts categories by ID.""" + wordpress_crawler.category_map = {3: "charlie", 1: "alpha", 2: "beta"} + + result = wordpress_crawler._map_categories([3, 1, 2]) + + # Should be sorted by ID: 1, 2, 3 + assert result == "alpha,beta,charlie" + + def test_supports_wordpress_source_kind(self, wordpress_crawler): + """Test supports method returns True for WordPress source kind.""" + assert wordpress_crawler.supports(SourceKind.WORDPRESS) is True + assert wordpress_crawler.supports(SourceKind.HTML) is False + + @pytest.mark.parametrize( + "pages,posts,expected_start,expected_end", + [ + ("1", "10", 1, 1), + ("5", "47", 1, 5), + ("10", "100", 1, 10), + ], + ) + def test_get_pagination_with_various_header_values( + self, wordpress_crawler, pages, posts, expected_start, expected_end + ): + """Test get_pagination with various header values.""" + mock_response = Mock() + mock_response.headers = { + WordpressCrawler.TOTAL_PAGES_HEADER: pages, + WordpressCrawler.TOTAL_POSTS_HEADER: posts, + } + + with patch.object(wordpress_crawler.client, "get", return_value=mock_response): + result = wordpress_crawler.get_pagination() + + assert result.start == expected_start + assert result.end == expected_end diff --git a/projects/crawler/tests/basango/services/test_date_parser.py b/projects/crawler/tests/basango/services/test_date_parser.py new file mode 100644 index 0000000..9fb7fe6 --- /dev/null +++ b/projects/crawler/tests/basango/services/test_date_parser.py @@ -0,0 +1,70 @@ +from datetime import datetime, timezone + +import pytest + +from basango.services.date_parser import DateParser + + +@pytest.mark.parametrize( + "date_str, fmt, pattern, replacement, expected", + [ + ( + "2004-02-12T15:19:21", + "%Y-%m-%dT%H:%M:%S", + None, + None, + 1076599161, # 2004-02-12 15:19:21 UTC + ), + ( + "08/10/2024 - 00:00", + "%Y-%m-%d %H:%M", + r"/(\d{2})\/(\d{2})\/(\d{4}) - (\d{2}:\d{2})/", + r"$3-$2-$1 $4", + 1728345600, # 2024-10-08 00:00:00 UTC + ), + ( + "mar 08/10/2024 - 00:00", + "%Y-%m-%d %H:%M", + r"/\w{3} (\d{2})\/(\d{2})\/(\d{4}) - (\d{2}:\d{2})/", + r"$3-$2-$1 $4", + 1728345600, # 2024-10-08 00:00:00 UTC + ), + ( + "Mardi 8 octobre 2024 - 00:00", + "%Y-%m-%d %H:%M", + r"/(\d{1}) (\d{1,2}) (\d{2}) (\d{4}) - (\d{2}:\d{2})/", + r"$4-$3-$2 $5", + 1728345600, # 2024-10-08 00:00:00 UTC + ), + ( + "8.10.2024 00:00", + "%d.%m.%Y %H:%M", + None, + None, + 1728345600, # 2024-10-08 00:00:00 UTC + ), + ], +) +def test_create_timestamp_with_valid_dates( + date_str: str, + fmt: str | None, + pattern: str | None, + replacement: str | None, + expected: int, +) -> None: + dr = DateParser() + result = dr.create_timestamp(date_str, fmt, pattern, replacement) + assert result == expected + + +def test_create_timestamp_with_invalid_date_falls_back_to_midnight_today() -> None: + dr = DateParser() + + # Compute expected midnight (UTC) before invoking the parser to avoid edge cases. + now = datetime.now(timezone.utc) + expected_midnight = int( + now.replace(hour=0, minute=0, second=0, microsecond=0).timestamp() + ) + + result = dr.create_timestamp("invalid date string", None, None, None) + assert result == expected_midnight diff --git a/projects/crawler/tests/conftest.py b/projects/crawler/tests/conftest.py new file mode 100644 index 0000000..81c79b8 --- /dev/null +++ b/projects/crawler/tests/conftest.py @@ -0,0 +1,9 @@ +import os +import sys + + +# Ensure 'src' is on sys.path so `import basango...` works in tests +ROOT = os.path.dirname(os.path.dirname(__file__)) +SRC = os.path.join(ROOT, "src") +if SRC not in sys.path: + sys.path.insert(0, SRC) diff --git a/projects/crawler/uv.lock b/projects/crawler/uv.lock new file mode 100644 index 0000000..dae5f8b --- /dev/null +++ b/projects/crawler/uv.lock @@ -0,0 +1,891 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "bandit" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/b5/7eb834e213d6f73aace21938e5e90425c92e5f42abafaf8a6d5d21beed51/bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b", size = 4240271, upload-time = "2025-07-06T03:10:50.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/ba5f909b40ea12ec542d5d7bdd13ee31c4d65f3beed20211ef81c18fa1f3/bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", size = 133808, upload-time = "2025-07-06T03:10:49.134Z" }, +] + +[[package]] +name = "basango" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "httpx" }, + { name = "markdownify" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "readability-lxml" }, + { name = "rq" }, + { name = "selectolax" }, + { name = "trafilatura" }, + { name = "typer" }, + { name = "uv-build" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.5" }, + { name = "httpx", specifier = ">=0.27.2" }, + { name = "markdownify", specifier = ">=0.13.1" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "readability-lxml", specifier = ">=0.8.1" }, + { name = "rq", specifier = ">=2.5.0" }, + { name = "selectolax", specifier = ">=0.3.20" }, + { name = "trafilatura", specifier = ">=1.7.0" }, + { name = "typer", specifier = ">=0.16.1" }, + { name = "uv-build", specifier = ">=0.8.12,<0.9.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.8.6" }, + { name = "pyright", specifier = ">=1.1.404" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "ruff", specifier = ">=0.12.9" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "courlan" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "tld" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" }, +] + +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, +] + +[[package]] +name = "dateparser" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840, upload-time = "2025-06-26T09:29:23.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "htmldate" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "dateparser" }, + { name = "lxml" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/aaae4cab984f0b7dd0f5f1b823fa2ed2fd4a2bb50acd5bd2f0d217562678/htmldate-1.9.3.tar.gz", hash = "sha256:ac0caf4628c3ded4042011e2d60dc68dfb314c77b106587dd307a80d77e708e9", size = 44913, upload-time = "2024-12-30T12:52:35.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/49/8872130016209c20436ce0c1067de1cf630755d0443d068a5bc17fa95015/htmldate-1.9.3-py3-none-any.whl", hash = "sha256:3fadc422cf3c10a5cdb5e1b914daf37ec7270400a80a1b37e2673ff84faaaff8", size = 31565, upload-time = "2024-12-30T12:52:32.145Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "justext" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml", extra = ["html-clean"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" }, +] + +[[package]] +name = "lxml" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, + { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, + { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, +] + +[package.optional-dependencies] +html-clean = [ + { name = "lxml-html-clean" }, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622, upload-time = "2025-04-09T11:33:59.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdownify" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/1b/6f2697b51eaca81f08852fd2734745af15718fea10222a1d40f8a239c4ea/markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c", size = 18771, upload-time = "2025-08-09T17:44:15.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/e2/7af643acb4cae0741dffffaa7f3f7c9e7ab4046724543ba1777c401d821c/markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351", size = 15561, upload-time = "2025-08-09T17:44:14.074Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/88/baf6b45d064271f19fefac7def6a030a893f912f430de0024dd595ced61f/pbr-7.0.0.tar.gz", hash = "sha256:cf4127298723dafbce3afd13775ccf3885be5d3c8435751b867f9a6a10b71a39", size = 129146, upload-time = "2025-08-13T09:16:41.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/120c3e21bf3fc0ef397a3906465ee9f5c76996c52811e65455eadc12d68a/pbr-7.0.0-py2.py3-none-any.whl", hash = "sha256:b447e63a2bc04fd975fc0480b8d5ebf979179e2c0ae203bf1eff9ea20073bc38", size = 125109, upload-time = "2025-08-13T09:16:40.269Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.404" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/6e/026be64c43af681d5632722acd100b06d3d39f383ec382ff50a71a6d5bce/pyright-1.1.404.tar.gz", hash = "sha256:455e881a558ca6be9ecca0b30ce08aa78343ecc031d37a198ffa9a7a1abeb63e", size = 4065679, upload-time = "2025-08-20T18:46:14.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl", hash = "sha256:c7b7ff1fdb7219c643079e4c3e7d4125f0dafcc19d253b47e898d130ea426419", size = 5902951, upload-time = "2025-08-20T18:46:12.096Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "readability-lxml" +version = "0.8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "cssselect" }, + { name = "lxml", extra = ["html-clean"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/3e/dc87d97532ddad58af786ec89c7036182e352574c1cba37bf2bf783d2b15/readability_lxml-0.8.4.1.tar.gz", hash = "sha256:9d2924f5942dd7f37fb4da353263b22a3e877ccf922d0e45e348e4177b035a53", size = 22874, upload-time = "2025-05-03T21:11:45.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/75/2cc58965097e351415af420be81c4665cf80da52a17ef43c01ffbe2caf91/readability_lxml-0.8.4.1-py3-none-any.whl", hash = "sha256:874c0cea22c3bf2b78c7f8df831bfaad3c0a89b7301d45a188db581652b4b465", size = 19912, upload-time = "2025-05-03T21:11:43.993Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "regex" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/5a/4c63457fbcaf19d138d72b2e9b39405954f98c0349b31c601bfcb151582c/regex-2025.9.1.tar.gz", hash = "sha256:88ac07b38d20b54d79e704e38aa3bd2c0f8027432164226bdee201a1c0c9c9ff", size = 400852, upload-time = "2025-09-01T22:10:10.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/25/b2959ce90c6138c5142fe5264ee1f9b71a0c502ca4c7959302a749407c79/regex-2025.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc6834727d1b98d710a63e6c823edf6ffbf5792eba35d3fa119531349d4142ef", size = 485932, upload-time = "2025-09-01T22:08:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/6507a2a85f3f2be6643438b7bd976e67ad73223692d6988eb1ff444106d3/regex-2025.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c3dc05b6d579875719bccc5f3037b4dc80433d64e94681a0061845bd8863c025", size = 289568, upload-time = "2025-09-01T22:08:59.258Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d8/de4a4b57215d99868f1640e062a7907e185ec7476b4b689e2345487c1ff4/regex-2025.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22213527df4c985ec4a729b055a8306272d41d2f45908d7bacb79be0fa7a75ad", size = 286984, upload-time = "2025-09-01T22:09:00.835Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/e8cb403403a57ed316e80661db0e54d7aa2efcd85cb6156f33cc18746922/regex-2025.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e3f6e3c5a5a1adc3f7ea1b5aec89abfc2f4fbfba55dafb4343cd1d084f715b2", size = 797514, upload-time = "2025-09-01T22:09:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/2446f2b9585fed61faaa7e2bbce3aca7dd8df6554c32addee4c4caecf24a/regex-2025.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcb89c02a0d6c2bec9b0bb2d8c78782699afe8434493bfa6b4021cc51503f249", size = 862586, upload-time = "2025-09-01T22:09:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/82ffbe9c0992c31bbe6ae1c4b4e21269a5df2559102b90543c9b56724c3c/regex-2025.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0e2f95413eb0c651cd1516a670036315b91b71767af83bc8525350d4375ccba", size = 910815, upload-time = "2025-09-01T22:09:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d8/7303ea38911759c1ee30cc5bc623ee85d3196b733c51fd6703c34290a8d9/regex-2025.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a41dc039e1c97d3c2ed3e26523f748e58c4de3ea7a31f95e1cf9ff973fff5a", size = 802042, upload-time = "2025-09-01T22:09:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0e/6ad51a55ed4b5af512bb3299a05d33309bda1c1d1e1808fa869a0bed31bc/regex-2025.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f0b4258b161094f66857a26ee938d3fe7b8a5063861e44571215c44fbf0e5df", size = 786764, upload-time = "2025-09-01T22:09:09.362Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/394e3ffae6baa5a9217bbd14d96e0e5da47bb069d0dbb8278e2681a2b938/regex-2025.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bf70e18ac390e6977ea7e56f921768002cb0fa359c4199606c7219854ae332e0", size = 856557, upload-time = "2025-09-01T22:09:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/cd/80/b288d3910c41194ad081b9fb4b371b76b0bbfdce93e7709fc98df27b37dc/regex-2025.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b84036511e1d2bb0a4ff1aec26951caa2dea8772b223c9e8a19ed8885b32dbac", size = 849108, upload-time = "2025-09-01T22:09:12.877Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cd/5ec76bf626d0d5abdc277b7a1734696f5f3d14fbb4a3e2540665bc305d85/regex-2025.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e05dcdfe224047f2a59e70408274c325d019aad96227ab959403ba7d58d2d7", size = 788201, upload-time = "2025-09-01T22:09:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/674672f3fdead107565a2499f3007788b878188acec6d42bc141c5366c2c/regex-2025.9.1-cp313-cp313-win32.whl", hash = "sha256:3b9a62107a7441b81ca98261808fed30ae36ba06c8b7ee435308806bd53c1ed8", size = 264508, upload-time = "2025-09-01T22:09:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/83/ad/931134539515eb64ce36c24457a98b83c1b2e2d45adf3254b94df3735a76/regex-2025.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:b38afecc10c177eb34cfae68d669d5161880849ba70c05cbfbe409f08cc939d7", size = 275469, upload-time = "2025-09-01T22:09:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/24/8c/96d34e61c0e4e9248836bf86d69cb224fd222f270fa9045b24e218b65604/regex-2025.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:ec329890ad5e7ed9fc292858554d28d58d56bf62cf964faf0aa57964b21155a0", size = 268586, upload-time = "2025-09-01T22:09:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/453cbea5323b049181ec6344a803777914074b9726c9c5dc76749966d12d/regex-2025.9.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:72fb7a016467d364546f22b5ae86c45680a4e0de6b2a6f67441d22172ff641f1", size = 486111, upload-time = "2025-09-01T22:09:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0e/92577f197bd2f7652c5e2857f399936c1876978474ecc5b068c6d8a79c86/regex-2025.9.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c9527fa74eba53f98ad86be2ba003b3ebe97e94b6eb2b916b31b5f055622ef03", size = 289520, upload-time = "2025-09-01T22:09:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/af/c6/b472398116cca7ea5a6c4d5ccd0fc543f7fd2492cb0c48d2852a11972f73/regex-2025.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c905d925d194c83a63f92422af7544ec188301451b292c8b487f0543726107ca", size = 287215, upload-time = "2025-09-01T22:09:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/cf/11/f12ecb0cf9ca792a32bb92f758589a84149017467a544f2f6bfb45c0356d/regex-2025.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74df7c74a63adcad314426b1f4ea6054a5ab25d05b0244f0c07ff9ce640fa597", size = 797855, upload-time = "2025-09-01T22:09:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/46/88/bbb848f719a540fb5997e71310f16f0b33a92c5d4b4d72d4311487fff2a3/regex-2025.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f6e935e98ea48c7a2e8be44494de337b57a204470e7f9c9c42f912c414cd6f5", size = 863363, upload-time = "2025-09-01T22:09:26.705Z" }, + { url = "https://files.pythonhosted.org/packages/54/a9/2321eb3e2838f575a78d48e03c1e83ea61bd08b74b7ebbdeca8abc50fc25/regex-2025.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4a62d033cd9ebefc7c5e466731a508dfabee827d80b13f455de68a50d3c2543d", size = 910202, upload-time = "2025-09-01T22:09:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/07/d1d70835d7d11b7e126181f316f7213c4572ecf5c5c97bdbb969fb1f38a2/regex-2025.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef971ebf2b93bdc88d8337238be4dfb851cc97ed6808eb04870ef67589415171", size = 801808, upload-time = "2025-09-01T22:09:30.733Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/29e4d1bed514ef2bf3a4ead3cb8bb88ca8af94130239a4e68aa765c35b1c/regex-2025.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d936a1db208bdca0eca1f2bb2c1ba1d8370b226785c1e6db76e32a228ffd0ad5", size = 786824, upload-time = "2025-09-01T22:09:32.61Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/20d8ccb1bee460faaa851e6e7cc4cfe852a42b70caa1dca22721ba19f02f/regex-2025.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7e786d9e4469698fc63815b8de08a89165a0aa851720eb99f5e0ea9d51dd2b6a", size = 857406, upload-time = "2025-09-01T22:09:34.117Z" }, + { url = "https://files.pythonhosted.org/packages/74/fe/60c6132262dc36430d51e0c46c49927d113d3a38c1aba6a26c7744c84cf3/regex-2025.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6b81d7dbc5466ad2c57ce3a0ddb717858fe1a29535c8866f8514d785fdb9fc5b", size = 848593, upload-time = "2025-09-01T22:09:35.598Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ae/2d4ff915622fabbef1af28387bf71e7f2f4944a348b8460d061e85e29bf0/regex-2025.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cd4890e184a6feb0ef195338a6ce68906a8903a0f2eb7e0ab727dbc0a3156273", size = 787951, upload-time = "2025-09-01T22:09:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/85/37/dc127703a9e715a284cc2f7dbdd8a9776fd813c85c126eddbcbdd1ca5fec/regex-2025.9.1-cp314-cp314-win32.whl", hash = "sha256:34679a86230e46164c9e0396b56cab13c0505972343880b9e705083cc5b8ec86", size = 269833, upload-time = "2025-09-01T22:09:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/4bed4d3d0570e16771defd5f8f15f7ea2311edcbe91077436d6908956c4a/regex-2025.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:a1196e530a6bfa5f4bde029ac5b0295a6ecfaaffbfffede4bbaf4061d9455b70", size = 278742, upload-time = "2025-09-01T22:09:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3e/7d7ac6fd085023312421e0d69dfabdfb28e116e513fadbe9afe710c01893/regex-2025.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:f46d525934871ea772930e997d577d48c6983e50f206ff7b66d4ac5f8941e993", size = 271860, upload-time = "2025-09-01T22:09:42.413Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rq" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "croniter" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/1c/1c390fd8594e7367c1ee672297f7a877c0982b9c26877242c5a509ad27c0/rq-2.5.0.tar.gz", hash = "sha256:b55d328fcaeaf25823b8b8450283225f8048bd1c52abaaca192c99201ab5c687", size = 666978, upload-time = "2025-08-15T10:41:34.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/36/8917bcfc9794cbc4dd984962feb401f2dfeee0d89e1e40e3367420996f42/rq-2.5.0-py3-none-any.whl", hash = "sha256:90c74eb5b5793ff08e6c3391fd6deb7151f308ac8f04b6831580b38e90688155", size = 108377, upload-time = "2025-08-15T10:41:21.792Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "selectolax" +version = "0.3.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/8bbe1b17098b4e2a63a251361870303c37ad4c3170536277096575c24ca4/selectolax-0.3.34.tar.gz", hash = "sha256:c2cdb30b60994f1e0b74574dd408f1336d2fadd68a3ebab8ea573740dcbf17e2", size = 4706599, upload-time = "2025-08-28T23:17:44.131Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/29/eeb77d1a77599023387d4d00655960dfa3d760557b42a65ef347e29b40b0/selectolax-0.3.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2bb74e079098d758bd3d5c77b1c66c90098de305e4084b60981e561acf52c12a", size = 2001199, upload-time = "2025-08-28T23:16:59.467Z" }, + { url = "https://files.pythonhosted.org/packages/21/80/326b9dd2901b64c3c654db9e8841ddc412b9c2af0047b7d43290bbb276be/selectolax-0.3.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc39822f714e6e434ceb893e1ccff873f3f88c8db8226ba2f8a5f4a7a0e2aa29", size = 1994171, upload-time = "2025-08-28T23:17:01.206Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/1265e4f9429b3c3cf098ba08cb3264d7e16990ed3029d89e9890012aae76/selectolax-0.3.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181b67949ec23b4f11b6f2e426ba9904dd25c73d12c2cb22caf8fae21a363e99", size = 2196092, upload-time = "2025-08-28T23:17:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/1c/41/e67100abd8b0b2a5e1d5d7fa864c31d31e9a2c0bbd08ce4e951235f13143/selectolax-0.3.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b09f9d7b22bbb633966ac2019ec059caf735a5bdb4a5784bab0f4db2198fd6a", size = 2233674, upload-time = "2025-08-28T23:17:03.928Z" }, + { url = "https://files.pythonhosted.org/packages/3a/24/7ad043805c9292b4f535071c223d10aad7703b4460d68de1dce9dcf21d3f/selectolax-0.3.34-cp313-cp313-win32.whl", hash = "sha256:6e2ae8a984f82c9373e8a5ec0450f67603fde843fed73675f5187986e9e45b59", size = 1686489, upload-time = "2025-08-28T23:17:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/6b/79/62666fbfcd847c0cfc2b75b496bfa8382d765e7a3d5a2c792004760a6e61/selectolax-0.3.34-cp313-cp313-win_amd64.whl", hash = "sha256:96acd5414aaf0bb8677258ff7b0f494953b2621f71be1e3d69e01743545509ec", size = 1789924, upload-time = "2025-08-28T23:17:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b5/0bb579210a7de36d97c359016e77119513d3e810c61e99ade72089bc1b4d/selectolax-0.3.34-cp313-cp313-win_arm64.whl", hash = "sha256:1d309fd17ba72bb46a282154f75752ed7746de6f00e2c1eec4cd421dcdadf008", size = 1737480, upload-time = "2025-08-28T23:17:08.575Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5c/ab87e8ecb3c6aa1053d1c6d1eba0e47e292cc72aff0f6fbb89d920d4d87c/selectolax-0.3.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3e9c4197563c9b62b56dd7545bfd993ce071fd40b8779736e9bc59813f014c23", size = 2000587, upload-time = "2025-08-28T23:17:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/72/8e/5c08bd5628f73ab582696f8349138a569115a0fd6ab71842e4115ceec4ff/selectolax-0.3.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f96eaa0da764a4b9e08e792c0f17cce98749f1406ffad35e6d4835194570bdbf", size = 1994327, upload-time = "2025-08-28T23:17:11.709Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/02b22eff289b29ee3f869a85e4be4f7f3cf4b480d429bb18aab014848917/selectolax-0.3.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:412ce46d963444cd378e9f3197a2f30b05d858722677a361fc44ad244d2bb7db", size = 2201620, upload-time = "2025-08-28T23:17:13.538Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d3/bdd3a94bb1276be4ef4371dbfd254137b22f5c54a94d051a8d72c3956dc6/selectolax-0.3.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58dd7dc062b0424adb001817bf9b05476d165a4db1885a69cac66ca16b313035", size = 2233487, upload-time = "2025-08-28T23:17:14.921Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6a/5d551c570f29bfca5815f45fa6e6a3310cc5bc6c9b1073a968d71f73612b/selectolax-0.3.34-cp314-cp314-win32.whl", hash = "sha256:4255558fa48e3685a13f3d9dfc84586146c7b0b86e44c899ac2ac263357c987f", size = 1779755, upload-time = "2025-08-28T23:17:16.322Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/5def41b07cb3b917841022489e6bd6c3277363c23b44eca00a0ada93221c/selectolax-0.3.34-cp314-cp314-win_amd64.whl", hash = "sha256:6cbf2707d79afd7e15083f3f32c11c9b6e39a39026c8b362ce25959842a837b6", size = 1877332, upload-time = "2025-08-28T23:17:17.766Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/63da99be8f78bbfca0cb3f9ad71b7475ab97383f830c86a9abd29c6d3f25/selectolax-0.3.34-cp314-cp314-win_arm64.whl", hash = "sha256:3aa83e4d1f5f5534c9d9e44fc53640c82edc7d0eef6fca0829830cccc8df9568", size = 1831124, upload-time = "2025-08-28T23:17:19.744Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/07d8031c6c106de10ff42b4440ad7fa6a038650942bb2e194e4eb9ffec6d/selectolax-0.3.34-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:bb0b9002974ec7052f7eb1439b8e404e11a00a26affcbdd73fc53fc55beec809", size = 2023889, upload-time = "2025-08-28T23:17:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/fa8220c2eae44928b5ae73eccd44baedb328109f115c948d796c46d11048/selectolax-0.3.34-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:38e5fdffab6d08800a19671ac9641ff9ca6738fad42090f4dd0da76e4db29582", size = 2011882, upload-time = "2025-08-28T23:17:22.844Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/657089f68f59308bd90137102a7f6da0c3770128ae7245e1290e99f5a48d/selectolax-0.3.34-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:871d35e19dfde9ee83c1df139940c2e5cdf6a50ef3d147a0e9acf382b63b5b3e", size = 2221871, upload-time = "2025-08-28T23:17:24.259Z" }, + { url = "https://files.pythonhosted.org/packages/d2/56/1ad7877f9b2b12f616a8847eca0a3047c6b5ed14588f21fe1f6915357efb/selectolax-0.3.34-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f3f269bc53bc84ccc166704263712f4448130ec827a38a0df230cffe3dc46a9", size = 2241032, upload-time = "2025-08-28T23:17:25.76Z" }, + { url = "https://files.pythonhosted.org/packages/60/c0/30ce665b7382f663fdbb282748ddee392a61c85f51862776b128d8644d45/selectolax-0.3.34-cp314-cp314t-win32.whl", hash = "sha256:b957d105c2f3d86de872f61be1c9a92e1d84580a5ec89a413282f60ffb3f7bc1", size = 1828494, upload-time = "2025-08-28T23:17:27.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/11d023ad74d0d1a48cefdddbb2d00365c4d9a97735d7c24c0f206cd1babb/selectolax-0.3.34-cp314-cp314t-win_amd64.whl", hash = "sha256:9c609d639ce09154d688063bb830dc351fb944fa52629e25717dbab45ad04327", size = 1951608, upload-time = "2025-08-28T23:17:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/a5f93b84e3e6de9756dc82465c0dff57b1c8a25b1815bca0817e4342494c/selectolax-0.3.34-cp314-cp314t-win_arm64.whl", hash = "sha256:6359e94d66fb4fce9fb7c9d18252c3d8cba28b90f7412da8ce610bd77746f750", size = 1852855, upload-time = "2025-08-28T23:17:30.746Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "stevedore" +version = "5.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858, upload-time = "2025-02-20T14:03:57.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" }, +] + +[[package]] +name = "tld" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" }, +] + +[[package]] +name = "trafilatura" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "courlan" }, + { name = "htmldate" }, + { name = "justext" }, + { name = "lxml" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404, upload-time = "2024-12-03T15:23:24.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557, upload-time = "2024-12-03T15:23:21.41Z" }, +] + +[[package]] +name = "typer" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uv-build" +version = "0.8.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/1d/109827cffcdd2430783450591083a3cc9b80c8d34f962ff86e00a7d73eaf/uv_build-0.8.12.tar.gz", hash = "sha256:49666685059bf5c62e5634371b00b2012ebe3e4e4d0f479cff0400bf66ad1e3a", size = 322245, upload-time = "2025-08-18T23:59:48.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/6e/75995ef959314680fc127c3d947bc2dec1fed57a0fb400b81270dda01132/uv_build-0.8.12-py3-none-linux_armv6l.whl", hash = "sha256:03cd118ae8731aeca7994a48d6f23a5d4aacef5ee9c88bc60daf99ad698cefae", size = 1318465, upload-time = "2025-08-18T23:59:19.615Z" }, + { url = "https://files.pythonhosted.org/packages/fc/55/fa65b463af6b2c1738b81d6153975ca3b1a07056552f0993c2cf7b324018/uv_build-0.8.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:23d3d46cd619640b4b3e2977cfe629fb898586d21b8b641c9385021b1755fde5", size = 1299484, upload-time = "2025-08-18T23:59:23.737Z" }, + { url = "https://files.pythonhosted.org/packages/55/21/14fb0309c64e324f13f309460fc5a1ebf4872c1f91be89d50039c8e3a91c/uv_build-0.8.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a6676b94db118f4b3e903acf52f4acc6e8b558330d576a8438181726b47bad15", size = 1177028, upload-time = "2025-08-18T23:59:25.052Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ae/61ebacd6b43f97300409412ba99d274305919bbda367c44ea4b114c91ac5/uv_build-0.8.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2135094eab1657c121a74176a41f2ad30066962f476dac11b6c48ad6cb279392", size = 1367327, upload-time = "2025-08-18T23:59:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f7/d8c29e322ecb569774e90f3e9a1b8018465a4c88e62c6083aa91f7c53de9/uv_build-0.8.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20199b48eebf3a07046d5988b4eca8c3a8c83e50299e8e6bba085bf8f2e02611", size = 1274839, upload-time = "2025-08-18T23:59:28.034Z" }, + { url = "https://files.pythonhosted.org/packages/a3/be/63ef8eb542b98d3d4536b8519f9e4d4dbf8f52443975740be9f833fa4985/uv_build-0.8.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fdd226820cfdba719779f4ccbf594258177f67ef1907141a8b959757c26d55c", size = 1426207, upload-time = "2025-08-18T23:59:29.687Z" }, + { url = "https://files.pythonhosted.org/packages/80/b0/3ea05c1cdbc32fd13e0e97d56e8b3be4cd350ed5e6d9aa137ebe65afb5ae/uv_build-0.8.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9c76003c6af6c6949f796448458bb104c5d3f7d9a1ced3f3aeed613e2f47677e", size = 1577750, upload-time = "2025-08-18T23:59:30.983Z" }, + { url = "https://files.pythonhosted.org/packages/53/ed/1391d420efdbeb07353db1404e34830a322fe2efb64853c0d4fcda315276/uv_build-0.8.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfe32cdb94c85981597d40efc08c01ff30267db18935df50ffcef1258e091d52", size = 1481257, upload-time = "2025-08-18T23:59:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/26/28/bc6c7d00fb3a4713f85359c8687067111021542f379d5ff49136cfbe9b64/uv_build-0.8.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96aa67f8071a025b41abc661ddd0cec2731d1530095479f2b810b1c04a09252", size = 1418075, upload-time = "2025-08-18T23:59:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/23/05/39236c6e86a5d49a0d4c80064907665db34a8c180ba3110bca436ddbb8f3/uv_build-0.8.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6878f2179dafb1053a413ad41f2f9640655489972bec6211aaf8d492b49614af", size = 1421678, upload-time = "2025-08-18T23:59:35.653Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/731bec1f5955de6ea33cffcf568a81375dfe80e17215dd66cdf659fcd28c/uv_build-0.8.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7bce23578e8abbb40fd70aebed1afd27d132915e451551322f10aa304dd8bf26", size = 1365561, upload-time = "2025-08-18T23:59:37.664Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b8/1219fa9d21c1deacd8d8b9f4b4193596ea6cdbef718e299b371354c19897/uv_build-0.8.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a9c57674dd757f8208b6e4929abd5bcb6b63bab1ea5fab0f3feaa4c40236c7dd", size = 1375369, upload-time = "2025-08-18T23:59:38.948Z" }, + { url = "https://files.pythonhosted.org/packages/ae/31/700da060b59d4bb163f146d2f673292937595efa77e71a73842b945e49c7/uv_build-0.8.12-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:021a75dec60bf14f0bebdf10aafa08a03ad5d2c9bfd82565b77ac56a82316911", size = 1290573, upload-time = "2025-08-18T23:59:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9b/711a875605583bed36ff18ccd5351f2582cafedef4720a667e90e6023e3a/uv_build-0.8.12-py3-none-musllinux_1_1_i686.whl", hash = "sha256:2884df52ef9c47bccebf0f616380b281078a4e50fd29a6d44e841f2e2532f687", size = 1380155, upload-time = "2025-08-18T23:59:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/67/31/4b0269dbebd18e406ec565ead0c0b05909d255cd4650dfac1b198542e92d/uv_build-0.8.12-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c8072519032f4c90e36ea4650fa4a86a30a6d3355082a31f996e7c9e6a6e92f6", size = 1462583, upload-time = "2025-08-18T23:59:43.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/2d47a047109ac53d40c3912d15a4aeadfa67c3937dcd7cd854f865e25fef/uv_build-0.8.12-py3-none-win32.whl", hash = "sha256:45830715e022b85994c06db03ea1a337684cef441ab3ecd38d4b03071845f662", size = 1251560, upload-time = "2025-08-18T23:59:44.425Z" }, + { url = "https://files.pythonhosted.org/packages/c6/11/d8a0a1b87e4cca37abbeb3756119260d9f84bc954cec0bfb04447138a19e/uv_build-0.8.12-py3-none-win_amd64.whl", hash = "sha256:b549a205e1a7487f278baa5fd59dae6901955be7af024dea9d17615e64312cf4", size = 1329565, upload-time = "2025-08-18T23:59:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/c2b30dd90d9fbd0ddef6db4b0fc60e80643d0ef2501229078dcff79067f1/uv_build-0.8.12-py3-none-win_arm64.whl", hash = "sha256:f0c05d62de6c8cb59eb686ac8c6a4e9549f81603864df4f853923eefc850f674", size = 1236604, upload-time = "2025-08-18T23:59:47.094Z" }, +] diff --git a/projects/mobile/.env b/projects/mobile/.env new file mode 100644 index 0000000..bf1805b --- /dev/null +++ b/projects/mobile/.env @@ -0,0 +1,3 @@ +EXPO_PUBLIC_API_URL=http://172.20.10.2:8000/api +EXPO_PUBLIC_SENTRY_DSN= +SENTRY_AUTH_TOKEN= diff --git a/projects/mobile/.gitignore b/projects/mobile/.gitignore new file mode 100644 index 0000000..9393cd6 --- /dev/null +++ b/projects/mobile/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.idea +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +.env.local diff --git a/projects/mobile/.husky/commit-msg b/projects/mobile/.husky/commit-msg new file mode 100644 index 0000000..e81b051 --- /dev/null +++ b/projects/mobile/.husky/commit-msg @@ -0,0 +1 @@ +npx commitlint --edit "$1" diff --git a/projects/mobile/.husky/pre-commit b/projects/mobile/.husky/pre-commit new file mode 100644 index 0000000..62eae82 --- /dev/null +++ b/projects/mobile/.husky/pre-commit @@ -0,0 +1 @@ +bun run check-types && bun run check && bun run lint:check diff --git a/projects/mobile/.prettierignore b/projects/mobile/.prettierignore new file mode 100644 index 0000000..6d06de5 --- /dev/null +++ b/projects/mobile/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage +dist +android +ios diff --git a/projects/mobile/.prettierrc b/projects/mobile/.prettierrc new file mode 100644 index 0000000..6e1efde --- /dev/null +++ b/projects/mobile/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "tabWidth": 4, + "useTabs": false, + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/projects/mobile/README.md b/projects/mobile/README.md new file mode 100644 index 0000000..1921903 --- /dev/null +++ b/projects/mobile/README.md @@ -0,0 +1,26 @@ +# Mobile + +![Coding Standard](https://github.com/bernard-ng/drc-news-app/actions/workflows/quality.yaml/badge.svg) + +--- + +### Get started + +1. Install dependencies + +```bash +bun install +``` + +2. Start the app + +```bash +bunx expo start +``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go) diff --git a/projects/mobile/android/.gitignore b/projects/mobile/android/.gitignore new file mode 100644 index 0000000..8a6be07 --- /dev/null +++ b/projects/mobile/android/.gitignore @@ -0,0 +1,16 @@ +# OSX +# +.DS_Store + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ + +# Bundle artifacts +*.jsbundle diff --git a/projects/mobile/android/app/build.gradle b/projects/mobile/android/app/build.gradle new file mode 100644 index 0000000..cca22f6 --- /dev/null +++ b/projects/mobile/android/app/build.gradle @@ -0,0 +1,178 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + // Use Expo CLI to bundle the app, this ensures the Metro config + // works correctly with Expo projects. + cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle") + +android { + ndkVersion rootProject.ext.ndkVersion + + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace 'dev.ngandu.congonews' + defaultConfig { + applicationId 'dev.ngandu.congonews' + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) + } + } + packagingOptions { + jniLibs { + useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + } + } + androidResources { + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' + } +} + +// Apply static values from `gradle.properties` to the `android.packagingOptions` +// Accepts values in comma delimited lists, example: +// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini +["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> + // Split option: 'foo,bar' -> ['foo', 'bar'] + def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); + // Trim all elements in place. + for (i in 0.. 0) { + println "android.packagingOptions.$prop += $options ($options.length)" + // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' + options.each { + android.packagingOptions[prop] += it + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; + def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; + def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + + if (isGifEnabled) { + // For animated gif support + implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") + } + + if (isWebpEnabled) { + // For webp support + implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") + if (isWebpAnimatedEnabled) { + // Animated webp support + implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") + } + } + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/projects/mobile/android/app/debug.keystore b/projects/mobile/android/app/debug.keystore new file mode 100644 index 0000000..364e105 Binary files /dev/null and b/projects/mobile/android/app/debug.keystore differ diff --git a/projects/mobile/android/app/proguard-rules.pro b/projects/mobile/android/app/proguard-rules.pro new file mode 100644 index 0000000..551eb41 --- /dev/null +++ b/projects/mobile/android/app/proguard-rules.pro @@ -0,0 +1,14 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# react-native-reanimated +-keep class com.swmansion.reanimated.** { *; } +-keep class com.facebook.react.turbomodule.** { *; } + +# Add any project specific keep options here: diff --git a/projects/mobile/android/app/src/debug/AndroidManifest.xml b/projects/mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3ec2507 --- /dev/null +++ b/projects/mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/projects/mobile/android/app/src/main/AndroidManifest.xml b/projects/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6c48780 --- /dev/null +++ b/projects/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainActivity.kt b/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainActivity.kt new file mode 100644 index 0000000..7e9679c --- /dev/null +++ b/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainActivity.kt @@ -0,0 +1,65 @@ +package dev.ngandu.congonews +import expo.modules.splashscreen.SplashScreenManager + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + // setTheme(R.style.AppTheme); + // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af + SplashScreenManager.registerOnActivity(this) + // @generated end expo-splashscreen + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainApplication.kt b/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainApplication.kt new file mode 100644 index 0000000..7943cbf --- /dev/null +++ b/projects/mobile/android/app/src/main/java/dev/ngandu/congonews/MainApplication.kt @@ -0,0 +1,57 @@ +package dev.ngandu.congonews + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List { + val packages = PackageList(this).packages + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return packages + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/projects/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/projects/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..e36d150 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/projects/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/projects/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..4ae854c Binary files /dev/null and b/projects/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/projects/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/projects/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..31f9fc4 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/projects/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/projects/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..36b8b98 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/projects/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/projects/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..69f33b9 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/projects/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml b/projects/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..883b2a0 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml b/projects/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000..5c25e72 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/projects/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..b27f12d Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b8da014 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6834498 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..609192f Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0733132 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6ac3826 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..809b17f Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..2704461 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..37a0e2b Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..7686db2 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cda0ed2 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6879dd3 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..4a9bf00 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b6dfacb Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..19ffcf9 Binary files /dev/null and b/projects/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/projects/mobile/android/app/src/main/res/values-night/colors.xml b/projects/mobile/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/values/colors.xml b/projects/mobile/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f387b90 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #ffffff + #ffffff + #023c69 + #ffffff + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/values/strings.xml b/projects/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c651223 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + congonews + automatic + contain + false + \ No newline at end of file diff --git a/projects/mobile/android/app/src/main/res/values/styles.xml b/projects/mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..6bc0170 --- /dev/null +++ b/projects/mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/projects/mobile/android/build.gradle b/projects/mobile/android/build.gradle new file mode 100644 index 0000000..abbcb8e --- /dev/null +++ b/projects/mobile/android/build.gradle @@ -0,0 +1,41 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' + minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') + compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') + targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') + kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' + + ndkVersion = "26.1.10909125" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath('com.android.tools.build:gradle') + classpath('com.facebook.react:react-native-gradle-plugin') + classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') + } +} + +apply plugin: "com.facebook.react.rootproject" + +allprojects { + repositories { + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) + } + maven { + // Android JSC is installed from npm + url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) + } + + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + } +} diff --git a/projects/mobile/android/gradle.properties b/projects/mobile/android/gradle.properties new file mode 100644 index 0000000..04490cd --- /dev/null +++ b/projects/mobile/android/gradle.properties @@ -0,0 +1,58 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Enable AAPT2 PNG crunching +android.enablePngCrunchInReleaseBuilds=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Enable GIF support in React Native images (~200 B increase) +expo.gif.enabled=true +# Enable webp support in React Native images (~85 KB increase) +expo.webp.enabled=true +# Enable animated webp support (~3.4 MB increase) +# Disabled by default because iOS doesn't support animated webp +expo.webp.animated=false + +# Enable network inspector +EX_DEV_CLIENT_NETWORK_INSPECTOR=true + +# Use legacy packaging to compress native libraries in the resulting APK. +expo.useLegacyPackaging=false + +android.extraMavenRepos=[] \ No newline at end of file diff --git a/projects/mobile/android/gradle/wrapper/gradle-wrapper.jar b/projects/mobile/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/projects/mobile/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/projects/mobile/android/gradle/wrapper/gradle-wrapper.properties b/projects/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..af6c36a --- /dev/null +++ b/projects/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=file:///Users/bernard-ng/gradle/gradle-8.10.2-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/projects/mobile/android/gradlew b/projects/mobile/android/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/projects/mobile/android/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/projects/mobile/android/gradlew.bat b/projects/mobile/android/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/projects/mobile/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/projects/mobile/android/sentry.properties b/projects/mobile/android/sentry.properties new file mode 100644 index 0000000..8757b6a --- /dev/null +++ b/projects/mobile/android/sentry.properties @@ -0,0 +1,4 @@ +defaults.url=https://glitchtip.devscast.tech/ +defaults.org=devscast-software +defaults.project=drc-news-app +# Using SENTRY_AUTH_TOKEN environment variable \ No newline at end of file diff --git a/projects/mobile/android/settings.gradle b/projects/mobile/android/settings.gradle new file mode 100644 index 0000000..46f19fc --- /dev/null +++ b/projects/mobile/android/settings.gradle @@ -0,0 +1,38 @@ +pluginManagement { + includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString()) +} +plugins { id("com.facebook.react.settings") } + +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { + ex.autolinkLibrariesFromCommand() + } else { + def command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'android' + ].toList() + ex.autolinkLibrariesFromCommand(command) + } +} + +rootProject.name = 'congonews' + +dependencyResolutionManagement { + versionCatalogs { + reactAndroidLibs { + from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) + } + } +} + +apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); +useExpoModules() + +include ':app' +includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) diff --git a/projects/mobile/app.json b/projects/mobile/app.json new file mode 100644 index 0000000..8c6e550 --- /dev/null +++ b/projects/mobile/app.json @@ -0,0 +1,64 @@ +{ + "expo": { + "name": "congonews", + "slug": "congonews", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./src/assets/images/logo.png", + "scheme": "congonews", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "githubUrl": "https://github.com/bernard-ng/drc-news-app", + "ios": { + "supportsTablet": true, + "bundleIdentifier": "dev.ngandu.congonews" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./src/assets/images/logo.png", + "backgroundColor": "#ffffff", + "package": "dev.ngandu.congonews" + }, + "package": "dev.ngandu.congonews" + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./src/assets/images/logo.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./src/assets/images/logo.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + ], + "expo-build-properties", + "expo-localization", + [ + "@sentry/react-native/expo", + { + "url": "https://glitchtip.devscast.tech/", + "project": "drc-news-app", + "organization": "devscast-software" + } + ] + ], + "experiments": { + "typedRoutes": true + }, + "extra": { + "router": { + "origin": false + }, + "eas": { + "projectId": "57281e7a-46e3-4aac-8715-5165fa0bf560" + } + }, + "owner": "bernard-ng" + } +} diff --git a/projects/mobile/babel.config.js b/projects/mobile/babel.config.js new file mode 100644 index 0000000..b21c172 --- /dev/null +++ b/projects/mobile/babel.config.js @@ -0,0 +1,7 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [["babel-preset-expo", { jsxRuntime: "automatic" }]], + plugins: ["react-native-reanimated/plugin"], + }; +}; diff --git a/projects/mobile/bun.lock b/projects/mobile/bun.lock new file mode 100644 index 0000000..c51a6e3 --- /dev/null +++ b/projects/mobile/bun.lock @@ -0,0 +1,3959 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "drc-news", + "dependencies": { + "@expo-google-fonts/inter": "^0.3.0", + "@expo/vector-icons": "^14.0.2", + "@hookform/resolvers": "^5.1.1", + "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/native": "^7.0.14", + "@sentry/react-native": "^6.15.1", + "@shopify/flash-list": "1.7.3", + "@tamagui/colors": "^1.126.1", + "@tamagui/config": "^1.126.1", + "@tamagui/linear-gradient": "^1.126.1", + "@tamagui/lucide-icons": "^1.129.2", + "@tamagui/theme-builder": "^1.126.1", + "@tanstack/react-query": "^5.74.4", + "axios": "^1.9.0", + "date-fns": "^4.1.0", + "expo": "~52.0.46", + "expo-blur": "~14.0.3", + "expo-build-properties": "~0.13.2", + "expo-constants": "~17.0.8", + "expo-dev-client": "~5.0.20", + "expo-font": "~13.0.4", + "expo-haptics": "~14.0.1", + "expo-linear-gradient": "~14.0.2", + "expo-linking": "~7.0.5", + "expo-localization": "~16.0.1", + "expo-network": "~7.0.5", + "expo-router": "~4.0.20", + "expo-secure-store": "^14.0.1", + "expo-splash-screen": "~0.29.24", + "expo-status-bar": "~2.0.1", + "expo-symbols": "~0.2.2", + "expo-system-ui": "~4.0.9", + "expo-web-browser": "~14.0.2", + "joi": "^17.13.3", + "qs": "^6.14.0", + "react": "18.3.1", + "react-content-loader": "^7.0.2", + "react-dom": "18.3.1", + "react-hook-form": "^7.58.1", + "react-native": "0.76.9", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", + "react-native-safe-area-context": "^5.4.0", + "react-native-screens": "~4.4.0", + "react-native-svg": "^15.11.2", + "react-native-toast-message": "^2.3.0", + "react-native-web": "~0.19.13", + "react-native-webview": "13.12.5", + "tamagui": "^1.126.1", + "zustand": "^5.0.5", + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@types/jest": "^29.5.12", + "@types/qs": "^6.9.18", + "@types/react": "~18.3.12", + "@types/react-test-renderer": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "commitizen": "^4.3.1", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^9.26.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-expo": "^9.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-native": "^5.0.0", + "eslint-plugin-unused-imports": "^4.1.4", + "husky": "^9.1.7", + "jest": "^29.2.1", + "jest-expo": "~52.0.6", + "lint-staged": "^16.0.0", + "prettier": "^3.5.3", + "react-test-renderer": "18.3.1", + "typescript": "^5.3.3", + }, + }, + }, + "overrides": { + "globals": "14.0.0", + }, + "packages": { + "@0no-co/graphql.web": ["@0no-co/graphql.web@1.1.2", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-N2NGsU5FLBhT8NZ+3l2YrzZSHITjNXNuDhC4iDiikv0IujaJ0Xc6xIxQZ/Ek3Cb+rgPjnLHYyJm11tInuJn+cw=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="], + + "@babel/core": ["@babel/core@7.27.1", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-module-transforms": "^7.27.1", "@babel/helpers": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ=="], + + "@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="], + + "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ=="], + + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.27.1", "", { "dependencies": { "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ=="], + + "@babel/helpers": ["@babel/helpers@7.27.1", "", { "dependencies": { "@babel/template": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ=="], + + "@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="], + + "@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA=="], + + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], + + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], + + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw=="], + + "@babel/plugin-proposal-class-properties": ["@babel/plugin-proposal-class-properties@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ=="], + + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg=="], + + "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw=="], + + "@babel/plugin-proposal-nullish-coalescing-operator": ["@babel/plugin-proposal-nullish-coalescing-operator@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA=="], + + "@babel/plugin-proposal-optional-chaining": ["@babel/plugin-proposal-optional-chaining@7.21.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA=="], + + "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], + + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="], + + "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], + + "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg=="], + + "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA=="], + + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + + "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], + + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], + + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA=="], + + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], + + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], + + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw=="], + + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], + + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA=="], + + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA=="], + + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q=="], + + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], + + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], + + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], + + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ=="], + + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], + + "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-flow": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg=="], + + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], + + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], + + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], + + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], + + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw=="], + + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], + + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], + + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA=="], + + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], + + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], + + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], + + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], + + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], + + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.27.2", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-parameters": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g=="], + + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], + + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], + + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="], + + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg=="], + + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], + + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], + + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], + + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], + + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], + + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw=="], + + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], + + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], + + "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TqGF3desVsTcp3WrJGj4HfKokfCXCLcHpt4PJF0D8/iT6LPd9RS82Upw3KPeyr6B22Lfd3DO8MVrmp0oRkUDdw=="], + + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], + + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], + + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], + + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], + + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg=="], + + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], + + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], + + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], + + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], + + "@babel/preset-env": ["@babel/preset-env@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.27.1", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-classes": "^7.27.1", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.27.1", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-parameters": "^7.27.1", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.27.1", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ=="], + + "@babel/preset-flow": ["@babel/preset-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-flow-strip-types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg=="], + + "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], + + "@babel/preset-react": ["@babel/preset-react@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.27.1", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + + "@babel/register": ["@babel/register@7.27.1", "", { "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K13lQpoV54LATKkzBpBAEu1GGSIRzxR9f4IN4V8DCDgiUMo2UDGagEZr3lPeVNJPLkWUi5JE4hCHKneVTwQlYQ=="], + + "@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="], + + "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="], + + "@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + + "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], + + "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], + + "@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="], + + "@commitlint/ensure": ["@commitlint/ensure@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw=="], + + "@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.1", "", {}, "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA=="], + + "@commitlint/format": ["@commitlint/format@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "chalk": "^5.3.0" } }, "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw=="], + + "@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "semver": "^7.6.0" } }, "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg=="], + + "@commitlint/lint": ["@commitlint/lint@19.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.1", "@commitlint/parse": "^19.8.1", "@commitlint/rules": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw=="], + + "@commitlint/load": ["@commitlint/load@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/execute-rule": "^19.8.1", "@commitlint/resolve-extends": "^19.8.1", "@commitlint/types": "^19.8.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A=="], + + "@commitlint/message": ["@commitlint/message@19.8.1", "", {}, "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg=="], + + "@commitlint/parse": ["@commitlint/parse@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw=="], + + "@commitlint/read": ["@commitlint/read@19.8.1", "", { "dependencies": { "@commitlint/top-level": "^19.8.1", "@commitlint/types": "^19.8.1", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" } }, "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ=="], + + "@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/types": "^19.8.1", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg=="], + + "@commitlint/rules": ["@commitlint/rules@19.8.1", "", { "dependencies": { "@commitlint/ensure": "^19.8.1", "@commitlint/message": "^19.8.1", "@commitlint/to-lines": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw=="], + + "@commitlint/to-lines": ["@commitlint/to-lines@19.8.1", "", {}, "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg=="], + + "@commitlint/top-level": ["@commitlint/top-level@19.8.1", "", { "dependencies": { "find-up": "^7.0.0" } }, "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw=="], + + "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + + "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], + + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], + + "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], + + "@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.26.0", "", {}, "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="], + + "@expo-google-fonts/inter": ["@expo-google-fonts/inter@0.3.0", "", {}, "sha512-x77HsdpizmZFNZ8nMd7hsNah/MyxhLmihZQEyIkB473jskBcXIpsyf+tVVLXvXEhtnpGx8zZ2XAe9u+i0qvIEw=="], + + "@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="], + + "@expo/cli": ["@expo/cli@0.22.26", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.12", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "~8.2.0", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.9", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q=="], + + "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], + + "@expo/config": ["@expo/config@10.0.11", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~9.0.17", "@expo/config-types": "^52.0.5", "@expo/json-file": "^9.0.2", "deepmerge": "^4.3.1", "getenv": "^1.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww=="], + + "@expo/config-plugins": ["@expo/config-plugins@9.0.17", "", { "dependencies": { "@expo/config-types": "^52.0.5", "@expo/json-file": "~9.0.2", "@expo/plist": "^0.2.2", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^1.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg=="], + + "@expo/config-types": ["@expo/config-types@52.0.5", "", {}, "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA=="], + + "@expo/devcert": ["@expo/devcert@1.2.0", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0", "glob": "^10.4.2" } }, "sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA=="], + + "@expo/env": ["@expo/env@0.4.2", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A=="], + + "@expo/fingerprint": ["@expo/fingerprint@0.11.11", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-gNyn1KnAOpEa8gSNsYqXMTcq0fSwqU/vit6fP5863vLSKxHm/dNt/gm/uZJxrRZxKq71KUJWF6I7d3z8qIfq5g=="], + + "@expo/image-utils": ["@expo/image-utils@0.6.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "fs-extra": "9.0.0", "getenv": "^1.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew=="], + + "@expo/json-file": ["@expo/json-file@9.1.4", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-7Bv86X27fPERGhw8aJEZvRcH9sk+9BenDnEmrI3ZpywKodYSBgc8lX9Y32faNVQ/p0YbDK9zdJ0BfAKNAOyi0A=="], + + "@expo/metro-config": ["@expo/metro-config@0.19.12", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.11", "@expo/env": "~0.4.2", "@expo/json-file": "~9.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-fhT3x1ikQWHpZgw7VrEghBdscFPz1laRYa8WcVRB18nTTqorF6S8qPYslkJu1faEziHZS7c2uyDzTYnrg/CKbg=="], + + "@expo/metro-runtime": ["@expo/metro-runtime@4.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw=="], + + "@expo/osascript": ["@expo/osascript@2.2.4", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "exec-async": "^2.2.0" } }, "sha512-Q+Oyj+1pdRiHHpev9YjqfMZzByFH8UhKvSszxa0acTveijjDhQgWrq4e9T/cchBHi0GWZpGczWyiyJkk1wM1dg=="], + + "@expo/package-manager": ["@expo/package-manager@1.8.4", "", { "dependencies": { "@expo/json-file": "^9.1.4", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-8H8tLga/NS3iS7QaX/NneRPqbObnHvVCfMCo0ShudreOFmvmgqhYjRlkZTRstSyFqefai8ONaT4VmnLHneRYYg=="], + + "@expo/plist": ["@expo/plist@0.2.2", "", { "dependencies": { "@xmldom/xmldom": "~0.7.7", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g=="], + + "@expo/prebuild-config": ["@expo/prebuild-config@8.2.0", "", { "dependencies": { "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/config-types": "^52.0.5", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.9", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-CxiPpd980s0jyxi7eyN3i/7YKu3XL+8qPjBZUCYtc0+axpGweqIkq2CslyLSKHyqVyH/zlPkbVgWdyiYavFS5Q=="], + + "@expo/rudder-sdk-node": ["@expo/rudder-sdk-node@1.1.1", "", { "dependencies": { "@expo/bunyan": "^4.0.0", "@segment/loosely-validate-event": "^2.0.0", "fetch-retry": "^4.1.1", "md5": "^2.2.1", "node-fetch": "^2.6.1", "remove-trailing-slash": "^0.1.0", "uuid": "^8.3.2" } }, "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ=="], + + "@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="], + + "@expo/server": ["@expo/server@0.5.3", "", { "dependencies": { "abort-controller": "^3.0.0", "debug": "^4.3.4", "source-map-support": "~0.5.21", "undici": "^6.18.2" } }, "sha512-WXsWzeBs5v/h0PUfHyNLLz07rwwO5myQ1A5DGYewyyGLmsyl61yVCe8AgAlp1wkiMsqhj2hZqI2u3K10QnCMrQ=="], + + "@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="], + + "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], + + "@expo/vector-icons": ["@expo/vector-icons@14.1.0", "", { "peerDependencies": { "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ=="], + + "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="], + + "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="], + + "@floating-ui/react": ["@floating-ui/react@0.27.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + + "@floating-ui/react-native": ["@floating-ui/react-native@0.10.7", "", { "dependencies": { "@floating-ui/core": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.64.0" } }, "sha512-deSecLPrdfl8RL1yyNJlbgqDDZFPuhBtJhY2aTnOZOoJWaal2vVOad9EBVIa0QV/YordgTyFPgDI8oLfyLZuZA=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@hookform/resolvers": ["@hookform/resolvers@5.1.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jest/console": ["@jest/console@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg=="], + + "@jest/core": ["@jest/core@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.7.0", "jest-config": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-resolve-dependencies": "^29.7.0", "jest-runner": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg=="], + + "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], + + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], + + "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], + + "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], + + "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + + "@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], + + "@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="], + + "@jest/test-result": ["@jest/test-result@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA=="], + + "@jest/test-sequencer": ["@jest/test-sequencer@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "slash": "^3.0.0" } }, "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw=="], + + "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.11.2", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ=="], + + "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], + + "@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="], + + "@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="], + + "@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="], + + "@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="], + + "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + + "@npmcli/fs": ["@npmcli/fs@3.1.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pkgr/core": ["@pkgr/core@0.2.4", "", {}, "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@react-native/assets-registry": ["@react-native/assets-registry@0.76.9", "", {}, "sha512-pN0Ws5xsjWOZ8P37efh0jqHHQmq+oNGKT4AyAoKRpxBDDDmlAmpaYjer9Qz7PpDKF+IUyRjF/+rBsM50a8JcUg=="], + + "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.9", "", { "dependencies": { "@react-native/codegen": "0.76.9" } }, "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ=="], + + "@react-native/babel-preset": ["@react-native/babel-preset@0.76.9", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.76.9", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw=="], + + "@react-native/codegen": ["@react-native/codegen@0.76.9", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.23.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA=="], + + "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.76.9", "", { "dependencies": { "@react-native/dev-middleware": "0.76.9", "@react-native/metro-babel-transformer": "0.76.9", "chalk": "^4.0.0", "execa": "^5.1.1", "invariant": "^2.2.4", "metro": "^0.81.0", "metro-config": "^0.81.0", "metro-core": "^0.81.0", "node-fetch": "^2.2.0", "readline": "^1.3.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*" }, "optionalPeers": ["@react-native-community/cli"] }, "sha512-08jx8ixCjjd4jNQwNpP8yqrjrDctN2qvPPlf6ebz1OJQk8e1sbUl3wVn1zhhMvWrYcaraDnatPb5uCPq+dn3NQ=="], + + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.76.9", "", {}, "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA=="], + + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.76.9", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.76.9", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA=="], + + "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.76.9", "", {}, "sha512-uGzp3dL4GfNDz+jOb8Nik1Vrfq1LHm0zESizrGhHACFiFlUSflVAnWuUAjlZlz5XfLhzGVvunG4Vdrpw8CD2ng=="], + + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.76.9", "", {}, "sha512-s6z6m8cK4SMjIX1hm8LT187aQ6//ujLrjzDBogqDCYXRbfjbAYovw5as/v2a2rhUIyJbS3UjokZm3W0H+Oh/RQ=="], + + "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.76.9", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.76.9", "hermes-parser": "0.23.1", "nullthrows": "^1.1.1" } }, "sha512-HGq11347UHNiO/NvVbAO35hQCmH8YZRs7in7nVq7SL99pnpZK4WXwLdAXmSuwz5uYqOuwnKYDlpadz8fkE94Mg=="], + + "@react-native/normalize-color": ["@react-native/normalize-color@2.1.0", "", {}, "sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA=="], + + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.8", "", {}, "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA=="], + + "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.76.9", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-2neUfZKuqMK2LzfS8NyOWOyWUJOWgDym5fUph6fN9qF+LNPjAvnc4Zr9+o+59qjNu/yXwQgVMWNU4+8WJuPVWw=="], + + "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.3.13", "", { "dependencies": { "@react-navigation/elements": "^2.4.2", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.9", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-J3MWXBJc3y6hefZNRqdj/JD4nzIDLzZL5GIYj89pR6oRf2Iibz9t1qV7yzxEc1KOaNDkXVZ/5U16PArEJFfykQ=="], + + "@react-navigation/core": ["@react-navigation/core@7.9.2", "", { "dependencies": { "@react-navigation/routers": "^7.3.7", "escape-string-regexp": "^4.0.0", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.3", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-lqCyKMWWaSwGK4VV3wRXXEKvl5IKrVH207Kp77TLCnITnd4KQIdgjzzJ/Pr62ugki3VTAErq1vg0yRlcXciCbg=="], + + "@react-navigation/elements": ["@react-navigation/elements@2.4.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.9", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-cudKLsRtOB+i8iDzfBKypdqiHsDy1ruqCfYAtwKEclDmLsxu3/90YXoBtoPyFNyIpsn3GtsJzZsrYWQh78xSWg=="], + + "@react-navigation/native": ["@react-navigation/native@7.1.9", "", { "dependencies": { "@react-navigation/core": "^7.9.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.3" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-/A0oBwZIeD23o4jsnB0fEyKmKS+l4LAbJP/ioVvsGEubGp+sc5ouQNranOh7JwR0R1eX0MjcsLKprEwB+nztdw=="], + + "@react-navigation/native-stack": ["@react-navigation/native-stack@7.3.13", "", { "dependencies": { "@react-navigation/elements": "^2.4.2", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.9", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-udH+HumX0PmaT6QQTqjU3ciiCwifBGtnw1+6B1bVEDw83q80WHotlMitaf8Enbuf7oWrxwB+Eow4tV5MJXgQtQ=="], + + "@react-navigation/routers": ["@react-navigation/routers@7.3.7", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-5ffgrefOs2zWqcCVX+OKn+RDx0puopQtxqetegFrTfWQ6pGXdY/5v4kBpPwaOFrNEeE/LPbHt9IJaJuvyhB7RA=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@segment/loosely-validate-event": ["@segment/loosely-validate-event@2.0.0", "", { "dependencies": { "component-type": "^1.2.1", "join-component": "^1.1.0" } }, "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw=="], + + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@8.54.0", "", { "dependencies": { "@sentry/core": "8.54.0" } }, "sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ=="], + + "@sentry-internal/feedback": ["@sentry-internal/feedback@8.54.0", "", { "dependencies": { "@sentry/core": "8.54.0" } }, "sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ=="], + + "@sentry-internal/replay": ["@sentry-internal/replay@8.54.0", "", { "dependencies": { "@sentry-internal/browser-utils": "8.54.0", "@sentry/core": "8.54.0" } }, "sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg=="], + + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@8.54.0", "", { "dependencies": { "@sentry-internal/replay": "8.54.0", "@sentry/core": "8.54.0" } }, "sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw=="], + + "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@3.5.0", "", {}, "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw=="], + + "@sentry/browser": ["@sentry/browser@8.54.0", "", { "dependencies": { "@sentry-internal/browser-utils": "8.54.0", "@sentry-internal/feedback": "8.54.0", "@sentry-internal/replay": "8.54.0", "@sentry-internal/replay-canvas": "8.54.0", "@sentry/core": "8.54.0" } }, "sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg=="], + + "@sentry/cli": ["@sentry/cli@2.46.0", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.46.0", "@sentry/cli-linux-arm": "2.46.0", "@sentry/cli-linux-arm64": "2.46.0", "@sentry/cli-linux-i686": "2.46.0", "@sentry/cli-linux-x64": "2.46.0", "@sentry/cli-win32-arm64": "2.46.0", "@sentry/cli-win32-i686": "2.46.0", "@sentry/cli-win32-x64": "2.46.0" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA=="], + + "@sentry/cli-darwin": ["@sentry/cli-darwin@2.46.0", "", { "os": "darwin" }, "sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg=="], + + "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.46.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg=="], + + "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.46.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg=="], + + "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.46.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q=="], + + "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.46.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ=="], + + "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.46.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA=="], + + "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.46.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ=="], + + "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww=="], + + "@sentry/core": ["@sentry/core@8.54.0", "", {}, "sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q=="], + + "@sentry/react": ["@sentry/react@8.54.0", "", { "dependencies": { "@sentry/browser": "8.54.0", "@sentry/core": "8.54.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w=="], + + "@sentry/react-native": ["@sentry/react-native@6.15.1", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "3.5.0", "@sentry/browser": "8.54.0", "@sentry/cli": "2.46.0", "@sentry/core": "8.54.0", "@sentry/react": "8.54.0", "@sentry/types": "8.54.0", "@sentry/utils": "8.54.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-uNYjkhi7LUeXe+a3ui3N+sUZ4PbBh/P3Q6Pz5esOQOAEV1N7hxkdnHVic1cVHsirEQvy9rUJPBnja47Va7OpQA=="], + + "@sentry/types": ["@sentry/types@8.54.0", "", { "dependencies": { "@sentry/core": "8.54.0" } }, "sha512-wztdtr7dOXQKi0iRvKc8XJhJ7HaAfOv8lGu0yqFOFwBZucO/SHnu87GOPi8mvrTiy1bentQO5l+zXWAaMvG4uw=="], + + "@sentry/utils": ["@sentry/utils@8.54.0", "", { "dependencies": { "@sentry/core": "8.54.0" } }, "sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw=="], + + "@shopify/flash-list": ["@shopify/flash-list@1.7.3", "", { "dependencies": { "recyclerlistview": "4.2.1", "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-RLhNptm02aqpqZvjj9pJPcU+EVYxOAJhPRCmDOaUbUP86+636w+plsbjpBPSYGvPZhPj56RtZ9FBlvolPeEmYA=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + + "@tamagui/accordion": ["@tamagui/accordion@1.126.12", "", { "dependencies": { "@tamagui/collapsible": "1.126.12", "@tamagui/collection": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-Hv2ORxwR3Ypf6gpvM27EsXAjkZaQf1Ks1DWc2TQh5JHKB44/aCNOx5D7epXaKfZ0OKiMxFjVMZ7ip/HZSN5PRg=="], + + "@tamagui/adapt": ["@tamagui/adapt@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/z-index-stack": "1.126.12" } }, "sha512-QZDF6cXtcEQVj6YYs/zJQ1/cHwC5cF0k1HjfA/DsyZ96xF5roWZDTBAAWAweyx1KgNlhfIOZqmcS8Cna0s3x+Q=="], + + "@tamagui/alert-dialog": ["@tamagui/alert-dialog@1.126.12", "", { "dependencies": { "@tamagui/animate-presence": "1.126.12", "@tamagui/aria-hidden": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/dialog": "1.126.12", "@tamagui/dismissable": "1.126.12", "@tamagui/focus-scope": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/popper": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/remove-scroll": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-fJMuZSU1aK247kcuf32WJDQ0gumKQMwqHHquPxkdXZqxhSqZXFAf3jULMzTI/sOzjQa1lEBL920vWMWDTLzuvw=="], + + "@tamagui/animate": ["@tamagui/animate@1.126.12", "", { "dependencies": { "@tamagui/animate-presence": "1.126.12" } }, "sha512-KBPUvUh5p7f0Dyt0Bxffj1CN++UL1YjgmcGmZQDLUNvSegC2bw7XXDiN2GQwm2uLI9FJJ0W/onjRCcC7TF5UWg=="], + + "@tamagui/animate-presence": ["@tamagui/animate-presence@1.126.12", "", { "dependencies": { "@tamagui/helpers": "1.126.12", "@tamagui/use-constant": "1.126.12", "@tamagui/use-force-update": "1.126.12", "@tamagui/use-presence": "1.126.12", "@tamagui/web": "1.126.12" } }, "sha512-NG3aCUUUCLjCAxSzFhc/i9JfwT0iozdmvLXAhDNrIXI6sRYkKOY61o/8wSKn9feaPuAQU9YmbIzFzflAAqj4Fw=="], + + "@tamagui/animations-css": ["@tamagui/animations-css@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/cubic-bezier-animator": "1.126.12", "@tamagui/use-presence": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-A/8RBt09hTcvHHeRUJ8QDXY3ZddZrCE1YFOy4ZPuhTUsv3XpokI30TRxox80CK9hFvOW5jzIEv01vKviyRbUvQ=="], + + "@tamagui/animations-moti": ["@tamagui/animations-moti@1.126.12", "", { "dependencies": { "@tamagui/use-presence": "1.126.12", "@tamagui/web": "1.126.12", "moti": "^0.29.0" }, "peerDependencies": { "react": "*" } }, "sha512-jNVvuw46sqd7ANCOw0rvTcFvER9ktGddqod8xFIdvAoCkjxfFBXpwGlW8u6CXTfrGQd+BMgrybjP2vrDp2H8LA=="], + + "@tamagui/animations-react-native": ["@tamagui/animations-react-native@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/use-presence": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-41JAU9j7/ad3i9l4AfppOidsEdUHHqiO4g0B6zpV4x5Y8EmphBqSM6DhuM4kKE5ywRkjSwSJYa/P54wR4L5p0A=="], + + "@tamagui/aria-hidden": ["@tamagui/aria-hidden@1.126.12", "", { "dependencies": { "aria-hidden": "^1.1.3" }, "peerDependencies": { "react": "*" } }, "sha512-6elQuZazsVFPGI2X/04qE+Eg6UQv+imacCcu9hqnwpu3gJGt1I+3oqW9zE4FlzLhfRC/ZFwJOapd4PYW+uNdQw=="], + + "@tamagui/avatar": ["@tamagui/avatar@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/image": "1.126.12", "@tamagui/shapes": "1.126.12", "@tamagui/text": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-ib+p/vGKDV8xganZb0wRURZHpzaYQIbRVljliU1OnrTyDvyxHU6oN+a0nzXnJlfT67FZhNjOdgrnO4hzjE0PEA=="], + + "@tamagui/button": ["@tamagui/button@1.126.12", "", { "dependencies": { "@tamagui/font-size": "1.126.12", "@tamagui/get-button-sized": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-6R8e5FRqrDQAx9Bu52yjRrQPasDrIlHXE4B4IvPu8M8Nn4GdEup4bV20wq3abg5PyHTGAUa6vJX1ElW44H+r5A=="], + + "@tamagui/card": ["@tamagui/card@1.126.12", "", { "dependencies": { "@tamagui/create-context": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-iQ3JOHa/vTgC+lR/cQQs8zaS89lW5ynCgu+S5KOEwFzQ0UuBlCfRU5xN56f6qiaYTnudanEgnCZfIMJhq9jOcA=="], + + "@tamagui/checkbox": ["@tamagui/checkbox@1.126.12", "", { "dependencies": { "@tamagui/checkbox-headless": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/font-size": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-9zHUeZXHfm3AdEqlrzUG0RFNRnbhUk84J3R9uDuskJxEn5TE8aktrpd/RObWQcjIFpbAe/p/g3use0KcKzMuTw=="], + + "@tamagui/checkbox-headless": ["@tamagui/checkbox-headless@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-PIgnHkVeZ1iGwl4smqO+v2kvZr4Bia3d1ssUDo2ZoGNeL8qy86IdGjEoX11kH1dCuw2JkiPsG8b4wMQ3kcpDHQ=="], + + "@tamagui/collapsible": ["@tamagui/collapsible@1.126.12", "", { "dependencies": { "@tamagui/animate-presence": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-qtxxLaRWYRamGQZDKVIgnNC/qOKOGBNlRe3rubQa3VVVUfriWZO7hhzQquUVdIVhkCWj0+xZ1Fz3aoGzhso7yg=="], + + "@tamagui/collection": ["@tamagui/collection@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-ZyR7vklkLiPZdK6LdR2g45L9yLJyZM0g9KJ1R9VtqzOHANIKvUBv8BGj9KL82HPYgx6MU5KzVs73BiJndKEbPg=="], + + "@tamagui/colors": ["@tamagui/colors@1.126.12", "", {}, "sha512-3buBUOaTiEAzmdFiNSBgLl7Q4eVrPRY3cINwbP0tny7UbX+kpRUOq3aWdFjZAkqe6e6mpwJlPHb6htdHTIFFXw=="], + + "@tamagui/compose-refs": ["@tamagui/compose-refs@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-CLdmRHvZWGnb6dUDmYLA/ua0H2G54iKc5Hy7aktHOZf003J2T+peImD/bGjfjOW0CoyRuTAdbtcAciMNmwJegA=="], + + "@tamagui/config": ["@tamagui/config@1.126.12", "", { "dependencies": { "@tamagui/animations-css": "1.126.12", "@tamagui/animations-moti": "1.126.12", "@tamagui/animations-react-native": "1.126.12", "@tamagui/colors": "1.126.12", "@tamagui/font-inter": "1.126.12", "@tamagui/font-silkscreen": "1.126.12", "@tamagui/react-native-media-driver": "1.126.12", "@tamagui/shorthands": "1.126.12", "@tamagui/themes": "1.126.12", "@tamagui/web": "1.126.12" } }, "sha512-mLTuNsV6E/1Qd0ohVIQBMlq3KgMJgdkUvtlPv5z2oq3SdGlLCysYFJ9tYIQq5goAt3iajYnZGx+YmRvmpxuQCA=="], + + "@tamagui/constants": ["@tamagui/constants@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-Fy5TtNYzZ+DacqxzEXGIDW/Wej0ftYOb7ffRsVD3pHfqWssVdqMOuqPX4ZXhBq5F3ztHhK4H26DMm8E+ROpxwg=="], + + "@tamagui/core": ["@tamagui/core@1.126.12", "", { "dependencies": { "@tamagui/react-native-media-driver": "1.126.12", "@tamagui/react-native-use-pressable": "1.126.12", "@tamagui/react-native-use-responder-events": "1.126.12", "@tamagui/use-event": "1.126.12", "@tamagui/web": "1.126.12" } }, "sha512-3HGjHIrX92UdMCMFZK3fTClpRy8UJ23uwx5cwvSOgxGk72lq7fssRJI7p5xvBY1H+841HWPBjfsYx0aMrGWNTg=="], + + "@tamagui/create-context": ["@tamagui/create-context@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-Z4lxdN7tUw0G538pJjI2YFhErUu75q0omdppUPhNb4XChat+b1bZBT+Y4jOrjEhtfSkZeHQ4J664K5/pU6BMpQ=="], + + "@tamagui/create-theme": ["@tamagui/create-theme@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" } }, "sha512-looo2G/9WDBFg+lydbu930lmKu2aZ0GmB6eHi2mkgrsRqB4uIcIYagP9SFhktG6+CXUW5iLd+KW8A/Kfp93ZkQ=="], + + "@tamagui/cubic-bezier-animator": ["@tamagui/cubic-bezier-animator@1.126.12", "", {}, "sha512-loElyQGGWIV/G7RUxekv8nFL8WeHUrd2FB8/w7GnVd7dZIO2px6Hw5fgOhHldZe4Pi51RJxJo5WJguf0qpZHJw=="], + + "@tamagui/dialog": ["@tamagui/dialog@1.126.12", "", { "dependencies": { "@tamagui/adapt": "1.126.12", "@tamagui/animate-presence": "1.126.12", "@tamagui/aria-hidden": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/dismissable": "1.126.12", "@tamagui/focus-scope": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/popper": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/remove-scroll": "1.126.12", "@tamagui/sheet": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/z-index-stack": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-1t1eLICxK+TWzWzQwSP/jWRrRszyhEBmwLGdAutINAwVgLjsoeBp6xs9k+OtxB+cFQkGsxeqNBVEdOXVludhew=="], + + "@tamagui/dismissable": ["@tamagui/dismissable@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/use-escape-keydown": "1.126.12", "@tamagui/use-event": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-m8Cb1qOjkP7ycK5aZDvOJhp8heEnv4NyFe0NegGRQ22+pXDM0LpYMnwA6LCGcH4FQa0fOOzYUzzwtnjFatZwsQ=="], + + "@tamagui/elements": ["@tamagui/elements@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-KAmoyCdlb0BL23JhcfFbcu4qOEuZyH54xSYV7m1apnQ/fnD76xA7GRhzinrk9Cmm0i14pV+Q1/3ngZivgaUCEQ=="], + + "@tamagui/fake-react-native": ["@tamagui/fake-react-native@1.126.12", "", {}, "sha512-A1VOuXnKag9/QELZHjcroSJYh6VXudDlqW2TwDzerXZFaJELgiDhlbcbdQpBnB0WHFel31oluCryf5QwDrlksQ=="], + + "@tamagui/floating": ["@tamagui/floating@1.126.12", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/react-native": "^0.10.7" }, "peerDependencies": { "react": "*" } }, "sha512-TNhugL6ju7QhBfHJPPWBkT0Rc+gI2Z4oT7jGiX6G200LSMWRZQ0OoYnFSvNRSEk1E0nV+RpxtISr5+Tb2SORuw=="], + + "@tamagui/focus-scope": ["@tamagui/focus-scope@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/start-transition": "1.126.12", "@tamagui/use-event": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-6/uLGB5PFqG294LCsVltVdKBbFhrlxMesns/fSbrsnEbjZvrK9SXp3sIaJYxBtzepMvV2MHFv+aZ1LzVCdQLCA=="], + + "@tamagui/focusable": ["@tamagui/focusable@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-e+878wj3WyG3+C9jtWcgUBikvBJ87BtJTLTtAHU/yDzBEdAQjYHDbRbJGKeM85MPDDHQ35DpnGy9dyxXfas7pQ=="], + + "@tamagui/font-inter": ["@tamagui/font-inter@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12" } }, "sha512-rqOpaQvmng/k1v1/GinSMYy2Qc54s83m3Quyv34Uq1PJMBMRb8NudJV85IG1ICHxxMOqLVbKnGmdTq+kOh1Sdw=="], + + "@tamagui/font-silkscreen": ["@tamagui/font-silkscreen@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12" } }, "sha512-qOX/R/8Ii3lT4jukf/zscfwKOnQInawGJaUPJkAr45/cmNittZyFmUVgtGa+Xut+Ngjr3Nu+/VLRYUmwhbRLTA=="], + + "@tamagui/font-size": ["@tamagui/font-size@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-5jsh7ixgYSi3OvjqnXcMEZmZ/VOwTexHKONLRP7EUXrgxLiJWoihgu2k/9WW2eqdJUMOegnOXvSyCnAfM7RtIA=="], + + "@tamagui/form": ["@tamagui/form@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/get-button-sized": "1.126.12", "@tamagui/get-font-sized": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/text": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-JDMH+OiBXqWN6yD+xiCGeUz1qyrYXDJlt2tLOoHZbcdTZFuexnuyEo8OztzJ2FxveVCN7QAQX2NaHJxZVmq69w=="], + + "@tamagui/get-button-sized": ["@tamagui/get-button-sized@1.126.12", "", { "dependencies": { "@tamagui/get-token": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-rvZ1Fy1sajLiGxTxx87VfuDbHRaf8BOi3Yi2EvDlOrclffNj8dh9a1QZARitFT0dWdRb400wClnmHaYow4yeTw=="], + + "@tamagui/get-font-sized": ["@tamagui/get-font-sized@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-1qrp/aAxol3VJI6SThs+83UenXSQMyeSRgq2/x4T3wgMz+IBmsX5CY4qMmZ3YrR5CTAgR/LKWkkIZsb//Bmm/A=="], + + "@tamagui/get-token": ["@tamagui/get-token@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-EoRIWz2daLYCOLPuLOSPJ79EeW9slQcRBJZUnoE8KPfYpoz2Da3jetBQqMymzFcxZia0UJjxJvw6X9IZLzuq6w=="], + + "@tamagui/group": ["@tamagui/group@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-7gY30KAeDPO2PXVnHOhCWfbF4Sc/8rmYHCl0Mot0YImU0dVYrLCzO1IIbuWQCb7QJ8zFPcALTCbAKFLApY9Xxw=="], + + "@tamagui/helpers": ["@tamagui/helpers@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/simple-hash": "1.126.12" } }, "sha512-ZChjCFGRRbTGf0CuqOG86miv0sQ7Bly3BNkgzWqNdxvK/TNywMKulVrmVDGz39X5Mcr1dcamj6o/4C3VrGr06A=="], + + "@tamagui/helpers-icon": ["@tamagui/helpers-icon@1.129.2", "", { "dependencies": { "@tamagui/core": "1.129.2" }, "peerDependencies": { "react": "*", "react-native-svg": ">=12" } }, "sha512-76uoq1+IHSlAqK+9+MFeSDfrQ1Kxwp8iKK0QcPl71wux9+WxyWW4Qp/fO6AMPfDvpLEWxsYOVIHuoTFdRzJ8FQ=="], + + "@tamagui/helpers-tamagui": ["@tamagui/helpers-tamagui@1.126.12", "", { "dependencies": { "@tamagui/helpers": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-8reniSIuZ4fQ/PMmYWT/9k/+apq6a8lLP7fxyMrMceH/m2f6IwzvNQi+3Sex6yCtzUB3BMaM8ppWrsWBUJcYAg=="], + + "@tamagui/image": ["@tamagui/image@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-gy5cXHJe1CWi8gibgKm8j4UJrN6gReyT0p10kKoUFKLP4v4AOoPKfv6FcmD9wqtDvKfoHihPweKFuvjsc7KFPg=="], + + "@tamagui/label": ["@tamagui/label@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/get-button-sized": "1.126.12", "@tamagui/get-font-sized": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-IO05udUcKZmGHXFZ9JpC72+3Sp7vwaR0soJxEtxU56+lRrRYjQBgT77OsuriUXEn39E0RQKmSKR7w7ncWouxwA=="], + + "@tamagui/linear-gradient": ["@tamagui/linear-gradient@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12", "@tamagui/stacks": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-GNzwNocld+6HWMgSRnvmFYlpKqg9cMkvXdIX2F2iMGZ/EdVX9QMI9G/KUHJD0oC7sipij56FCUq5HDSGumEasA=="], + + "@tamagui/list-item": ["@tamagui/list-item@1.126.12", "", { "dependencies": { "@tamagui/font-size": "1.126.12", "@tamagui/get-font-sized": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-d5X4WkCaKFZoA8nD/fpxAc6b/ihBFN5PdPL8cs3tigrtfCgyB5o/Wyl8eMWvmtP3Eiis1cCUUux/SWIuMi3sCg=="], + + "@tamagui/lucide-icons": ["@tamagui/lucide-icons@1.129.2", "", { "dependencies": { "@tamagui/core": "1.129.2", "@tamagui/helpers-icon": "1.129.2" }, "peerDependencies": { "react": "*", "react-native-svg": ">=12" } }, "sha512-+evTybm6EekF9/AM8CCwugPAtuLMNBSV6XnLjDOWV50Ia887HszBMceQ5QTkLCyPOfRCJlbl8Tdvsda5Eowjzw=="], + + "@tamagui/normalize-css-color": ["@tamagui/normalize-css-color@1.126.12", "", { "dependencies": { "@react-native/normalize-color": "^2.1.0" } }, "sha512-gaFrZtioA2fXZeJwwOY6p9DmOFzMCIzW5kJ3qg3fCEzoQogDCUY5/fhbbutQjIf7DlPWEf7U9LvysRFO/IZH8A=="], + + "@tamagui/polyfill-dev": ["@tamagui/polyfill-dev@1.126.12", "", {}, "sha512-P6KH5jbnt8KoJXj/jRYdpyZBOu6YBbnKTqhuBH80fHfCvYDkYBsAxb3XGCj7LZtFEJ4MZoP6N925MZ1Q4Dsznw=="], + + "@tamagui/popover": ["@tamagui/popover@1.126.12", "", { "dependencies": { "@floating-ui/react": "^0.27.4", "@tamagui/adapt": "1.126.12", "@tamagui/animate": "1.126.12", "@tamagui/aria-hidden": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/dismissable": "1.126.12", "@tamagui/floating": "1.126.12", "@tamagui/focus-scope": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/popper": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/remove-scroll": "1.126.12", "@tamagui/scroll-view": "1.126.12", "@tamagui/sheet": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "react-freeze": "^1.0.3" }, "peerDependencies": { "react": "*" } }, "sha512-kpVYviuqkLWU2ofe0g6wFIR6AnwEn9xLewZE3Mxr9Wxqbz0wVTohElAK+NpX2qAjISJIYpPk9recfoHSQ6FLlQ=="], + + "@tamagui/popper": ["@tamagui/popper@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/floating": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/start-transition": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-wJHWUCsDOvG1eilMv755dFWkR3/T/o4cBw7JTJysWgp3aHdDEw7ZZQaxsNcfp8yhVncC9TY/csfH0dXsJLrTyQ=="], + + "@tamagui/portal": ["@tamagui/portal@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/start-transition": "1.126.12", "@tamagui/use-did-finish-ssr": "1.126.12", "@tamagui/use-event": "1.126.12", "@tamagui/z-index-stack": "1.126.12" } }, "sha512-ZEusqYWFTEl8/RnFhwTlWsqub5TcH+WedYr2Pr19Zrhjt0JAdWoEg27DLncPmnt3Zj9mKvTtf2YrscQLeGeCvQ=="], + + "@tamagui/progress": ["@tamagui/progress@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/stacks": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-m+AKhdJAWUGyGxdh49d96d84pQgavZrp642SR87jhQOI2Lx3V62De/AtSuYKbx4HWchGC5f3fW3BclLj4DXTuA=="], + + "@tamagui/radio-group": ["@tamagui/radio-group@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/radio-headless": "1.126.12", "@tamagui/roving-focus": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-tLvkcp6dOtbFoTt0LbMJSWhYN8q/bnC22ZMRyBkDBkmkqPOpBvQzuW5NPQcJjm0IjnDSdMLVFdXmsdGe3XJdJg=="], + + "@tamagui/radio-headless": ["@tamagui/radio-headless@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-ss8LIPSlfOA60U807MsHrU86R4Q5l0jiPYb0J+/RDUC3i3zl3v6AGd5HuYN+qpIlhqY6wBz0nXUUZlHlfNp9Mw=="], + + "@tamagui/react-native-media-driver": ["@tamagui/react-native-media-driver@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" }, "peerDependencies": { "react-native": "*" } }, "sha512-yXi618Zjt7AVHFXFZKakHw+AfDAhgxDGkA/nNvk2vhBLHqn3SrLOI+s/B3WJx5Ts6Uz6SvyHJP2etY5TBtWE2g=="], + + "@tamagui/react-native-use-pressable": ["@tamagui/react-native-use-pressable@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-CReoZVVpp0C9OggKIqMQx3pum9Myw+GpRr9YdJSk3EAq4xLGvMvY4vLLV7mW+syH9JVbqiX7whCEk3MPNXxkjA=="], + + "@tamagui/react-native-use-responder-events": ["@tamagui/react-native-use-responder-events@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-CMuX3jV6KVnzaKjbqNYQZtN8qPMutQSQkBoUJHTCbUcj8nElYpjjXYIP3l+S6GkntGArVlx7qxlPl1gMU3tVeA=="], + + "@tamagui/remove-scroll": ["@tamagui/remove-scroll@1.126.12", "", { "dependencies": { "react-remove-scroll": "^2.6.0" }, "peerDependencies": { "react": "*" } }, "sha512-xzmXRwStYLIT1XHbAYCrnizDB1sOYAnM5hR4kEU8zMNPz3nyFYIn+jj1lTma5Px26hCWyhZkFjIUAfXDf1ue6A=="], + + "@tamagui/roving-focus": ["@tamagui/roving-focus@1.126.12", "", { "dependencies": { "@tamagui/collection": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-direction": "1.126.12", "@tamagui/use-event": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-pmgRD+VIKDBrv+crn/hIuNr88XEVx8Cw0aSfJ9Fz5tswq8dx6VJXcDEGw2gPF3aftyivkxzBbqDOz4toKQJ/hw=="], + + "@tamagui/scroll-view": ["@tamagui/scroll-view@1.126.12", "", { "dependencies": { "@tamagui/stacks": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-fpz3N708nXxAe7D/3xABukg7VTuNG0qtUAK9aFAZ8cn35j7yZuCI6tU3GDSO30UZhFo9eWSHTCeOrvIqsLJoGA=="], + + "@tamagui/select": ["@tamagui/select@1.126.12", "", { "dependencies": { "@floating-ui/react": "^0.27.4", "@floating-ui/react-dom": "^2.1.2", "@floating-ui/react-native": "^0.10.7", "@tamagui/adapt": "1.126.12", "@tamagui/animate-presence": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/dismissable": "1.126.12", "@tamagui/focus-scope": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/list-item": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/remove-scroll": "1.126.12", "@tamagui/separator": "1.126.12", "@tamagui/sheet": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-debounce": "1.126.12", "@tamagui/use-event": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-mD5c+3kBf5aSyFz6oKOlDqpU9VFo4VZ8Lzw4/22QgLTvgdS3iwUAJa2GfwAqAaPufsN5Pvafpnb/EParQRIZQg=="], + + "@tamagui/separator": ["@tamagui/separator@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-uywZSKQD5P+6I7S0+UMgWsteYeGioocaYxR/6rZ06wFvfLuerF5YwV5tGz2IVxEd+yJKH12LbpjYkwTert9H1Q=="], + + "@tamagui/shapes": ["@tamagui/shapes@1.126.12", "", { "dependencies": { "@tamagui/stacks": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-kxAIoUoUZSaoBB0kFmFkgDynOVjAj0U57cdCanhajFt+PtoIeAxc6TlAWzMCBk18mP4xM+YJhbB1ACCli1koLA=="], + + "@tamagui/sheet": ["@tamagui/sheet@1.126.12", "", { "dependencies": { "@tamagui/adapt": "1.126.12", "@tamagui/animate-presence": "1.126.12", "@tamagui/animations-react-native": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/remove-scroll": "1.126.12", "@tamagui/scroll-view": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-constant": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-did-finish-ssr": "1.126.12", "@tamagui/use-keyboard-visible": "1.126.12", "@tamagui/z-index-stack": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-pAlR3uh7XEowfGvCBQuQiG4z1XZKTz1N93VOAY8t1+K66sWpgssXmafMQ+6guTejLMr+YZPpVXGeU5SxcOg/cQ=="], + + "@tamagui/shorthands": ["@tamagui/shorthands@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" } }, "sha512-N8H4mq+mb44PzOxW3W6C/NEDg6yyFjeeXAbCjoUVsABQsGThjea9nZrLxk1rFfgKRkObHkOKNqK1QV/fkmdZAg=="], + + "@tamagui/simple-hash": ["@tamagui/simple-hash@1.126.12", "", {}, "sha512-+JGmNo5SAoLTDZIfkH6EfzDS1aXw9NiTJBnOuMNLPiJCE+1zPs0HezD9Iv6hroIU0WaiUfw5E103Phlp9wlNeQ=="], + + "@tamagui/slider": ["@tamagui/slider@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-debounce": "1.126.12", "@tamagui/use-direction": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-IiURazRgvYBW+kCqPk82CL4+EtobPw5T8LbJG21BGseLLlU74peiCDQKVwoi+vbjcuSly61Aao8tZN37UzTVNw=="], + + "@tamagui/stacks": ["@tamagui/stacks@1.126.12", "", { "dependencies": { "@tamagui/core": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-HoLDrbghaDLPPvZS98DtOfFOOIYIBKnUNwJ/YdCpGsbLvQtkwvQUuToBrwo0BSYT+lYumkbFiaootzS7sA8yEg=="], + + "@tamagui/start-transition": ["@tamagui/start-transition@1.126.12", "", {}, "sha512-iBf+eXd8yH/AvQFaXHOFyFHtHdFVYW+hB+mCKywjc4PnK4ZZW8AwBSuXVB8Z/BPtTjm6DSNhTg1Y50aBVoYusg=="], + + "@tamagui/switch": ["@tamagui/switch@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/switch-headless": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-0IKlzb6RUJ7j3uMYuroENTGDJ2EgasYYPWYfgUMOzjPwwB5qbgegibq24GoyOkWbHqCUMidG2iQ5f6A4bq6M8Q=="], + + "@tamagui/switch-headless": ["@tamagui/switch-headless@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/use-previous": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-vuiAtNryUKJrO0v8ab7ey30/2BmxkpMwLgx9mIKJAtcQxNj2YsQBVspyCjQey3J3kytoLHQOwRTBViKZFMuacQ=="], + + "@tamagui/tabs": ["@tamagui/tabs@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/get-button-sized": "1.126.12", "@tamagui/group": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/roving-focus": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-direction": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-uy8fFfuCKKzkVLNTNEATFiavMra7EciLaTLOd0IfM2Q1FsbpC9IWRhCyUypBKwGvKehKD20QKc33KVaUERpBtQ=="], + + "@tamagui/text": ["@tamagui/text@1.126.12", "", { "dependencies": { "@tamagui/get-font-sized": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-VeCT4r1k+0Xyg84zDd4MPf7zkOP6A1vHfOqsOOvNTsAADnura3wR9qQ+zFyZ5z2NuPJiRtRdWWVmbJx60yQ3HQ=="], + + "@tamagui/theme": ["@tamagui/theme@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-wzOo4XYcJSM88YQ+8fvQYmWr38QWOUo1I8rEq4BKcKOrAstqUudEperdNlX1ZQQXSxEsUMa+/MggQd7v1DQfcw=="], + + "@tamagui/theme-builder": ["@tamagui/theme-builder@1.126.12", "", { "dependencies": { "@tamagui/create-theme": "1.126.12", "color2k": "^2.0.2" } }, "sha512-RzofAeZjC+j7MKQpZQoXEEEJ6LsRC1qEE1NO6FiZZAWcpTp+xiaWwo63NTY1TvhAtQg8zVMOjX15CXUkKSuB3g=="], + + "@tamagui/themes": ["@tamagui/themes@1.126.12", "", { "dependencies": { "@tamagui/colors": "1.126.12", "@tamagui/create-theme": "1.126.12", "@tamagui/theme-builder": "1.126.12", "@tamagui/web": "1.126.12", "color2k": "^2.0.2" } }, "sha512-io7gDp6XefJcHKyGXwJvYI+n/tRm1LcNQ8hBqxTeBdrLGaLerfqawxEtZV8tvHQUasEUA7MWX2SxNL++Edf5Uw=="], + + "@tamagui/timer": ["@tamagui/timer@1.126.12", "", {}, "sha512-RSkpQegOBT4swVXPQjjDRqJP9eC8Mzv8ObmoIcbxwoWm/vS0fbn6UwHhHiQrKQvZHHIciXB8HzjRc8ljKxbuuQ=="], + + "@tamagui/toggle-group": ["@tamagui/toggle-group@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/font-size": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/group": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/roving-focus": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-direction": "1.126.12", "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-FGIXaXYzOlE6CqupkBXU4KaLUsiuLWjCSVxkbl+z/IoBcxUy1hHgtjKCLpZSdZiChBDmCCS2SaZm5O8Z5zPfsg=="], + + "@tamagui/tooltip": ["@tamagui/tooltip@1.126.12", "", { "dependencies": { "@floating-ui/react": "^0.27.4", "@tamagui/compose-refs": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/floating": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/popover": "1.126.12", "@tamagui/popper": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/use-controllable-state": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-sJEYRAaUlumuPoTj48N6l8uPpU0Vcj8h1QpeTMSCtK2LioKs3Dvwh1EqkS9Zdz6+GFouUxUCZiPe6S8hvECRlA=="], + + "@tamagui/types": ["@tamagui/types@1.126.12", "", {}, "sha512-fIBHhyKFs6HYDBvHEUiOKHTdSzuLRX5TPIZoh4SH6r3GzmkbElaPErcTe8FgiwJgbkp8NQXog7wiRMRtJKtrYw=="], + + "@tamagui/use-callback-ref": ["@tamagui/use-callback-ref@1.126.12", "", {}, "sha512-iU9irW+0zSqMiuEEOGZnFhtYa8Rt4EzdzVvg47463ny6ehy3N5DaOYAi1Vzg/ziJLRqu+o8YTO1oYNJwNLbGuw=="], + + "@tamagui/use-constant": ["@tamagui/use-constant@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-Ukjl85j2C2JEh1ge3K8X8iFM7hYK2QAaTO43fu3tr2sUgxR+Kiuro6bYIuktaWc1NCNY38sPKaJwILprFCuH4w=="], + + "@tamagui/use-controllable-state": ["@tamagui/use-controllable-state@1.126.12", "", { "dependencies": { "@tamagui/start-transition": "1.126.12", "@tamagui/use-event": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-EmVrFYiUgEqqbwtOlvCr3tTzIp9zq84us8zb7PYRcmN4PlsRrYion0BtjHOhcc8pxRntPKpYMbbMMeNi+Crqzw=="], + + "@tamagui/use-debounce": ["@tamagui/use-debounce@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-8z4AYa4UruX1ZIYFY8oTNvqeBD4+KnbyRGF1xS3qdE17rbFhrzaB/Tn7qt3xuI7M5iCnQOCneyxQ+4ZBfYHyFA=="], + + "@tamagui/use-did-finish-ssr": ["@tamagui/use-did-finish-ssr@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-GfH4n3bsF6lscxElAlYlf3fsKrvJbOsUheN7UYyyveKlcYEsT3iAAGXXk/H0/0RVx9sZ4rLX3+zfnf0LNzzgGg=="], + + "@tamagui/use-direction": ["@tamagui/use-direction@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-KYtKf4UTkzIV1E41CK18pWY0cWXRdo41teYy5oW7NR4fE4PDo6B+cPl0M6S8KOZIxSDeLFfYWnaGztXx8q6r3w=="], + + "@tamagui/use-escape-keydown": ["@tamagui/use-escape-keydown@1.126.12", "", { "dependencies": { "@tamagui/use-callback-ref": "1.126.12" } }, "sha512-WJxLRifc50u6bYGFvavbzhA0vgCfgzqluiO+fgZKVCFBT7QrgYVrO4FefF96E87HScil8sLnphGpcSOp/MwwNQ=="], + + "@tamagui/use-event": ["@tamagui/use-event@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-CNwCRPYSTyHablAg97gjaoZ7VOY8XNY0MfUItug1N1xgezx9TcTq/O056ZLAf8UFHsOsWjnthWcLnPeWCw238g=="], + + "@tamagui/use-force-update": ["@tamagui/use-force-update@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-ChxPzjdDoYNRehAzJRAAFfMZHvTWQLe6XKWj8YzCIXWygKMMTNo9GFsx+4M1dgL82OhV95mwjYTxc4ICsCbL/g=="], + + "@tamagui/use-keyboard-visible": ["@tamagui/use-keyboard-visible@1.126.12", "", { "peerDependencies": { "react": "*" } }, "sha512-U/SFooHydfFMviIqLzgDvXOk6JUYIBTLGpOyNLZ5/5rBGiTTLU/TFripNBOUuLDnPR4mbTMvkAhgTWKLe67D8A=="], + + "@tamagui/use-presence": ["@tamagui/use-presence@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-YWgHH00hGQJ5rp8ot0DVPkS4/vkVrjjKa1647y5EiPEiHcUDs1FS5dLAKt9B6tYYaqV30sdXPbM9kBemlgYbXg=="], + + "@tamagui/use-previous": ["@tamagui/use-previous@1.126.12", "", {}, "sha512-0i41WIYXIQ/yzk95XCOM0gVt937dJrwm/9j3wX5LPpUXoBKO5k0G08UvvEJgnVYGXiIb6KvM0OuENbDwr+PRFw=="], + + "@tamagui/use-window-dimensions": ["@tamagui/use-window-dimensions@1.126.12", "", { "dependencies": { "@tamagui/constants": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-mRp7P3bzzUbGxJYlWoD59DHgJKfvPJfyReZDG3Hv0BONmZPTIvwvMxB+VUKrhyUBllU4SiZuDdE4+hbdwSSryA=="], + + "@tamagui/visually-hidden": ["@tamagui/visually-hidden@1.126.12", "", { "dependencies": { "@tamagui/web": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-M97xl9J2UYDuPROWLbEaD3KIFLDQ78SQB6nYq69GKm0vIkV294PPq3yZrl2npyDvzWUyml5ePAcq1k6YbLS8+A=="], + + "@tamagui/web": ["@tamagui/web@1.126.12", "", { "dependencies": { "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/helpers": "1.126.12", "@tamagui/normalize-css-color": "1.126.12", "@tamagui/timer": "1.126.12", "@tamagui/types": "1.126.12", "@tamagui/use-did-finish-ssr": "1.126.12", "@tamagui/use-event": "1.126.12", "@tamagui/use-force-update": "1.126.12" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-/Ts/sR8UgMNSafWE/AoOS7EZFidXXeoyjbm9HbvrDVUAU0qzOfm8KVN52gssurxN1W6WaHekUbObB/eWOfwXyA=="], + + "@tamagui/z-index-stack": ["@tamagui/z-index-stack@1.126.12", "", {}, "sha512-Okq1nWURF/TjZ8Rsx7Y3niR0a7F/clam1vc7UY6pI41jpft+H2CiHIT8U3uR4Qqgv8lnqiqD1p47FfTWWguVVg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.76.0", "", {}, "sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.76.1", "", { "dependencies": { "@tanstack/query-core": "5.76.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-YxdLZVGN4QkT5YT1HKZQWiIlcgauIXEIsMOTSjvyD5wLYK8YVvKZUPAysMqossFJJfDpJW3pFn7WNZuPOqq+fw=="], + + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], + + "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ=="], + + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], + + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + + "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="], + + "@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], + + "@types/node-forge": ["@types/node-forge@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ=="], + + "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], + + "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + + "@types/react": ["@types/react@18.3.21", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw=="], + + "@types/react-test-renderer": ["@types/react-test-renderer@18.3.1", "", { "dependencies": { "@types/react": "^18" } }, "sha512-vAhnk0tG2eGa37lkU9+s5SoroCsRI08xnsWFiAXOuPH2jqzMbcXvKExXViPi1P5fIklDeCvXqyrdmipFaSkZrA=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.32.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/utils": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.32.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.32.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.7.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.7.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.7.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2", "", { "os": "linux", "cpu": "arm" }, "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.7.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.7.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.7.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.7.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.7.2", "", { "os": "linux", "cpu": "none" }, "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.7.2", "", { "os": "linux", "cpu": "none" }, "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.7.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.7.2", "", { "os": "linux", "cpu": "x64" }, "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.7.2", "", { "os": "linux", "cpu": "x64" }, "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.7.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.7.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.7.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.7.2", "", { "os": "win32", "cpu": "x64" }, "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA=="], + + "@urql/core": ["@urql/core@5.1.1", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.5", "wonka": "^6.3.2" } }, "sha512-aGh024z5v2oINGD/In6rAtVKTm4VmQ2TxKQBAtk2ZSME5dunZFcjltw4p5ENQg+5CBhZ3FHMzl0Oa+rwqiWqlg=="], + + "@urql/exchange-retry": ["@urql/exchange-retry@1.3.1", "", { "dependencies": { "@urql/core": "^5.1.1", "wonka": "^6.3.2" } }, "sha512-EEmtFu8JTuwsInqMakhLq+U3qN8ZMd5V3pX44q0EqD2imqTDsa8ikZqJ1schVrN8HljOdN+C08cwZ1/r5uIgLw=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-loose": ["acorn-loose@8.5.0", "", { "dependencies": { "acorn": "^8.14.0" } }, "sha512-ppga7pybjwX2HSJv5ayHe6QG4wmNS1RQ2wjBMFTVnOj0h8Rxsmtc6fnVzINqHSSRz23sTe9IL3UAt/PU9gc4FA=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "ast-types": ["ast-types@0.15.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="], + + "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "babel-core": ["babel-core@7.0.0-bridge.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="], + + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.13", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g=="], + + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.11.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ=="], + + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.4", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw=="], + + "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.19.13", "", {}, "sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ=="], + + "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.23.1", "", { "dependencies": { "hermes-parser": "0.23.1" } }, "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA=="], + + "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], + + "babel-preset-expo": ["babel-preset-expo@12.0.11", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.9", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-4m6D92nKEieg+7DXa8uSvpr0GjfuRfM/G0t0I/Q5hF8HleEv5ms3z4dJ+p52qXSJsm760tMqLdO93Ywuoi7cCQ=="], + + "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], + + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "bplist-creator": ["bplist-creator@0.0.7", "", { "dependencies": { "stream-buffers": "~2.2.0" } }, "sha512-xp/tcaV3T5PCiaY04mXga7o/TE+t95gqeLmADeBI1CvZtdWTbgBt3uLpvh4UWtenKeBhCV6oVxGk38yZr2uYEA=="], + + "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="], + + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="], + + "buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="], + + "buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cacache": ["cacache@18.0.4", "", { "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", "ssri": "^10.0.0", "tar": "^6.1.11", "unique-filename": "^3.0.0" } }, "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ=="], + + "cachedir": ["cachedir@2.3.0", "", {}, "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], + + "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001718", "", {}, "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw=="], + + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "char-regex": ["char-regex@2.0.2", "", {}, "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], + + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], + + "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], + + "collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "color2k": ["color2k@2.0.3", "", {}, "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "commitizen": ["commitizen@4.3.1", "", { "dependencies": { "cachedir": "2.3.0", "cz-conventional-changelog": "3.3.0", "dedent": "0.7.0", "detect-indent": "6.1.0", "find-node-modules": "^2.1.2", "find-root": "1.1.0", "fs-extra": "9.1.0", "glob": "7.2.3", "inquirer": "8.2.5", "is-utf8": "^0.2.1", "lodash": "4.17.21", "minimist": "1.2.7", "strip-bom": "4.0.0", "strip-json-comments": "3.1.1" }, "bin": { "cz": "bin/git-cz", "git-cz": "bin/git-cz", "commitizen": "bin/commitizen" } }, "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "component-type": ["component-type@1.2.2", "", {}, "sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.8.0", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="], + + "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="], + + "conventional-commit-types": ["conventional-commit-types@3.0.0", "", {}, "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg=="], + + "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "core-js-compat": ["core-js-compat@3.42.0", "", { "dependencies": { "browserslist": "^4.24.4" } }, "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.1.0", "", { "dependencies": { "jiti": "^2.4.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g=="], + + "create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="], + + "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], + + "css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="], + + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + + "css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], + + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], + + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + + "cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], + + "data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + + "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-gateway": ["default-gateway@4.2.0", "", { "dependencies": { "execa": "^1.0.0", "ip-regex": "^2.1.0" } }, "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "del": ["del@6.1.1", "", { "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.2", "p-map": "^4.0.0", "rimraf": "^3.0.2", "slash": "^3.0.0" } }, "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-file": ["detect-file@1.0.0", "", {}, "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], + + "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.154", "", {}, "sha512-G4VCFAyKbp1QJ+sWdXYIRYsPGvlV5sDACfCmoMFog3rjm1syLhI41WXm/swZypwCIWIm4IFLWzHY14joWMQ5Fw=="], + + "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], + + "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + + "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "eslint": ["eslint@9.26.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.26.0", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "zod": "^3.24.2" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ=="], + + "eslint-config-airbnb": ["eslint-config-airbnb@19.0.4", "", { "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", "object.entries": "^1.1.5" }, "peerDependencies": { "eslint": "^7.32.0 || ^8.2.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0" } }, "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew=="], + + "eslint-config-airbnb-base": ["eslint-config-airbnb-base@15.0.0", "", { "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", "object.entries": "^1.1.5", "semver": "^6.3.0" }, "peerDependencies": { "eslint": "^7.32.0 || ^8.2.0", "eslint-plugin-import": "^2.25.2" } }, "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig=="], + + "eslint-config-airbnb-typescript": ["eslint-config-airbnb-typescript@18.0.0", "", { "dependencies": { "eslint-config-airbnb-base": "^15.0.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg=="], + + "eslint-config-expo": ["eslint-config-expo@9.2.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-expo": "^0.1.4", "eslint-plugin-import": "^2.30.0", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", "globals": "^16.0.0" }, "peerDependencies": { "eslint": ">=8.10" } }, "sha512-TQgmSx+2mRM7qUS0hB5kTDrHcSC35rA1UzOSgK5YRLmSkSMlKLmXkUrhwOpnyo9D/nHdf4ERRAySRYxgA6dlrw=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg=="], + + "eslint-plugin-expo": ["eslint-plugin-expo@0.1.4", "", { "dependencies": { "@typescript-eslint/types": "^8.29.1", "@typescript-eslint/utils": "^8.29.1", "eslint": "^9.24.0" } }, "sha512-YA7yiMacQbLJySuyJA0Eb5V65obqp6fVOWtw1JdYDRWC5MeToPrnNvhGDpk01Bv3Vm4ownuzUfvi89MXi1d6cg=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.4.0", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.0" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-react-native": ["eslint-plugin-react-native@5.0.0", "", { "dependencies": { "eslint-plugin-react-native-globals": "^0.1.1" }, "peerDependencies": { "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-VyWlyCC/7FC/aONibOwLkzmyKg4j9oI8fzrk9WYNs4I8/m436JuOTAFwLvEn1CVvc7La4cPfbCyspP4OYpP52Q=="], + + "eslint-plugin-react-native-globals": ["eslint-plugin-react-native-globals@0.1.2", "", {}, "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g=="], + + "eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.1.4", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ=="], + + "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], + + "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], + + "execa": ["execa@5.1.1", "", { "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" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], + + "expo": ["expo@52.0.46", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.26", "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/fingerprint": "0.11.11", "@expo/metro-config": "0.19.12", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.11", "expo-asset": "~11.0.5", "expo-constants": "~17.0.8", "expo-file-system": "~18.0.12", "expo-font": "~13.0.4", "expo-keep-awake": "~14.0.3", "expo-modules-autolinking": "2.0.8", "expo-modules-core": "2.2.3", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-JG89IVZLp7DWzgeiQb+0N43kWOF1DUm3esBvAS9cPFWZsM9x8nDXgbvtREcycDPA6E+yJsSC+086CigeUY6sVA=="], + + "expo-asset": ["expo-asset@11.0.5", "", { "dependencies": { "@expo/image-utils": "^0.6.5", "expo-constants": "~17.0.8", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-TL60LmMBGVzs3NQcO8ylWqBumMh4sx0lmeJsn7+9C88fylGDhyyVnKZ1PyTXo9CVDBkndutZx2JUEQWM9BaiXw=="], + + "expo-blur": ["expo-blur@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw=="], + + "expo-build-properties": ["expo-build-properties@0.13.3", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-gw7AYP+YF50Gr912BedelRDTfR4GnUEn9p5s25g4nv0hTJGWpBZdCYR5/Oi2rmCHJXxBqhPjxzV7JRh72fntLg=="], + + "expo-constants": ["expo-constants@17.0.8", "", { "dependencies": { "@expo/config": "~10.0.11", "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg=="], + + "expo-dev-client": ["expo-dev-client@5.0.20", "", { "dependencies": { "expo-dev-launcher": "5.0.35", "expo-dev-menu": "6.0.25", "expo-dev-menu-interface": "1.9.3", "expo-manifests": "~0.15.8", "expo-updates-interface": "~1.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg=="], + + "expo-dev-launcher": ["expo-dev-launcher@5.0.35", "", { "dependencies": { "ajv": "8.11.0", "expo-dev-menu": "6.0.25", "expo-manifests": "~0.15.8", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog=="], + + "expo-dev-menu": ["expo-dev-menu@6.0.25", "", { "dependencies": { "expo-dev-menu-interface": "1.9.3" }, "peerDependencies": { "expo": "*" } }, "sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA=="], + + "expo-dev-menu-interface": ["expo-dev-menu-interface@1.9.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg=="], + + "expo-file-system": ["expo-file-system@18.0.12", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg=="], + + "expo-font": ["expo-font@13.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw=="], + + "expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="], + + "expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="], + + "expo-keep-awake": ["expo-keep-awake@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg=="], + + "expo-linear-gradient": ["expo-linear-gradient@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ=="], + + "expo-linking": ["expo-linking@7.0.5", "", { "dependencies": { "expo-constants": "~17.0.5", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-3KptlJtcYDPWohk0MfJU75MJFh2ybavbtcSd84zEPfw9s1q3hjimw3sXnH03ZxP54kiEWldvKmmnGcVffBDB1g=="], + + "expo-localization": ["expo-localization@16.0.1", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-kUrXiV/Pq9r7cG+TMt+Qa49IUQ9Y/czVwen4hmiboTclTopcWdIeCzYZv6JGtufoPpjEO9vVx1QJrXYl9V2u0Q=="], + + "expo-manifests": ["expo-manifests@0.15.8", "", { "dependencies": { "@expo/config": "~10.0.11", "expo-json-utils": "~0.14.0" }, "peerDependencies": { "expo": "*" } }, "sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw=="], + + "expo-modules-autolinking": ["expo-modules-autolinking@2.0.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ=="], + + "expo-modules-core": ["expo-modules-core@2.2.3", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-01QqZzpP/wWlxnNly4G06MsOBUTbMDj02DQigZoXfDh80vd/rk3/uVXqnZgOdLSggTs6DnvOgAUy0H2q30XdUg=="], + + "expo-network": ["expo-network@7.0.5", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-5dlowKAimhIDN1/lBRnN6SSH6c07f12R3QrfLf3b3GEr6D+EijH2wE537mmwPh1p+254LAkm0Z5ZEXxbwII4sA=="], + + "expo-router": ["expo-router@4.0.21", "", { "dependencies": { "@expo/metro-runtime": "4.0.1", "@expo/server": "^0.5.3", "@radix-ui/react-slot": "1.0.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "client-only": "^0.0.1", "react-helmet-async": "^1.3.0", "react-native-helmet-async": "2.0.4", "react-native-is-edge-to-edge": "^1.1.6", "schema-utils": "^4.0.1", "semver": "~7.6.3", "server-only": "^0.0.1" }, "peerDependencies": { "@react-navigation/drawer": "^7.1.1", "expo": "*", "expo-constants": "~17.0.8", "expo-linking": "~7.0.5", "react-native-reanimated": "*", "react-native-safe-area-context": "*", "react-native-screens": "*" }, "optionalPeers": ["@react-navigation/drawer", "react-native-reanimated"] }, "sha512-z1U9cGZbgL+ZSHp533VMobOqdkUpFBlDXBpd9/JH+Q0wW49is0G2PrJVUYMzdwr30HSUltdO/19W8rRwjfOnFw=="], + + "expo-secure-store": ["expo-secure-store@14.2.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-hYBbaAD70asKTFd/eZBKVu+9RTo9OSTMMLqXtzDF8ndUGjpc6tmRCoZtrMHlUo7qLtwL5jm+vpYVBWI8hxh/1Q=="], + + "expo-splash-screen": ["expo-splash-screen@0.29.24", "", { "dependencies": { "@expo/prebuild-config": "~8.2.0" }, "peerDependencies": { "expo": "*" } }, "sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g=="], + + "expo-status-bar": ["expo-status-bar@2.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-AkIPX7jWHRPp83UBZ1iXtVvyr0g+DgBVvIXTtlmPtmUsm8Vq9Bb5IGj86PW8osuFlgoTVAg7HI/+Ok7yEYwiRg=="], + + "expo-symbols": ["expo-symbols@0.2.2", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-yTk1MxhA61YflYIMortImD57MCneKEoSvU1acqQ4oKigV5+cNw1XKB7GhcKe3d8Ny3ikC/b1Ia+HQjR0Hmr4JA=="], + + "expo-system-ui": ["expo-system-ui@4.0.9", "", { "dependencies": { "@react-native/normalize-colors": "0.76.8", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-hqBc0EWeK/BTB8i4H84vqNjje8GgxhapYrcWdg5qriaRA/u+bNNxhmpZXdAjFuhonOP4SmAbF+gjoJJWsTrhUg=="], + + "expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="], + + "expo-web-browser": ["expo-web-browser@14.0.2", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Hncv2yojhTpHbP6SGWARBFdl7P6wBHc1O8IKaNsH0a/IEakq887o1eRhLxZ5IwztPQyRDhpqHdgJ+BjWolOnwA=="], + + "exponential-backoff": ["exponential-backoff@3.1.2", "", {}, "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-loops": ["fast-loops@1.1.4", "", {}, "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg=="], + + "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + + "fbemitter": ["fbemitter@3.0.0", "", { "dependencies": { "fbjs": "^3.0.0" } }, "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw=="], + + "fbjs": ["fbjs@3.0.5", "", { "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg=="], + + "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], + + "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], + + "fetch-retry": ["fetch-retry@4.1.1", "", {}, "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="], + + "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "find-cache-dir": ["find-cache-dir@2.1.0", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ=="], + + "find-node-modules": ["find-node-modules@2.1.3", "", { "dependencies": { "findup-sync": "^4.0.0", "merge": "^2.1.1" } }, "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "findup-sync": ["findup-sync@4.0.0", "", { "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^4.0.2", "resolve-dir": "^1.0.1" } }, "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], + + "flow-parser": ["flow-parser@0.270.0", "", {}, "sha512-WjU6NZjaENlHjiO6eGyfAbuk0OC5XkKoN+XCY2g1nDDW230smGGxI9Ltp0qJdj0+ae2MrO1fwwn3vOupv9R+Xw=="], + + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "framer-motion": ["framer-motion@6.5.1", "", { "dependencies": { "@motionone/dom": "10.12.0", "framesync": "6.0.1", "hey-listen": "^1.0.8", "popmotion": "11.0.3", "style-value-types": "5.0.0", "tslib": "^2.1.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": ">=16.8 || ^17.0.0 || ^18.0.0", "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" } }, "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw=="], + + "framesync": ["framesync@6.0.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA=="], + + "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], + + "getenv": ["getenv@1.0.0", "", {}, "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg=="], + + "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + + "global-modules": ["global-modules@1.0.0", "", { "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" } }, "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg=="], + + "global-prefix": ["global-prefix@1.0.2", "", { "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hermes-estree": ["hermes-estree@0.23.1", "", {}, "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg=="], + + "hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="], + + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + + "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], + + "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], + + "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "inline-style-prefixer": ["inline-style-prefixer@6.0.4", "", { "dependencies": { "css-in-js-utils": "^3.1.0", "fast-loops": "^1.1.3" } }, "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg=="], + + "inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], + + "internal-ip": ["internal-ip@4.3.0", "", { "dependencies": { "default-gateway": "^4.2.0", "ipaddr.js": "^1.9.0" } }, "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "ip-regex": ["ip-regex@2.1.0", "", {}, "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + + "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-path-cwd": ["is-path-cwd@2.2.0", "", {}, "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="], + + "istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="], + + "jest-changed-files": ["jest-changed-files@29.7.0", "", { "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="], + + "jest-circus": ["jest-circus@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.7.0", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0", "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw=="], + + "jest-cli": ["jest-cli@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "create-jest": "^29.7.0", "exit": "^0.1.2", "import-local": "^3.0.2", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg=="], + + "jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="], + + "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="], + + "jest-each": ["jest-each@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "jest-util": "^29.7.0", "pretty-format": "^29.7.0" } }, "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ=="], + + "jest-environment-jsdom": ["jest-environment-jsdom@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA=="], + + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], + + "jest-expo": ["jest-expo@52.0.6", "", { "dependencies": { "@expo/config": "~10.0.11", "@expo/json-file": "^9.0.2", "@jest/create-cache-key-function": "^29.2.1", "@jest/globals": "^29.2.1", "babel-jest": "^29.2.1", "fbemitter": "^3.0.0", "find-up": "^5.0.0", "jest-environment-jsdom": "^29.2.1", "jest-snapshot": "^29.2.1", "jest-watch-select-projects": "^2.0.0", "jest-watch-typeahead": "2.2.1", "json5": "^2.2.3", "lodash": "^4.17.19", "react-server-dom-webpack": "19.0.0-rc-6230622a1a-20240610", "react-test-renderer": "18.3.1", "server-only": "^0.0.1", "stacktrace-js": "^2.0.2" }, "peerDependencies": { "expo": "*", "react-native": "*" }, "bin": { "jest": "bin/jest.js" } }, "sha512-Ql60mCy4cfwyNvCW2wpEXbw/3i5H+SmB1XP1z0SJUpafGBipq6xMjPcgQpe/7PzAHTc/ikD+dFA0sPnljDJmZQ=="], + + "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="], + + "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + + "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + + "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], + + "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "jest-resolve": ["jest-resolve@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA=="], + + "jest-resolve-dependencies": ["jest-resolve-dependencies@29.7.0", "", { "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" } }, "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA=="], + + "jest-runner": ["jest-runner@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-leak-detector": "^29.7.0", "jest-message-util": "^29.7.0", "jest-resolve": "^29.7.0", "jest-runtime": "^29.7.0", "jest-util": "^29.7.0", "jest-watcher": "^29.7.0", "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ=="], + + "jest-runtime": ["jest-runtime@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ=="], + + "jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-watch-select-projects": ["jest-watch-select-projects@2.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "chalk": "^3.0.0", "prompts": "^2.2.1" } }, "sha512-j00nW4dXc2NiCW6znXgFLF9g8PJ0zP25cpQ1xRro/HU2GBfZQFZD0SoXnAlaoKkIY4MlfTMkKGbNXFpvCdjl1w=="], + + "jest-watch-typeahead": ["jest-watch-typeahead@2.2.1", "", { "dependencies": { "ansi-escapes": "^6.0.0", "chalk": "^4.0.0", "jest-regex-util": "^29.0.0", "jest-watcher": "^29.0.0", "slash": "^5.0.0", "string-length": "^5.0.1", "strip-ansi": "^7.0.1" }, "peerDependencies": { "jest": "^27.0.0 || ^28.0.0 || ^29.0.0" } }, "sha512-jYpYmUnTzysmVnwq49TAxlmtOAwp8QIqvZyoofQFn8fiWhEDZj33ZXzg3JA4nGnzWFm1hbWf3ADpteUokvXgFA=="], + + "jest-watcher": ["jest-watcher@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g=="], + + "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "join-component": ["join-component@1.1.0", "", {}, "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsc-android": ["jsc-android@250231.0.0", "", {}, "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw=="], + + "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], + + "jscodeshift": ["jscodeshift@0.14.0", "", { "dependencies": { "@babel/core": "^7.13.16", "@babel/parser": "^7.13.16", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@babel/preset-flow": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "@babel/register": "^7.13.16", "babel-core": "^7.0.0-bridge.0", "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", "recast": "^0.21.0", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" }, "bin": { "jscodeshift": "bin/jscodeshift.js" } }, "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA=="], + + "jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + + "lightningcss": ["lightningcss@1.27.0", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.27.0", "lightningcss-darwin-x64": "1.27.0", "lightningcss-freebsd-x64": "1.27.0", "lightningcss-linux-arm-gnueabihf": "1.27.0", "lightningcss-linux-arm64-gnu": "1.27.0", "lightningcss-linux-arm64-musl": "1.27.0", "lightningcss-linux-x64-gnu": "1.27.0", "lightningcss-linux-x64-musl": "1.27.0", "lightningcss-win32-arm64-msvc": "1.27.0", "lightningcss-win32-x64-msvc": "1.27.0" } }, "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@16.0.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ=="], + + "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], + + "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], + + "lodash.map": ["lodash.map@4.6.0", "", {}, "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "longest": ["longest@2.0.1", "", {}, "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="], + + "md5-file": ["md5-file@3.2.3", "", { "dependencies": { "buffer-alloc": "^1.1.0" }, "bin": { "md5-file": "cli.js" } }, "sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw=="], + + "mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + + "meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], + + "merge": ["merge@2.1.1", "", {}, "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "metro": ["metro@0.81.5", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.25.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.81.5", "metro-cache": "0.81.5", "metro-cache-key": "0.81.5", "metro-config": "0.81.5", "metro-core": "0.81.5", "metro-file-map": "0.81.5", "metro-resolver": "0.81.5", "metro-runtime": "0.81.5", "metro-source-map": "0.81.5", "metro-symbolicate": "0.81.5", "metro-transform-plugins": "0.81.5", "metro-transform-worker": "0.81.5", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-YpFF0DDDpDVygeca2mAn7K0+us+XKmiGk4rIYMz/CRdjFoCGqAei/IQSpV0UrGfQbToSugpMQeQJveaWSH88Hg=="], + + "metro-babel-transformer": ["metro-babel-transformer@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" } }, "sha512-oKCQuajU5srm+ZdDcFg86pG/U8hkSjBlkyFjz380SZ4TTIiI5F+OQB830i53D8hmqmcosa4wR/pnKv8y4Q3dLw=="], + + "metro-cache": ["metro-cache@0.81.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "metro-core": "0.81.5" } }, "sha512-wOsXuEgmZMZ5DMPoz1pEDerjJ11AuMy9JifH4yNW7NmWS0ghCRqvDxk13LsElzLshey8C+my/tmXauXZ3OqZgg=="], + + "metro-cache-key": ["metro-cache-key@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-lGWnGVm1UwO8faRZ+LXQUesZSmP1LOg14OVR+KNPBip8kbMECbQJ8c10nGesw28uQT7AE0lwQThZPXlxDyCLKQ=="], + + "metro-config": ["metro-config@0.81.5", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.81.5", "metro-cache": "0.81.5", "metro-core": "0.81.5", "metro-runtime": "0.81.5" } }, "sha512-oDRAzUvj6RNRxratFdcVAqtAsg+T3qcKrGdqGZFUdwzlFJdHGR9Z413sW583uD2ynsuOjA2QB6US8FdwiBdNKg=="], + + "metro-core": ["metro-core@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.81.5" } }, "sha512-+2R0c8ByfV2N7CH5wpdIajCWa8escUFd8TukfoXyBq/vb6yTCsznoA25FhNXJ+MC/cz1L447Zj3vdUfCXIZBwg=="], + + "metro-file-map": ["metro-file-map@0.81.5", "", { "dependencies": { "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-mW1PKyiO3qZvjeeVjj1brhkmIotObA3/9jdbY1fQQYvEWM6Ml7bN/oJCRDGn2+bJRlG+J8pwyJ+DgdrM4BsKyg=="], + + "metro-minify-terser": ["metro-minify-terser@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-/mn4AxjANnsSS3/Bb+zA1G5yIS5xygbbz/OuPaJYs0CPcZCaWt66D+65j4Ft/nJkffUxcwE9mk4ubpkl3rjgtw=="], + + "metro-resolver": ["metro-resolver@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-6BX8Nq3g3go3FxcyXkVbWe7IgctjDTk6D9flq+P201DfHHQ28J+DWFpVelFcrNTn4tIfbP/Bw7u/0g2BGmeXfQ=="], + + "metro-runtime": ["metro-runtime@0.81.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-M/Gf71ictUKP9+77dV/y8XlAWg7xl76uhU7ggYFUwEdOHHWPG6gLBr1iiK0BmTjPFH8yRo/xyqMli4s3oGorPQ=="], + + "metro-source-map": ["metro-source-map@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.81.5", "nullthrows": "^1.1.1", "ob1": "0.81.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-Jz+CjvCKLNbJZYJTBeN3Kq9kIJf6b61MoLBdaOQZJ5Ajhw6Pf95Nn21XwA8BwfUYgajsi6IXsp/dTZsYJbN00Q=="], + + "metro-symbolicate": ["metro-symbolicate@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.81.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-X3HV3n3D6FuTE11UWFICqHbFMdTavfO48nXsSpnNGFkUZBexffu0Xd+fYKp+DJLNaQr3S+lAs8q9CgtDTlRRuA=="], + + "metro-transform-plugins": ["metro-transform-plugins@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-MmHhVx/1dJC94FN7m3oHgv5uOjKH8EX8pBeu1pnPMxbJrx6ZuIejO0k84zTSaQTZ8RxX1wqwzWBpXAWPjEX8mA=="], + + "metro-transform-worker": ["metro-transform-worker@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.81.5", "metro-babel-transformer": "0.81.5", "metro-cache": "0.81.5", "metro-cache-key": "0.81.5", "metro-minify-terser": "0.81.5", "metro-source-map": "0.81.5", "metro-transform-plugins": "0.81.5", "nullthrows": "^1.1.1" } }, "sha512-lUFyWVHa7lZFRSLJEv+m4jH8WrR5gU7VIjUlg4XmxQfV8ngY4V10ARKynLhMYPeQGl7Qvf+Ayg0eCZ272YZ4Mg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.7", "", {}, "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "moti": ["moti@0.29.0", "", { "dependencies": { "framer-motion": "^6.5.1" }, "peerDependencies": { "react-native-reanimated": "*" } }, "sha512-o/blVE3lm0i/6E5X0RLK59SVWEGxo7pQh8dTm+JykVCYY9bcz0lWyZFCO1s+MMNq+nMsSZBX8lkp4im/AZmhyw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nano-spawn": ["nano-spawn@1.0.1", "", {}, "sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-postinstall": ["napi-postinstall@0.2.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "node-dir": ["node-dir@0.1.17", "", { "dependencies": { "minimatch": "^3.0.2" } }, "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "npm-package-arg": ["npm-package-arg@11.0.3", "", { "dependencies": { "hosted-git-info": "^7.0.0", "proc-log": "^4.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + + "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], + + "ob1": ["ob1@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-iNpbeXPLmaiT9I5g16gFFFjsF3sGxLpYG2EGP3dfFB4z+l9X60mp/yRzStHhMtuNt8qmf7Ww80nOPQHngHhnIQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + + "parse-png": ["parse-png@2.1.0", "", { "dependencies": { "pngjs": "^3.3.0" } }, "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + + "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], + + "popmotion": ["popmotion@11.0.3", "", { "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", "style-value-types": "5.0.0", "tslib": "^2.1.0" } }, "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + + "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], + + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-content-loader": ["react-content-loader@7.0.2", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA=="], + + "react-devtools-core": ["react-devtools-core@5.3.2", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + + "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], + + "react-helmet-async": ["react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="], + + "react-hook-form": ["react-hook-form@7.58.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-native": ["react-native@0.76.9", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native/assets-registry": "0.76.9", "@react-native/codegen": "0.76.9", "@react-native/community-cli-plugin": "0.76.9", "@react-native/gradle-plugin": "0.76.9", "@react-native/js-polyfills": "0.76.9", "@react-native/normalize-colors": "0.76.9", "@react-native/virtualized-lists": "0.76.9", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "^0.23.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.81.0", "metro-source-map": "^0.81.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^5.3.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "^18.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg=="], + + "react-native-gesture-handler": ["react-native-gesture-handler@2.20.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg=="], + + "react-native-helmet-async": ["react-native-helmet-async@2.0.4", "", { "dependencies": { "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-m3CkXWss6B1dd6mCMleLpzDCJJGGaHOLQsUzZv8kAASJmMfmVT4d2fx375iXKTRWT25ThBfae3dECuX5cq/8hg=="], + + "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.1.7", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w=="], + + "react-native-reanimated": ["react-native-reanimated@3.16.7", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-qoUUQOwE1pHlmQ9cXTJ2MX9FQ9eHllopCLiWOkDkp6CER95ZWeXhJCP4cSm6AD4jigL5jHcZf/SkWrg8ttZUsw=="], + + "react-native-safe-area-context": ["react-native-safe-area-context@5.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA=="], + + "react-native-screens": ["react-native-screens@4.4.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg=="], + + "react-native-svg": ["react-native-svg@15.12.0", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A=="], + + "react-native-toast-message": ["react-native-toast-message@2.3.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-d7LldTK1ei1Bl7RFhoOYw8hVQ4oKPQHORYI//xR9Pyz3HxSlFlvQbueE5X3KLoemRRgBrOUg3zY6DxXnxrVLRg=="], + + "react-native-web": ["react-native-web@0.19.13", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A=="], + + "react-native-webview": ["react-native-webview@13.12.5", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-INOKPom4dFyzkbxbkuQNfeRG9/iYnyRDzrDkJeyvSWgJAW2IDdJkWFJBS2v0RxIL4gqLgHkiIZDOfiLaNnw83Q=="], + + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + + "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-server-dom-webpack": ["react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1" }, "peerDependencies": { "react": "19.0.0-rc-6230622a1a-20240610", "react-dom": "19.0.0-rc-6230622a1a-20240610", "webpack": "^5.59.0" } }, "sha512-nr+IsOVD07QdeCr4BLvR5TALfLaZLi9AIaoa6vXymBc051iDPWedJujYYrjRJy5+9jp9oCx3G8Tt/Bs//TckJw=="], + + "react-shallow-renderer": ["react-shallow-renderer@16.15.0", "", { "dependencies": { "object-assign": "^4.1.1", "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-test-renderer": ["react-test-renderer@18.3.1", "", { "dependencies": { "react-is": "^18.3.1", "react-shallow-renderer": "^16.15.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readline": ["readline@1.3.0", "", {}, "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="], + + "recast": ["recast@0.21.5", "", { "dependencies": { "ast-types": "0.15.2", "esprima": "~4.0.0", "source-map": "~0.6.1", "tslib": "^2.0.1" } }, "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg=="], + + "recyclerlistview": ["recyclerlistview@4.2.1", "", { "dependencies": { "lodash.debounce": "4.0.8", "prop-types": "15.8.1", "ts-object-utils": "0.0.5" }, "peerDependencies": { "react": ">= 15.2.1", "react-native": ">= 0.30.0" } }, "sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA=="], + + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], + + "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], + + "regjsparser": ["regjsparser@0.12.0", "", { "dependencies": { "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ=="], + + "remove-trailing-slash": ["remove-trailing-slash@0.1.1", "", {}, "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="], + + "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], + + "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="], + + "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sf-symbols-typescript": ["sf-symbols-typescript@2.1.0", "", {}, "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A=="], + + "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], + + "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], + + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + + "stack-generator": ["stack-generator@2.0.10", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + + "stacktrace-gps": ["stacktrace-gps@3.1.2", "", { "dependencies": { "source-map": "0.5.6", "stackframe": "^1.3.4" } }, "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ=="], + + "stacktrace-js": ["stacktrace-js@2.0.2", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="], + + "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], + + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-length": ["string-length@5.0.1", "", { "dependencies": { "char-regex": "^2.0.0", "strip-ansi": "^7.0.1" } }, "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], + + "style-value-types": ["style-value-types@5.0.0", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "^2.1.0" } }, "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA=="], + + "styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "synckit": ["synckit@0.11.5", "", { "dependencies": { "@pkgr/core": "^0.2.4", "tslib": "^2.8.1" } }, "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA=="], + + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + + "tamagui": ["tamagui@1.126.12", "", { "dependencies": { "@tamagui/accordion": "1.126.12", "@tamagui/adapt": "1.126.12", "@tamagui/alert-dialog": "1.126.12", "@tamagui/animate-presence": "1.126.12", "@tamagui/avatar": "1.126.12", "@tamagui/button": "1.126.12", "@tamagui/card": "1.126.12", "@tamagui/checkbox": "1.126.12", "@tamagui/compose-refs": "1.126.12", "@tamagui/constants": "1.126.12", "@tamagui/core": "1.126.12", "@tamagui/create-context": "1.126.12", "@tamagui/dialog": "1.126.12", "@tamagui/elements": "1.126.12", "@tamagui/fake-react-native": "1.126.12", "@tamagui/focusable": "1.126.12", "@tamagui/font-size": "1.126.12", "@tamagui/form": "1.126.12", "@tamagui/get-button-sized": "1.126.12", "@tamagui/get-font-sized": "1.126.12", "@tamagui/get-token": "1.126.12", "@tamagui/group": "1.126.12", "@tamagui/helpers-tamagui": "1.126.12", "@tamagui/image": "1.126.12", "@tamagui/label": "1.126.12", "@tamagui/linear-gradient": "1.126.12", "@tamagui/list-item": "1.126.12", "@tamagui/polyfill-dev": "1.126.12", "@tamagui/popover": "1.126.12", "@tamagui/popper": "1.126.12", "@tamagui/portal": "1.126.12", "@tamagui/progress": "1.126.12", "@tamagui/radio-group": "1.126.12", "@tamagui/react-native-media-driver": "1.126.12", "@tamagui/scroll-view": "1.126.12", "@tamagui/select": "1.126.12", "@tamagui/separator": "1.126.12", "@tamagui/shapes": "1.126.12", "@tamagui/sheet": "1.126.12", "@tamagui/slider": "1.126.12", "@tamagui/stacks": "1.126.12", "@tamagui/switch": "1.126.12", "@tamagui/tabs": "1.126.12", "@tamagui/text": "1.126.12", "@tamagui/theme": "1.126.12", "@tamagui/toggle-group": "1.126.12", "@tamagui/tooltip": "1.126.12", "@tamagui/use-controllable-state": "1.126.12", "@tamagui/use-debounce": "1.126.12", "@tamagui/use-force-update": "1.126.12", "@tamagui/use-window-dimensions": "1.126.12", "@tamagui/visually-hidden": "1.126.12", "@tamagui/z-index-stack": "1.126.12" }, "peerDependencies": { "react": "*" } }, "sha512-+V8qEG7WQFIx4yFoq+8s39Wtuq6d+edFeA0AsQhBnyI4HfGw78zqTAWDfh9oVLchyOk8NdAmJhHwAj60LglClQ=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "temp": ["temp@0.8.4", "", { "dependencies": { "rimraf": "~2.6.2" } }, "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg=="], + + "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + + "tempy": ["tempy@0.7.1", "", { "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg=="], + + "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], + + "terser": ["terser@5.39.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.3.14", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + + "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + + "tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "ts-object-utils": ["ts-object-utils@0.0.5", "", {}, "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "ua-parser-js": ["ua-parser-js@1.0.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], + + "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], + + "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.0", "", {}, "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg=="], + + "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.1.0", "", {}, "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w=="], + + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + + "unique-filename": ["unique-filename@3.0.0", "", { "dependencies": { "unique-slug": "^4.0.0" } }, "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g=="], + + "unique-slug": ["unique-slug@4.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ=="], + + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "unrs-resolver": ["unrs-resolver@1.7.2", "", { "dependencies": { "napi-postinstall": "^0.2.2" }, "optionalDependencies": { "@unrs/resolver-binding-darwin-arm64": "1.7.2", "@unrs/resolver-binding-darwin-x64": "1.7.2", "@unrs/resolver-binding-freebsd-x64": "1.7.2", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", "@unrs/resolver-binding-linux-x64-musl": "1.7.2", "@unrs/resolver-binding-wasm32-wasi": "1.7.2", "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" } }, "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-latest-callback": ["use-latest-callback@0.2.3", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + + "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + + "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], + + "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="], + + "webpack": ["webpack@5.99.8", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ=="], + + "webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], + + "whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], + + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], + + "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "wonka": ["wonka@6.3.5", "", {}, "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], + + "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + + "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + + "xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="], + + "xmlbuilder": ["xmlbuilder@14.0.0", "", {}, "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + + "zustand": ["zustand@5.0.5", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg=="], + + "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "@babel/register/make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], + + "@commitlint/config-validator/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@commitlint/format/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "@commitlint/is-ignored/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@commitlint/load/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "@commitlint/read/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], + + "@commitlint/types/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@expo/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/cli/form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="], + + "@expo/cli/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + + "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + + "@expo/cli/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "@expo/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/cli/ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + + "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "@expo/config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@expo/config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/config-plugins/@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="], + + "@expo/config-plugins/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/config-plugins/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@expo/config-plugins/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "@expo/devcert/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@expo/env/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/fingerprint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/image-utils/fs-extra": ["fs-extra@9.0.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^1.0.0" } }, "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g=="], + + "@expo/image-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "@expo/metro-config/@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="], + + "@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/metro-config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@expo/package-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + + "@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.9", "", {}, "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw=="], + + "@expo/prebuild-config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "@expo/xcpretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/reporters/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "@jest/transform/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@npmcli/fs/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@react-native/babel-preset/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.25.1", "", { "dependencies": { "hermes-parser": "0.25.1" } }, "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ=="], + + "@react-native/community-cli-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@react-native/community-cli-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@react-native/dev-middleware/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@react-native/dev-middleware/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "@react-navigation/core/react-is": ["react-is@19.1.0", "", {}, "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg=="], + + "@tamagui/helpers-icon/@tamagui/core": ["@tamagui/core@1.129.2", "", { "dependencies": { "@tamagui/react-native-media-driver": "1.129.2", "@tamagui/react-native-use-pressable": "1.129.2", "@tamagui/react-native-use-responder-events": "1.129.2", "@tamagui/use-event": "1.129.2", "@tamagui/web": "1.129.2" } }, "sha512-73cQKIRuHw7VfuEL6xUd3UHubms3Xezrjho1Hocw+nU7gjRfVpzMvqYmCTkk20aUDZWk5ZWrN39cClX+mq6ETw=="], + + "@tamagui/lucide-icons/@tamagui/core": ["@tamagui/core@1.129.2", "", { "dependencies": { "@tamagui/react-native-media-driver": "1.129.2", "@tamagui/react-native-use-pressable": "1.129.2", "@tamagui/react-native-use-responder-events": "1.129.2", "@tamagui/use-event": "1.129.2", "@tamagui/web": "1.129.2" } }, "sha512-73cQKIRuHw7VfuEL6xUd3UHubms3Xezrjho1Hocw+nU7gjRfVpzMvqYmCTkk20aUDZWk5ZWrN39cClX+mq6ETw=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "babel-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "cacache/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], + + "chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "compressible/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + + "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "connect/finalhandler": ["finalhandler@1.1.2", "", { "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" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], + + "create-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + + "default-gateway/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], + + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "domexception/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "expo-build-properties/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "expo-dev-launcher/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="], + + "expo-modules-autolinking/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + + "express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "express/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], + + "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "find-cache-dir/make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], + + "find-cache-dir/pkg-dir": ["pkg-dir@3.0.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "istanbul-lib-instrument/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "jest-circus/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-circus/dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], + + "jest-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-each/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-resolve/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-resolve/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "jest-runner/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + + "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-snapshot/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-watch-select-projects/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + + "jest-watch-typeahead/ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], + + "jest-watch-typeahead/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-watch-typeahead/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "jest-watch-typeahead/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-watcher/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "jscodeshift/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jscodeshift/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], + + "jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "jsdom/ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + + "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "listr2/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "make-dir/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "metro/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "metro/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "metro/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "metro-babel-transformer/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], + + "metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "mkdirp/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "plist/@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="], + + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "raw-body/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "rc/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.9", "", {}, "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw=="], + + "react-native/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "react-native/scheduler": ["scheduler@0.24.0-canary-efb381bbf-20230505", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA=="], + + "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], + + "react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], + + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + + "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], + + "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "serve-static/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "simple-plist/bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], + + "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "stacktrace-gps/source-map": ["source-map@0.5.6", "", {}, "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA=="], + + "string-length/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "supports-hyperlinks/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + + "tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "terser-webpack-plugin/jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "tsconfig-paths/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "tsconfig-paths/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "xcode/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "@babel/register/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "@commitlint/top-level/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "@expo/cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/cli/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@expo/cli/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@expo/cli/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], + + "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + + "@expo/config-plugins/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "@expo/config-plugins/@expo/json-file/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], + + "@expo/config-plugins/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/config-plugins/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/config-plugins/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@expo/config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@expo/devcert/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@expo/env/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/env/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/fingerprint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/fingerprint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/image-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/image-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/image-utils/fs-extra/universalify": ["universalify@1.0.0", "", {}, "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="], + + "@expo/metro-config/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "@expo/metro-config/@expo/json-file/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], + + "@expo/metro-config/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/metro-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/metro-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@expo/package-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/package-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "@expo/package-manager/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], + + "@expo/package-manager/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + + "@expo/xcpretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@expo/xcpretty/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@jest/console/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/core/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/reporters/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/reporters/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "@jest/transform/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "@react-native/community-cli-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@react-native/community-cli-plugin/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/react-native-media-driver": ["@tamagui/react-native-media-driver@1.129.2", "", { "dependencies": { "@tamagui/web": "1.129.2" }, "peerDependencies": { "react-native": "*" } }, "sha512-0DbaeiHtwIlow7L3N122bqUU+8yIG83PDe1t/dvDi8EJvVghutI5s/xXJ4TlJcOVpKMMWbcpMv70Hu6Q52bhlA=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/react-native-use-pressable": ["@tamagui/react-native-use-pressable@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-smLo/dXK4SHiQ427QQc4Y9CbgLRWGV/UZqqRaEEztIgNkqCnfcg3ccKbCglmCDl+HmaJ77KNH1eOtevrfrjtUA=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/react-native-use-responder-events": ["@tamagui/react-native-use-responder-events@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-NyZ3PmN6Z+hbObiotolSfaZk9mmD097ir6sCeMc6h+H1d3hlwmESstd/E6Nvi711NL4wfX1kHN99ObU6UMN2/Q=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/use-event": ["@tamagui/use-event@1.129.2", "", { "dependencies": { "@tamagui/constants": "1.129.2" }, "peerDependencies": { "react": "*" } }, "sha512-LrCZS/iHmgxJ2+KbrzgXVgDvYWUiiapHqeP/GwldbyvVLmtJeB/DEol4/0yurZfAa+MeEiBooFZqU9ThYxNCkA=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web": ["@tamagui/web@1.129.2", "", { "dependencies": { "@tamagui/compose-refs": "1.129.2", "@tamagui/constants": "1.129.2", "@tamagui/helpers": "1.129.2", "@tamagui/normalize-css-color": "1.129.2", "@tamagui/timer": "1.129.2", "@tamagui/types": "1.129.2", "@tamagui/use-did-finish-ssr": "1.129.2", "@tamagui/use-event": "1.129.2", "@tamagui/use-force-update": "1.129.2" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-VGdrlJiXqY1hJKbQK+gHOZzaveBiHRCwvDg5ENmqCebiPlbk38iXfneA6HLAaXg8FnZDeRgIrMvwnmH5sC339A=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/react-native-media-driver": ["@tamagui/react-native-media-driver@1.129.2", "", { "dependencies": { "@tamagui/web": "1.129.2" }, "peerDependencies": { "react-native": "*" } }, "sha512-0DbaeiHtwIlow7L3N122bqUU+8yIG83PDe1t/dvDi8EJvVghutI5s/xXJ4TlJcOVpKMMWbcpMv70Hu6Q52bhlA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/react-native-use-pressable": ["@tamagui/react-native-use-pressable@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-smLo/dXK4SHiQ427QQc4Y9CbgLRWGV/UZqqRaEEztIgNkqCnfcg3ccKbCglmCDl+HmaJ77KNH1eOtevrfrjtUA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/react-native-use-responder-events": ["@tamagui/react-native-use-responder-events@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-NyZ3PmN6Z+hbObiotolSfaZk9mmD097ir6sCeMc6h+H1d3hlwmESstd/E6Nvi711NL4wfX1kHN99ObU6UMN2/Q=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/use-event": ["@tamagui/use-event@1.129.2", "", { "dependencies": { "@tamagui/constants": "1.129.2" }, "peerDependencies": { "react": "*" } }, "sha512-LrCZS/iHmgxJ2+KbrzgXVgDvYWUiiapHqeP/GwldbyvVLmtJeB/DEol4/0yurZfAa+MeEiBooFZqU9ThYxNCkA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web": ["@tamagui/web@1.129.2", "", { "dependencies": { "@tamagui/compose-refs": "1.129.2", "@tamagui/constants": "1.129.2", "@tamagui/helpers": "1.129.2", "@tamagui/normalize-css-color": "1.129.2", "@tamagui/timer": "1.129.2", "@tamagui/types": "1.129.2", "@tamagui/use-did-finish-ssr": "1.129.2", "@tamagui/use-event": "1.129.2", "@tamagui/use-force-update": "1.129.2" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-VGdrlJiXqY1hJKbQK+gHOZzaveBiHRCwvDg5ENmqCebiPlbk38iXfneA6HLAaXg8FnZDeRgIrMvwnmH5sC339A=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "babel-jest/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "babel-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "connect/finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "connect/finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], + + "connect/finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "create-jest/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "create-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "default-gateway/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "default-gateway/execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], + + "default-gateway/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "default-gateway/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "expo-build-properties/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "expo-dev-launcher/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "expo-modules-autolinking/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "expo-modules-autolinking/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "find-cache-dir/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "find-cache-dir/pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "inquirer/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "istanbul-lib-report/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-circus/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-circus/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-config/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-each/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-each/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-resolve/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-resolve/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-runner/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-runner/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-runner/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "jest-runtime/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-runtime/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-snapshot/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-snapshot/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-validate/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-validate/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watch-select-projects/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-watch-select-projects/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watch-typeahead/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-watch-typeahead/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watch-typeahead/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "jest-watcher/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-watcher/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watcher/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jscodeshift/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jscodeshift/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "listr2/wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], + + "metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + + "metro-file-map/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "metro/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "metro/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "metro/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "react-native/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "react-native/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-static/send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "string-length/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "terser-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "@expo/cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/cli/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "@expo/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "@expo/config-plugins/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@expo/devcert/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@expo/env/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/fingerprint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/image-utils/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/metro-config/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/metro-config/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@expo/package-manager/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "@expo/package-manager/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "@expo/xcpretty/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@jest/console/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/core/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/reporters/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/transform/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/types/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "@react-native/community-cli-plugin/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/use-event/@tamagui/constants": ["@tamagui/constants@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-jBJY3szH5vW0qJowLTzdpLabA9NlFk63IIPHrCLUIwf1ydeQxhkpsV7CZJGItSzSFi2+K1w44sdJJ99DHArgyg=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/compose-refs": ["@tamagui/compose-refs@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-BJbcTL/XdGpqiD+UzDN7WlZ9woR7ZyUvAEs97D+P+7CDjs3PYhwYnwwQYlrkgGonIdQShQzo2lX9BWurvSt3ZQ=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/constants": ["@tamagui/constants@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-jBJY3szH5vW0qJowLTzdpLabA9NlFk63IIPHrCLUIwf1ydeQxhkpsV7CZJGItSzSFi2+K1w44sdJJ99DHArgyg=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/helpers": ["@tamagui/helpers@1.129.2", "", { "dependencies": { "@tamagui/constants": "1.129.2", "@tamagui/simple-hash": "1.129.2" } }, "sha512-K5hg1L7/tFPz2fZUNm76HMom6Za6mUALLvywmWaLu2X60NQFx+9a2s8dkzkLGEVyMoynThqCsA9fN3fFnarOMQ=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/normalize-css-color": ["@tamagui/normalize-css-color@1.129.2", "", { "dependencies": { "@react-native/normalize-color": "^2.1.0" } }, "sha512-Yn2lqAvbGQfP5TyXBUqZkLEtigjmZZXDPVEiG87Dgz8G7jhrrFs/hmReuKdC603HwKrU94sU0CeAyQLawLFcpA=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/timer": ["@tamagui/timer@1.129.2", "", {}, "sha512-Pcw8h1HXdUgz5aQRUraBm4k27f5i2NdyzPg2OO2VbWa2WJ6+o8B9kJ/17Wr/pPJRfrJRD3WoQt3CZVezvgKklw=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/types": ["@tamagui/types@1.129.2", "", {}, "sha512-OIC6QtRsupwu0F9c4LEayi6AcMxNDMGPrtI/lJOzl+Q78TyUzeqK/qhmWE6o/iWGxCKo8Zdow/9Amo9Zf5qatg=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/use-did-finish-ssr": ["@tamagui/use-did-finish-ssr@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-o3xzXakZLu95GUF2etRsKA20Rg2uHL/hbTLI3Rt4Q0Bf51rmbrN2KsoZD+WAr86rlVMY1kDaek0udTztGMkpyA=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/use-force-update": ["@tamagui/use-force-update@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-VmTMTg03y3FRJ2s4kleQ3JQwzvhkAw+4YXa7mwLvIPu/PkkK3b2wIyx+/SXITiVvOP0IlSMDRv15fijl+JOQzA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/use-event/@tamagui/constants": ["@tamagui/constants@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-jBJY3szH5vW0qJowLTzdpLabA9NlFk63IIPHrCLUIwf1ydeQxhkpsV7CZJGItSzSFi2+K1w44sdJJ99DHArgyg=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/compose-refs": ["@tamagui/compose-refs@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-BJbcTL/XdGpqiD+UzDN7WlZ9woR7ZyUvAEs97D+P+7CDjs3PYhwYnwwQYlrkgGonIdQShQzo2lX9BWurvSt3ZQ=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/constants": ["@tamagui/constants@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-jBJY3szH5vW0qJowLTzdpLabA9NlFk63IIPHrCLUIwf1ydeQxhkpsV7CZJGItSzSFi2+K1w44sdJJ99DHArgyg=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/helpers": ["@tamagui/helpers@1.129.2", "", { "dependencies": { "@tamagui/constants": "1.129.2", "@tamagui/simple-hash": "1.129.2" } }, "sha512-K5hg1L7/tFPz2fZUNm76HMom6Za6mUALLvywmWaLu2X60NQFx+9a2s8dkzkLGEVyMoynThqCsA9fN3fFnarOMQ=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/normalize-css-color": ["@tamagui/normalize-css-color@1.129.2", "", { "dependencies": { "@react-native/normalize-color": "^2.1.0" } }, "sha512-Yn2lqAvbGQfP5TyXBUqZkLEtigjmZZXDPVEiG87Dgz8G7jhrrFs/hmReuKdC603HwKrU94sU0CeAyQLawLFcpA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/timer": ["@tamagui/timer@1.129.2", "", {}, "sha512-Pcw8h1HXdUgz5aQRUraBm4k27f5i2NdyzPg2OO2VbWa2WJ6+o8B9kJ/17Wr/pPJRfrJRD3WoQt3CZVezvgKklw=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/types": ["@tamagui/types@1.129.2", "", {}, "sha512-OIC6QtRsupwu0F9c4LEayi6AcMxNDMGPrtI/lJOzl+Q78TyUzeqK/qhmWE6o/iWGxCKo8Zdow/9Amo9Zf5qatg=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/use-did-finish-ssr": ["@tamagui/use-did-finish-ssr@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-o3xzXakZLu95GUF2etRsKA20Rg2uHL/hbTLI3Rt4Q0Bf51rmbrN2KsoZD+WAr86rlVMY1kDaek0udTztGMkpyA=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/use-force-update": ["@tamagui/use-force-update@1.129.2", "", { "peerDependencies": { "react": "*" } }, "sha512-VmTMTg03y3FRJ2s4kleQ3JQwzvhkAw+4YXa7mwLvIPu/PkkK3b2wIyx+/SXITiVvOP0IlSMDRv15fijl+JOQzA=="], + + "babel-jest/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "create-jest/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "default-gateway/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "default-gateway/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "default-gateway/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "default-gateway/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "default-gateway/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "expo-modules-autolinking/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "inquirer/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-circus/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-config/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-diff/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-each/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-matcher-utils/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-message-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-resolve/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-runner/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-runtime/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-snapshot/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-validate/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-watch-select-projects/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-watch-typeahead/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-watcher/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jscodeshift/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "listr2/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "listr2/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], + + "metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "metro/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "react-native/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "serve-static/send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "terser-webpack-plugin/jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "@expo/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@tamagui/helpers-icon/@tamagui/core/@tamagui/web/@tamagui/helpers/@tamagui/simple-hash": ["@tamagui/simple-hash@1.129.2", "", {}, "sha512-M9Jl/lk/rdxC9KMSdy9jZRITrInW0a+MBzbL0eZcGkEdiKo0dK25UWeZVsbM4z5fsihNPpCnwPulJ7l//KmlJg=="], + + "@tamagui/lucide-icons/@tamagui/core/@tamagui/web/@tamagui/helpers/@tamagui/simple-hash": ["@tamagui/simple-hash@1.129.2", "", {}, "sha512-M9Jl/lk/rdxC9KMSdy9jZRITrInW0a+MBzbL0eZcGkEdiKo0dK25UWeZVsbM4z5fsihNPpCnwPulJ7l//KmlJg=="], + + "default-gateway/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@commitlint/top-level/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], + + "@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "@expo/cli/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "@expo/package-manager/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + } +} diff --git a/projects/mobile/commitlint.config.js b/projects/mobile/commitlint.config.js new file mode 100644 index 0000000..ef93365 --- /dev/null +++ b/projects/mobile/commitlint.config.js @@ -0,0 +1,23 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + [ + "feat", // New feature + "fix", // Bug fix + "docs", // Documentation only changes + "style", // Changes that do not affect the meaning of the code (white-space, formatting, etc) + "refactor", // Code changes that neither fixes a bug nor adds a feature + "perf", // Performance improvements + "test", // Adding missing tests or correcting existing tests + "build", // Changes that affect the build system or external dependencies + "ci", // Changes to CI configuration files and scripts + "chore", // Other changes that don't modify src or test files + "revert", // Reverts a previous commit + ], + ], + "subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]], + }, +}; diff --git a/projects/mobile/eas.json b/projects/mobile/eas.json new file mode 100644 index 0000000..a1a498e --- /dev/null +++ b/projects/mobile/eas.json @@ -0,0 +1,21 @@ +{ + "cli": { + "version": ">= 16.3.1", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": {} + } +} diff --git a/projects/mobile/eslint.config.js b/projects/mobile/eslint.config.js new file mode 100644 index 0000000..6d5fe64 --- /dev/null +++ b/projects/mobile/eslint.config.js @@ -0,0 +1,56 @@ +const { defineConfig } = require("eslint/config"); +const expoConfig = require("eslint-config-expo/flat"); +const prettierPlugin = require("eslint-plugin-prettier"); +const unusedImportsPlugin = require("eslint-plugin-unused-imports"); + +module.exports = defineConfig([ + expoConfig, + { + plugins: { + prettier: prettierPlugin, + "unused-imports": unusedImportsPlugin, + }, + rules: { + "import/default": "off", + "react/prop-types": "off", + "react/react-in-jsx-scope": "off", + "import/named": "off", + "import/namespace": "error", + "import/export": "error", + "no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "import/order": [ + "error", + { + groups: ["builtin", "external", "internal"], + pathGroups: [ + { + pattern: "react", + group: "external", + position: "before", + }, + ], + pathGroupsExcludedImportTypes: ["react"], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "prettier/prettier": "error", + }, + }, + { + ignores: ["dist/*"], + }, +]); diff --git a/projects/mobile/ios/.gitignore b/projects/mobile/ios/.gitignore new file mode 100644 index 0000000..8beb344 --- /dev/null +++ b/projects/mobile/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/projects/mobile/ios/.xcode.env b/projects/mobile/ios/.xcode.env new file mode 100644 index 0000000..3d5782c --- /dev/null +++ b/projects/mobile/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/projects/mobile/ios/Podfile b/projects/mobile/ios/Podfile new file mode 100644 index 0000000..50f409f --- /dev/null +++ b/projects/mobile/ios/Podfile @@ -0,0 +1,66 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] + +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' +install! 'cocoapods', + :deterministic_uuids => false + +prepare_react_native_project! + +target 'congonews' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', + ) + + # This is necessary for Xcode 14, because it signs resource bundles by default + # when building for devices. + installer.target_installation_results.pod_target_installation_results + .each do |pod_name, target_installation_result| + target_installation_result.resource_bundle_targets.each do |resource_bundle_target| + resource_bundle_target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end + end +end diff --git a/projects/mobile/ios/Podfile.lock b/projects/mobile/ios/Podfile.lock new file mode 100644 index 0000000..811c6f5 --- /dev/null +++ b/projects/mobile/ios/Podfile.lock @@ -0,0 +1,2573 @@ +PODS: + - boost (1.84.0) + - DoubleConversion (1.1.6) + - EXConstants (17.0.8): + - ExpoModulesCore + - EXJSONUtils (0.14.0) + - EXManifests (0.15.8): + - ExpoModulesCore + - Expo (52.0.46): + - ExpoModulesCore + - expo-dev-client (5.0.20): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (5.0.35): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Main (= 5.0.35) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Main (5.0.35): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Unsafe (5.0.35): + - DoubleConversion + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu (6.0.25): + - DoubleConversion + - expo-dev-menu/Main (= 6.0.25) + - expo-dev-menu/ReactNativeCompatibles (= 6.0.25) + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu-interface (1.9.3) + - expo-dev-menu/Main (6.0.25): + - DoubleConversion + - EXManifests + - expo-dev-menu-interface + - expo-dev-menu/Vendored + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/ReactNativeCompatibles (6.0.25): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/SafeAreaView (6.0.25): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/Vendored (6.0.25): + - DoubleConversion + - expo-dev-menu/SafeAreaView + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoAsset (11.0.5): + - ExpoModulesCore + - ExpoBlur (14.0.3): + - ExpoModulesCore + - ExpoFileSystem (18.0.12): + - ExpoModulesCore + - ExpoFont (13.0.4): + - ExpoModulesCore + - ExpoHaptics (14.0.1): + - ExpoModulesCore + - ExpoHead (4.0.21): + - ExpoModulesCore + - ExpoKeepAwake (14.0.3): + - ExpoModulesCore + - ExpoLinearGradient (14.0.2): + - ExpoModulesCore + - ExpoLinking (7.0.5): + - ExpoModulesCore + - ExpoLocalization (16.0.1): + - ExpoModulesCore + - ExpoModulesCore (2.2.3): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoNetwork (7.0.5): + - ExpoModulesCore + - ExpoSecureStore (14.2.3): + - ExpoModulesCore + - ExpoSplashScreen (0.29.24): + - ExpoModulesCore + - ExpoSymbols (0.2.2): + - ExpoModulesCore + - ExpoSystemUI (4.0.9): + - ExpoModulesCore + - ExpoWebBrowser (14.0.2): + - ExpoModulesCore + - EXUpdatesInterface (1.0.0): + - ExpoModulesCore + - fast_float (6.1.4) + - FBLazyVector (0.76.9) + - fmt (11.0.2) + - glog (0.3.5) + - hermes-engine (0.76.9): + - hermes-engine/Pre-built (= 0.76.9) + - hermes-engine/Pre-built (0.76.9) + - RCT-Folly (2024.10.14.00): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly/Default (= 2024.10.14.00) + - RCT-Folly/Default (2024.10.14.00): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly/Fabric (2024.10.14.00): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCTDeprecation (0.76.9) + - RCTRequired (0.76.9) + - RCTTypeSafety (0.76.9): + - FBLazyVector (= 0.76.9) + - RCTRequired (= 0.76.9) + - React-Core (= 0.76.9) + - React (0.76.9): + - React-Core (= 0.76.9) + - React-Core/DevSupport (= 0.76.9) + - React-Core/RCTWebSocket (= 0.76.9) + - React-RCTActionSheet (= 0.76.9) + - React-RCTAnimation (= 0.76.9) + - React-RCTBlob (= 0.76.9) + - React-RCTImage (= 0.76.9) + - React-RCTLinking (= 0.76.9) + - React-RCTNetwork (= 0.76.9) + - React-RCTSettings (= 0.76.9) + - React-RCTText (= 0.76.9) + - React-RCTVibration (= 0.76.9) + - React-callinvoker (0.76.9) + - React-Core (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default (= 0.76.9) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/CoreModulesHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/Default (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/DevSupport (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default (= 0.76.9) + - React-Core/RCTWebSocket (= 0.76.9) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTActionSheetHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTAnimationHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTBlobHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTImageHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTLinkingHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTNetworkHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTSettingsHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTTextHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTVibrationHeaders (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTWebSocket (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTDeprecation + - React-Core/Default (= 0.76.9) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-CoreModules (0.76.9): + - DoubleConversion + - fast_float + - fmt + - RCT-Folly + - RCTTypeSafety + - React-Core/CoreModulesHeaders + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTBlob + - React-RCTImage + - ReactCodegen + - ReactCommon + - SocketRocket + - React-cxxreact (0.76.9): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-callinvoker + - React-debug + - React-jsi + - React-jsinspector + - React-logger + - React-perflogger + - React-runtimeexecutor + - React-timing + - React-debug (0.76.9) + - React-defaultsnativemodule (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-domnativemodule + - React-Fabric + - React-featureflags + - React-featureflagsnativemodule + - React-graphics + - React-idlecallbacksnativemodule + - React-ImageManager + - React-microtasksnativemodule + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-domnativemodule (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricComponents + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/animations (= 0.76.9) + - React-Fabric/attributedstring (= 0.76.9) + - React-Fabric/componentregistry (= 0.76.9) + - React-Fabric/componentregistrynative (= 0.76.9) + - React-Fabric/components (= 0.76.9) + - React-Fabric/core (= 0.76.9) + - React-Fabric/dom (= 0.76.9) + - React-Fabric/imagemanager (= 0.76.9) + - React-Fabric/leakchecker (= 0.76.9) + - React-Fabric/mounting (= 0.76.9) + - React-Fabric/observers (= 0.76.9) + - React-Fabric/scheduler (= 0.76.9) + - React-Fabric/telemetry (= 0.76.9) + - React-Fabric/templateprocessor (= 0.76.9) + - React-Fabric/uimanager (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/animations (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistry (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistrynative (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.76.9) + - React-Fabric/components/root (= 0.76.9) + - React-Fabric/components/view (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/legacyviewmanagerinterop (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/root (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/view (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric/core (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/dom (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/imagemanager (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/leakchecker (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/mounting (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers/events (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/scheduler (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancetimeline + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/telemetry (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/templateprocessor (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager/consistency (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-FabricComponents (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.76.9) + - React-FabricComponents/textlayoutmanager (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.76.9) + - React-FabricComponents/components/iostextinput (= 0.76.9) + - React-FabricComponents/components/modal (= 0.76.9) + - React-FabricComponents/components/rncore (= 0.76.9) + - React-FabricComponents/components/safeareaview (= 0.76.9) + - React-FabricComponents/components/scrollview (= 0.76.9) + - React-FabricComponents/components/text (= 0.76.9) + - React-FabricComponents/components/textinput (= 0.76.9) + - React-FabricComponents/components/unimplementedview (= 0.76.9) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/inputaccessory (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/iostextinput (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/modal (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/rncore (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/safeareaview (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/scrollview (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/text (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/textinput (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/unimplementedview (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/textlayoutmanager (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - Yoga + - React-FabricImage (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Fabric + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - Yoga + - React-featureflags (0.76.9) + - React-featureflagsnativemodule (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-graphics (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly/Fabric + - React-jsi + - React-jsiexecutor + - React-utils + - React-hermes (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimeexecutor + - React-idlecallbacksnativemodule (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-ImageManager (0.76.9): + - glog + - RCT-Folly/Fabric + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - React-jserrorhandler (0.76.9): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - React-cxxreact + - React-debug + - React-jsi + - React-jsi (0.76.9): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-jsiexecutor (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-cxxreact + - React-jsi + - React-jsinspector + - React-perflogger + - React-jsinspector (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - React-featureflags + - React-jsi + - React-perflogger + - React-runtimeexecutor + - React-jsitracing (0.76.9): + - React-jsi + - React-logger (0.76.9): + - glog + - React-Mapbuffer (0.76.9): + - glog + - React-debug + - React-microtasksnativemodule (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 5.4.0) + - react-native-safe-area-context/fabric (= 5.4.0) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-webview (13.12.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-nativeconfig (0.76.9) + - React-NativeModulesApple (0.76.9): + - glog + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-jsi + - React-jsinspector + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-perflogger (0.76.9): + - DoubleConversion + - RCT-Folly (= 2024.10.14.00) + - React-performancetimeline (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - React-cxxreact + - React-timing + - React-RCTActionSheet (0.76.9): + - React-Core/RCTActionSheetHeaders (= 0.76.9) + - React-RCTAnimation (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - RCTTypeSafety + - React-Core/RCTAnimationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTAppDelegate (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-nativeconfig + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-RCTNetwork + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactCodegen + - ReactCommon + - React-RCTBlob (0.76.9): + - DoubleConversion + - fast_float + - fmt + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTNetwork + - ReactCodegen + - ReactCommon + - React-RCTFabric (0.76.9): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - React-Core + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-nativeconfig + - React-performancetimeline + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - Yoga + - React-RCTImage (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - RCTTypeSafety + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTNetwork + - ReactCodegen + - ReactCommon + - React-RCTLinking (0.76.9): + - React-Core/RCTLinkingHeaders (= 0.76.9) + - React-jsi (= 0.76.9) + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - ReactCommon/turbomodule/core (= 0.76.9) + - React-RCTNetwork (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - RCTTypeSafety + - React-Core/RCTNetworkHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTSettings (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - RCTTypeSafety + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-RCTText (0.76.9): + - React-Core/RCTTextHeaders (= 0.76.9) + - Yoga + - React-RCTVibration (0.76.9): + - RCT-Folly (= 2024.10.14.00) + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCodegen + - ReactCommon + - React-rendererconsistency (0.76.9) + - React-rendererdebug (0.76.9): + - DoubleConversion + - fast_float + - fmt + - RCT-Folly + - React-debug + - React-rncore (0.76.9) + - React-RuntimeApple (0.76.9): + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - React-callinvoker + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - React-RuntimeCore (0.76.9): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - React-runtimeexecutor (0.76.9): + - React-jsi (= 0.76.9) + - React-RuntimeHermes (0.76.9): + - hermes-engine + - RCT-Folly/Fabric (= 2024.10.14.00) + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsitracing + - React-nativeconfig + - React-RuntimeCore + - React-utils + - React-runtimescheduler (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - React-callinvoker + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - React-timing (0.76.9) + - React-utils (0.76.9): + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - React-debug + - React-jsi (= 0.76.9) + - ReactCodegen (0.76.9): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactCommon (0.76.9): + - ReactCommon/turbomodule (= 0.76.9) + - ReactCommon/turbomodule (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-callinvoker + - React-cxxreact + - React-jsi + - React-logger + - React-perflogger + - ReactCommon/turbomodule/bridging (= 0.76.9) + - ReactCommon/turbomodule/core (= 0.76.9) + - ReactCommon/turbomodule/bridging (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-callinvoker + - React-cxxreact + - React-jsi (= 0.76.9) + - React-logger + - React-perflogger + - ReactCommon/turbomodule/core (0.76.9): + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - React-callinvoker + - React-cxxreact + - React-debug (= 0.76.9) + - React-featureflags (= 0.76.9) + - React-jsi + - React-logger + - React-perflogger + - React-utils (= 0.76.9) + - RNFlashList (1.7.3): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNGestureHandler (2.20.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated (= 3.16.7) + - RNReanimated/worklets (= 3.16.7) + - Yoga + - RNReanimated/reanimated (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated/apple (= 3.16.7) + - Yoga + - RNReanimated/reanimated/apple (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated/worklets (3.16.7): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (4.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 4.4.0) + - Yoga + - RNScreens/common (4.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNSentry (6.15.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Sentry/HybridSDK (= 8.52.1) + - Yoga + - RNSVG (15.12.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNSVG/common (= 15.12.0) + - Yoga + - RNSVG/common (15.12.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - Sentry/HybridSDK (8.52.1) + - SocketRocket (0.7.1) + - Yoga (0.0.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXConstants (from `../node_modules/expo-constants/ios`) + - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) + - EXManifests (from `../node_modules/expo-manifests/ios`) + - Expo (from `../node_modules/expo`) + - expo-dev-client (from `../node_modules/expo-dev-client/ios`) + - expo-dev-launcher (from `../node_modules/expo-dev-launcher`) + - expo-dev-menu (from `../node_modules/expo-dev-menu`) + - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) + - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoBlur (from `../node_modules/expo-blur/ios`) + - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) + - ExpoFont (from `../node_modules/expo-font/ios`) + - ExpoHaptics (from `../node_modules/expo-haptics/ios`) + - ExpoHead (from `../node_modules/expo-router/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) + - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoLocalization (from `../node_modules/expo-localization/ios`) + - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoNetwork (from `../node_modules/expo-network/ios`) + - ExpoSecureStore (from `../node_modules/expo-secure-store/ios`) + - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) + - ExpoSymbols (from `../node_modules/expo-symbols/ios`) + - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) + - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) + - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`) + - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) + - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactCodegen (from `build/generated/ios`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNFlashList (from `../node_modules/@shopify/flash-list`)" + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - "RNSentry (from `../node_modules/@sentry/react-native`)" + - RNSVG (from `../node_modules/react-native-svg`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - Sentry + - SocketRocket + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXConstants: + :path: "../node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../node_modules/expo-json-utils/ios" + EXManifests: + :path: "../node_modules/expo-manifests/ios" + Expo: + :path: "../node_modules/expo" + expo-dev-client: + :path: "../node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../node_modules/expo-dev-menu-interface/ios" + ExpoAsset: + :path: "../node_modules/expo-asset/ios" + ExpoBlur: + :path: "../node_modules/expo-blur/ios" + ExpoFileSystem: + :path: "../node_modules/expo-file-system/ios" + ExpoFont: + :path: "../node_modules/expo-font/ios" + ExpoHaptics: + :path: "../node_modules/expo-haptics/ios" + ExpoHead: + :path: "../node_modules/expo-router/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" + ExpoLinearGradient: + :path: "../node_modules/expo-linear-gradient/ios" + ExpoLinking: + :path: "../node_modules/expo-linking/ios" + ExpoLocalization: + :path: "../node_modules/expo-localization/ios" + ExpoModulesCore: + :path: "../node_modules/expo-modules-core" + ExpoNetwork: + :path: "../node_modules/expo-network/ios" + ExpoSecureStore: + :path: "../node_modules/expo-secure-store/ios" + ExpoSplashScreen: + :path: "../node_modules/expo-splash-screen/ios" + ExpoSymbols: + :path: "../node_modules/expo-symbols/ios" + ExpoSystemUI: + :path: "../node_modules/expo-system-ui/ios" + ExpoWebBrowser: + :path: "../node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../node_modules/expo-updates-interface/ios" + fast_float: + :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + fmt: + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2024-11-12-RNv0.76.2-5b4aa20c719830dcf5684832b89a6edb95ac3d64 + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" + React-nativeconfig: + :path: "../node_modules/react-native/ReactCommon" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactCodegen: + :path: build/generated/ios + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNFlashList: + :path: "../node_modules/@shopify/flash-list" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + RNSentry: + :path: "../node_modules/@sentry/react-native" + RNSVG: + :path: "../node_modules/react-native-svg" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + boost: 1dca942403ed9342f98334bf4c3621f011aa7946 + DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b + EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 + EXManifests: a19d50504b8826546a4782770317bc83fffec87d + Expo: a6ff273c618506b12129a0d06f2a08201bfbcf43 + expo-dev-client: db44302cdbe0ec55b0ef1849c9a23a76dec6dbac + expo-dev-launcher: 792cd1c83fbec4a1a66fe91a0c283368dbad851c + expo-dev-menu: dd3197d2b0107ee036ffd85f95e75a950ab52ada + expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4 + ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 + ExpoBlur: 392c1207f71d0ecf22371621c1fbd44ba84d9742 + ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 + ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 + ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d + ExpoHead: df924203fbf8e0913fc38b0f6aec71f9a9115482 + ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 + ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60 + ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 + ExpoLocalization: 7776ea3bdb112125390745bbaf919b734b2ad1c7 + ExpoModulesCore: c25d77625038b1968ea1afefc719862c0d8dd993 + ExpoNetwork: 16083eb5ed34ce1c8e916fb6292fdb6e9daac0ca + ExpoSecureStore: b367d9f62c9102d808afbeb1561636d4276e439d + ExpoSplashScreen: 8261985ce9778f904abc7e31bed3538dce67ed4d + ExpoSymbols: f3002db15156cd4e505c77b6ea1df5c984db9965 + ExpoSystemUI: b82a45cf0f6a4fa18d07c46deba8725dd27688b4 + ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3 + EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db + fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 + FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 + fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 + glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 + RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 + RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f + React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d + React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab + React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 + React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e + React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 + React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c + React-featureflagsnativemodule: 742a8325b3c821d2a1ca13a6d2a0fc72d04555e0 + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: d61d9c9816131bf70d3d80cd04889fc625ee523f + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead + react-native-safe-area-context: c68127652d8b9a26a28ac9597167a3ad90bcd713 + react-native-webview: 6b9fc65c1951203a3e958ff3cc0a858d4b6be901 + React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc + React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 345a6f1b82abc578437df0ce7e9c48740eca827c + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: 007b1a98201cc49b5bc6e1417d7fe3f6fc6e2b78 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 + React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec + React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb + React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d + React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + RNFlashList: 648e2273693600985aaa72a2ef4700e815500901 + RNGestureHandler: fffddeb8af59709c6d8de11b6461a6af63cad532 + RNReanimated: 2e5069649cbab2c946652d3b97589b2ae0526220 + RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1 + RNSentry: ac378c5d235ecca7b574e09d9b293bb54217702d + RNSVG: 43aff28bae846abe9c45625fe8b4816b7ac73ffd + Sentry: 2cbbe3592f30050c60e916c63c7f5a2fa584005e + SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a + +PODFILE CHECKSUM: 0a3ac74460338901ee05a7a3af48752c68fd63da + +COCOAPODS: 1.16.2 diff --git a/projects/mobile/ios/Podfile.properties.json b/projects/mobile/ios/Podfile.properties.json new file mode 100644 index 0000000..5f99b5d --- /dev/null +++ b/projects/mobile/ios/Podfile.properties.json @@ -0,0 +1,8 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "newArchEnabled": "true", + "apple.extraPods": "[]", + "apple.ccacheEnabled": "false", + "apple.privacyManifestAggregationEnabled": "true" +} diff --git a/projects/mobile/ios/congonews.xcodeproj/project.pbxproj b/projects/mobile/ios/congonews.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8931dbb --- /dev/null +++ b/projects/mobile/ios/congonews.xcodeproj/project.pbxproj @@ -0,0 +1,565 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 6FEE8534B0014B26866C6E3E /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A042A0F86314AE083C51458 /* noop-file.swift */; }; + 96905EF65AED1B983A6B3ABC /* libPods-congonews.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-congonews.a */; }; + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; + B29BD45696CCAB45765E0F16 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4CB82215718D17B6B9FD7D09 /* PrivacyInfo.xcprivacy */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* congonews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = congonews.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = congonews/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = congonews/AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = congonews/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = congonews/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = congonews/main.m; sourceTree = ""; }; + 4CB82215718D17B6B9FD7D09 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = congonews/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-congonews.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-congonews.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A042A0F86314AE083C51458 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "congonews/noop-file.swift"; sourceTree = ""; }; + 6C2E3173556A471DD304B334 /* Pods-congonews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-congonews.debug.xcconfig"; path = "Target Support Files/Pods-congonews/Pods-congonews.debug.xcconfig"; sourceTree = ""; }; + 7A4D352CD337FB3A3BF06240 /* Pods-congonews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-congonews.release.xcconfig"; path = "Target Support Files/Pods-congonews/Pods-congonews.release.xcconfig"; sourceTree = ""; }; + 9C2D8B3824F04E9ABC110642 /* congonews-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "congonews-Bridging-Header.h"; path = "congonews/congonews-Bridging-Header.h"; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = congonews/SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-congonews/ExpoModulesProvider.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 96905EF65AED1B983A6B3ABC /* libPods-congonews.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* congonews */ = { + isa = PBXGroup; + children = ( + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB71A68108700A75B9A /* main.m */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 6A042A0F86314AE083C51458 /* noop-file.swift */, + 9C2D8B3824F04E9ABC110642 /* congonews-Bridging-Header.h */, + 4CB82215718D17B6B9FD7D09 /* PrivacyInfo.xcprivacy */, + ); + name = congonews; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-congonews.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* congonews */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + D65327D7A22EEC0BE12398D9 /* Pods */, + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* congonews.app */, + ); + name = Products; + sourceTree = ""; + }; + 92DBD88DE9BF7D494EA9DA96 /* congonews */ = { + isa = PBXGroup; + children = ( + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */, + ); + name = congonews; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = congonews/Supporting; + sourceTree = ""; + }; + D65327D7A22EEC0BE12398D9 /* Pods */ = { + isa = PBXGroup; + children = ( + 6C2E3173556A471DD304B334 /* Pods-congonews.debug.xcconfig */, + 7A4D352CD337FB3A3BF06240 /* Pods-congonews.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 92DBD88DE9BF7D494EA9DA96 /* congonews */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* congonews */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "congonews" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + 11378ABF61A5BF00615FBD20 /* [Expo] Configure project */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + D4611507A3C3479CA9D72986 /* Upload Debug Symbols to Sentry */, + 23D738AB1D34F3A280EB9CA3 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = congonews; + productName = congonews; + productReference = 13B07F961A680F5B00A75B9A /* congonews.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "congonews" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* congonews */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + B29BD45696CCAB45765E0F16 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-congonews-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 11378ABF61A5BF00615FBD20 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-congonews/expo-configure-project.sh\"\n"; + }; + 23D738AB1D34F3A280EB9CA3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-congonews/Pods-congonews-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-congonews/Pods-congonews-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-congonews/Pods-congonews-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-congonews/Pods-congonews-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D4611507A3C3479CA9D72986 /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, + 6FEE8534B0014B26866C6E3E /* noop-file.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-congonews.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = congonews/congonews.entitlements; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = congonews/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = dev.ngandu.congonews; + PRODUCT_NAME = congonews; + SWIFT_OBJC_BRIDGING_HEADER = "congonews/congonews-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-congonews.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = congonews/congonews.entitlements; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = congonews/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = dev.ngandu.congonews; + PRODUCT_NAME = congonews; + SWIFT_OBJC_BRIDGING_HEADER = "congonews/congonews-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "congonews" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "congonews" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/projects/mobile/ios/congonews.xcodeproj/xcshareddata/xcschemes/congonews.xcscheme b/projects/mobile/ios/congonews.xcodeproj/xcshareddata/xcschemes/congonews.xcscheme new file mode 100644 index 0000000..06f4151 --- /dev/null +++ b/projects/mobile/ios/congonews.xcodeproj/xcshareddata/xcschemes/congonews.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/mobile/ios/congonews.xcworkspace/contents.xcworkspacedata b/projects/mobile/ios/congonews.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0e26fd1 --- /dev/null +++ b/projects/mobile/ios/congonews.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/projects/mobile/ios/congonews/AppDelegate.h b/projects/mobile/ios/congonews/AppDelegate.h new file mode 100644 index 0000000..1658a43 --- /dev/null +++ b/projects/mobile/ios/congonews/AppDelegate.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface AppDelegate : EXAppDelegateWrapper + +@end diff --git a/projects/mobile/ios/congonews/AppDelegate.mm b/projects/mobile/ios/congonews/AppDelegate.mm new file mode 100644 index 0000000..b27f832 --- /dev/null +++ b/projects/mobile/ios/congonews/AppDelegate.mm @@ -0,0 +1,62 @@ +#import "AppDelegate.h" + +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ + return [self bundleURL]; +} + +- (NSURL *)bundleURL +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +@end diff --git a/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..e3b5d54 Binary files /dev/null and b/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/Contents.json b/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/projects/mobile/ios/congonews/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/projects/mobile/ios/congonews/Images.xcassets/Contents.json b/projects/mobile/ios/congonews/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/projects/mobile/ios/congonews/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/projects/mobile/ios/congonews/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..15f02ab --- /dev/null +++ b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "components": { + "alpha": "1.000", + "blue": "1.00000000000000", + "green": "1.00000000000000", + "red": "1.00000000000000" + }, + "color-space": "srgb" + }, + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/Contents.json new file mode 100644 index 0000000..f65c008 --- /dev/null +++ b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "image@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "image@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image.png b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image.png new file mode 100644 index 0000000..ac9efb2 Binary files /dev/null and b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image.png differ diff --git a/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@2x.png new file mode 100644 index 0000000..84e8c78 Binary files /dev/null and b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@2x.png differ diff --git a/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@3x.png new file mode 100644 index 0000000..de861bd Binary files /dev/null and b/projects/mobile/ios/congonews/Images.xcassets/SplashScreenLogo.imageset/image@3x.png differ diff --git a/projects/mobile/ios/congonews/Info.plist b/projects/mobile/ios/congonews/Info.plist new file mode 100644 index 0000000..364dad9 --- /dev/null +++ b/projects/mobile/ios/congonews/Info.plist @@ -0,0 +1,85 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + congonews + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + congonews + dev.ngandu.congonews + + + + CFBundleURLSchemes + + exp+congonews + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Automatic + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/projects/mobile/ios/congonews/PrivacyInfo.xcprivacy b/projects/mobile/ios/congonews/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..5bb83c5 --- /dev/null +++ b/projects/mobile/ios/congonews/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + 0A2A.1 + 3B52.1 + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/projects/mobile/ios/congonews/SplashScreen.storyboard b/projects/mobile/ios/congonews/SplashScreen.storyboard new file mode 100644 index 0000000..158767f --- /dev/null +++ b/projects/mobile/ios/congonews/SplashScreen.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/projects/mobile/ios/congonews/Supporting/Expo.plist b/projects/mobile/ios/congonews/Supporting/Expo.plist new file mode 100644 index 0000000..750be02 --- /dev/null +++ b/projects/mobile/ios/congonews/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/projects/mobile/ios/congonews/congonews-Bridging-Header.h b/projects/mobile/ios/congonews/congonews-Bridging-Header.h new file mode 100644 index 0000000..e11d920 --- /dev/null +++ b/projects/mobile/ios/congonews/congonews-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/projects/mobile/ios/congonews/congonews.entitlements b/projects/mobile/ios/congonews/congonews.entitlements new file mode 100644 index 0000000..f683276 --- /dev/null +++ b/projects/mobile/ios/congonews/congonews.entitlements @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/projects/mobile/ios/congonews/main.m b/projects/mobile/ios/congonews/main.m new file mode 100644 index 0000000..25181b6 --- /dev/null +++ b/projects/mobile/ios/congonews/main.m @@ -0,0 +1,10 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} + diff --git a/projects/mobile/ios/congonews/noop-file.swift b/projects/mobile/ios/congonews/noop-file.swift new file mode 100644 index 0000000..b2ffafb --- /dev/null +++ b/projects/mobile/ios/congonews/noop-file.swift @@ -0,0 +1,4 @@ +// +// @generated +// A blank Swift file must be created for native modules with Swift files to work correctly. +// diff --git a/projects/mobile/ios/sentry.properties b/projects/mobile/ios/sentry.properties new file mode 100644 index 0000000..8757b6a --- /dev/null +++ b/projects/mobile/ios/sentry.properties @@ -0,0 +1,4 @@ +defaults.url=https://glitchtip.devscast.tech/ +defaults.org=devscast-software +defaults.project=drc-news-app +# Using SENTRY_AUTH_TOKEN environment variable \ No newline at end of file diff --git a/projects/mobile/metro.config.js b/projects/mobile/metro.config.js new file mode 100644 index 0000000..15d922a --- /dev/null +++ b/projects/mobile/metro.config.js @@ -0,0 +1,6 @@ +const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getSentryExpoConfig(__dirname); + +module.exports = config; diff --git a/projects/mobile/package.json b/projects/mobile/package.json new file mode 100644 index 0000000..efb8eb9 --- /dev/null +++ b/projects/mobile/package.json @@ -0,0 +1,147 @@ +{ + "name": "drc-news", + "main": "expo-router/entry", + "version": "1.0.0", + "scripts": { + "start": "expo start", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "test": "jest --watchAll", + "============= EAS BUILD =============": "", + "build:ios:dev": "eas build --profile development --platform ios", + "build:ios:sim": "eas build --profile dev-sim --platform ios", + "build:ios:prev": "eas build --profile preview --platform ios", + "build:ios:e2e": "eas build --profile ios-e2e --platform ios", + "build:android:dev": "eas build --profile development --platform android", + "build:android:sim": "eas build --profile dev-sim --platform android", + "build:android:prev": "eas build --profile preview --platform android", + "build:android:e2e": "eas build --profile android-e2e --platform android", + "build:android:prod": "eas build --profile production --platform android", + "build:ios:prod": "eas build --profile production --platform ios", + "===================== EAS SUBMIT =====================": "", + "eas:android:submit": "eas submit -p android --profile production", + "eas:ios:submit": "eas submit -p ios --profile production", + "=========== CODE STYLE ============": "", + "check-types": "tsc --noEmit", + "check": "prettier ./src --check", + "format": "prettier ./src --write", + "lint:check": "eslint ./src --debug", + "lint:fix": "eslint ./src --fix", + "============= HUSKY =============": "", + "prepare": "husky", + "commit": "cz", + "============= MISCELLANEOUS =============": "", + "delete:dstore": "find . -name '.DS_Store' -type f -delete" + }, + "lint-staged": { + "*.ts": [ + "prettier --write", + "eslint --fix" + ], + "*.tsx": [ + "prettier --write", + "eslint --fix" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + }, + "jest": { + "preset": "jest-expo" + }, + "overrides": { + "globals": "14.0.0" + }, + "dependencies": { + "@expo-google-fonts/inter": "^0.3.0", + "@expo/vector-icons": "^14.0.2", + "@hookform/resolvers": "^5.1.1", + "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/native": "^7.0.14", + "@sentry/react-native": "^6.15.1", + "@shopify/flash-list": "1.7.3", + "@tamagui/colors": "^1.126.1", + "@tamagui/config": "^1.126.1", + "@tamagui/linear-gradient": "^1.126.1", + "@tamagui/lucide-icons": "^1.129.2", + "@tamagui/theme-builder": "^1.126.1", + "@tanstack/react-query": "^5.74.4", + "axios": "^1.9.0", + "date-fns": "^4.1.0", + "expo": "~52.0.46", + "expo-blur": "~14.0.3", + "expo-build-properties": "~0.13.2", + "expo-constants": "~17.0.8", + "expo-dev-client": "~5.0.20", + "expo-font": "~13.0.4", + "expo-haptics": "~14.0.1", + "expo-linear-gradient": "~14.0.2", + "expo-linking": "~7.0.5", + "expo-localization": "~16.0.1", + "expo-network": "~7.0.5", + "expo-router": "~4.0.20", + "expo-secure-store": "^14.0.1", + "expo-splash-screen": "~0.29.24", + "expo-status-bar": "~2.0.1", + "expo-symbols": "~0.2.2", + "expo-system-ui": "~4.0.9", + "expo-web-browser": "~14.0.2", + "joi": "^17.13.3", + "qs": "^6.14.0", + "react": "18.3.1", + "react-content-loader": "^7.0.2", + "react-dom": "18.3.1", + "react-hook-form": "^7.58.1", + "react-native": "0.76.9", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", + "react-native-safe-area-context": "^5.4.0", + "react-native-screens": "~4.4.0", + "react-native-svg": "^15.11.2", + "react-native-toast-message": "^2.3.0", + "react-native-web": "~0.19.13", + "react-native-webview": "13.12.5", + "tamagui": "^1.126.1", + "zustand": "^5.0.5" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@types/jest": "^29.5.12", + "@types/qs": "^6.9.18", + "@types/react": "~18.3.12", + "@types/react-test-renderer": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "commitizen": "^4.3.1", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^9.26.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-expo": "^9.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-native": "^5.0.0", + "eslint-plugin-unused-imports": "^4.1.4", + "husky": "^9.1.7", + "jest": "^29.2.1", + "jest-expo": "~52.0.6", + "lint-staged": "^16.0.0", + "prettier": "^3.5.3", + "react-test-renderer": "18.3.1", + "typescript": "^5.3.3" + }, + "private": true +} diff --git a/projects/mobile/src/api/client.ts b/projects/mobile/src/api/client.ts new file mode 100644 index 0000000..b058eaa --- /dev/null +++ b/projects/mobile/src/api/client.ts @@ -0,0 +1,117 @@ +import axios, { AxiosInstance } from "axios"; + +import { RefreshTokenPayload, RefreshTokenResponse } from "@/api/schema/identity-and-access/login"; +import { clearTokens, getAccessToken, getRefreshToken, setTokens } from "@/store/auth"; + +const endpoint = process.env.EXPO_PUBLIC_API_URL!; +const client: AxiosInstance = axios.create({ + baseURL: endpoint, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, +}); + +let isAuthTokenRefreshing = false; +let failedRequestsQueue: ((token: string) => void)[] = []; + +const processFailedRequestsQueue = (token: string) => { + failedRequestsQueue.forEach(callback => callback(token)); + failedRequestsQueue = []; +}; + +// Wait for 120 seconds before timing out +axios.interceptors.request.use(config => { + config.timeout = 120_000; + return config; +}); + +// Add the Authorization header to all requests +client.interceptors.request.use(async config => { + const token = await getAccessToken(); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } + + return config; +}); + +// Handle 401 errors and refresh the token +client.interceptors.response.use( + response => response, + async error => { + const originalRequest = error.config; + const status = error.response?.status; + + if (status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + if (isAuthTokenRefreshing) { + return new Promise(resolve => { + failedRequestsQueue.push((token: string) => { + originalRequest.headers["Authorization"] = `Bearer ${token}`; + resolve(client(originalRequest)); + }); + }); + } + + isAuthTokenRefreshing = true; + + try { + const refreshToken = await getRefreshToken(); + if (!refreshToken) { + await clearTokens(); + return Promise.reject(error); + } + + const response = await axios.post(`${endpoint}/token/refresh`, { + refresh_token: refreshToken, + } as RefreshTokenPayload); + + const updatedToken = response.data.token; + await setTokens(updatedToken, refreshToken); + processFailedRequestsQueue(updatedToken); + + originalRequest.headers["Authorization"] = `Bearer ${updatedToken}`; + return client(originalRequest); + } catch (error) { + await clearTokens(); + return Promise.reject(error); + } finally { + isAuthTokenRefreshing = false; + } + } + + return Promise.reject(error); + } +); + +if (__DEV__) { + // Log HTTP requests and responses + client.interceptors.request.use( + async config => { + console.log("HTTP REQUEST", { + baseURL: config.baseURL, + url: config.url, + data: config.data, + }); + + return config; + }, + error => console.log(JSON.stringify(error)) + ); + + client.interceptors.response.use( + response => { + console.log("HTTP RESPONSE", { + stats: response.status, + data: response.data, + }); + + return response; + }, + error => console.log(JSON.stringify(error)) + ); +} + +export default client; diff --git a/projects/mobile/src/api/endpoint.ts b/projects/mobile/src/api/endpoint.ts new file mode 100644 index 0000000..c422e2b --- /dev/null +++ b/projects/mobile/src/api/endpoint.ts @@ -0,0 +1,33 @@ +export const endpoint = { + feedManagement: { + addArticleToBookmark: (bookmarkId: string, articleId: string) => + `feed/bookmarks/${bookmarkId}/articles/${articleId}`, + addCommentToArticle: (articleId: string) => `/feed/articles/${articleId}/comments`, + createBookmark: `/feed/bookmarks`, + deleteBookmark: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}`, + followSource: (sourceId: string) => `/feed/sources/${sourceId}/follow`, + getArticleCommentList: (articleId: string) => `/feed/articles/${articleId}/comments`, + getArticleDetails: (articleId: string) => `/feed/articles/${articleId}`, + getArticleOverviewList: `/feed/articles`, + getBookmarkList: `/feed/bookmarks`, + getBookmarkedArticlesList: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}/articles`, + getSourceArticleOverviewList: (sourceId: string) => `/feed/sources/${sourceId}/articles`, + getSourceDetails: (sourceId: string) => `/feed/sources/${sourceId}`, + getSourceOverviewList: `/feed/sources`, + removeArticleFromBookmark: (bookmarkId: string, articleId: string) => + `/feed/bookmarks/${bookmarkId}/articles/${articleId}`, + unfollowSource: (sourceId: string) => `/feed/sources/${sourceId}/unfollow`, + updateBookmark: (bookmarkId: string) => `/feed/bookmarks/${bookmarkId}`, + }, + identityAndAccess: { + login: "/login_check", + logout: "/token/invalidate", + register: "/register", + getUserProfile: "/me", + requestPassword: "/password/request", + confirmAccount: (token: string) => `/account/confirm/${token}`, + resetPassword: (token: string) => `/password/reset/${token}`, + unlockAccount: (token: string) => `/account/unlock/${token}`, + updatePassword: "/password/update", + }, +}; diff --git a/projects/mobile/src/api/request/feed-management/article.ts b/projects/mobile/src/api/request/feed-management/article.ts new file mode 100644 index 0000000..6775960 --- /dev/null +++ b/projects/mobile/src/api/request/feed-management/article.ts @@ -0,0 +1,19 @@ +import { endpoint } from "@/api/endpoint"; +import { Article, ArticleOverview, TrendingArticle } from "@/api/schema/feed-management/article"; +import { ArticleFilters, useGetQuery, usePaginatedInfiniteQuery, usePaginatedQuery } from "@/api/shared"; + +export const useArticleTrendingList = (filters: ArticleFilters = {}) => { + return usePaginatedQuery("/feed/trending", filters); +}; + +export const useArticleDetails = (articleId: string) => { + return useGetQuery
(endpoint.feedManagement.getArticleDetails(articleId)); +}; + +export const useArticleOverviewList = (filters: ArticleFilters = {}) => { + return usePaginatedQuery(endpoint.feedManagement.getArticleOverviewList, filters); +}; + +export const useInfiniteArticleOverviewList = (filters: ArticleFilters = {}) => { + return usePaginatedInfiniteQuery(endpoint.feedManagement.getArticleOverviewList, filters); +}; diff --git a/projects/mobile/src/api/request/feed-management/bookmark.ts b/projects/mobile/src/api/request/feed-management/bookmark.ts new file mode 100644 index 0000000..6d901c8 --- /dev/null +++ b/projects/mobile/src/api/request/feed-management/bookmark.ts @@ -0,0 +1,34 @@ +import { endpoint } from "@/api/endpoint"; +import { Bookmark, BookmarkedArticle, BookmarkPayload } from "@/api/schema/feed-management/bookmark"; +import { ArticleFilters, useDeleteQuery, usePaginatedInfiniteQuery, usePostQuery, usePutQuery } from "@/api/shared"; + +export const useCreateBookmark = () => { + return usePostQuery(endpoint.feedManagement.createBookmark); +}; + +export const useUpdateBookmark = (bookmarkId: string) => { + return usePutQuery(endpoint.feedManagement.updateBookmark(bookmarkId)); +}; + +export const useDeleteBookmark = (bookmarkId: string) => { + return useDeleteQuery(endpoint.feedManagement.deleteBookmark(bookmarkId)); +}; + +export const useAddArticleToBookmark = (bookmarkId: string, articleId: string) => { + return usePostQuery(endpoint.feedManagement.addArticleToBookmark(bookmarkId, articleId)); +}; + +export const useRemoveArticleFromBookmark = (bookmarkId: string, articleId: string) => { + return useDeleteQuery(endpoint.feedManagement.removeArticleFromBookmark(bookmarkId, articleId)); +}; + +export const useBookmarkList = (filters: ArticleFilters = {}) => { + return usePaginatedInfiniteQuery(endpoint.feedManagement.getBookmarkList, filters); +}; + +export const useBookmarkedArticlesList = (bookmarkId: string, filters: ArticleFilters = {}) => { + return usePaginatedInfiniteQuery( + endpoint.feedManagement.getBookmarkedArticlesList(bookmarkId), + filters + ); +}; diff --git a/projects/mobile/src/api/request/feed-management/comment.ts b/projects/mobile/src/api/request/feed-management/comment.ts new file mode 100644 index 0000000..cadbe5e --- /dev/null +++ b/projects/mobile/src/api/request/feed-management/comment.ts @@ -0,0 +1,15 @@ +import { endpoint } from "@/api/endpoint"; +import { Comment, CommentPayload } from "@/api/schema/feed-management/comment"; +import { useDeleteQuery, usePaginatedInfiniteQuery, usePostQuery } from "@/api/shared"; + +export const useArticleCommentList = (articleId: string) => { + return usePaginatedInfiniteQuery(endpoint.feedManagement.getArticleCommentList(articleId)); +}; + +export const useAddCommentToArticle = (articleId: string) => { + return usePostQuery(endpoint.feedManagement.addCommentToArticle(articleId)); +}; + +export const useRemoveCommentFromArticle = (articleId: string, commentId: string) => { + return useDeleteQuery(endpoint.feedManagement.removeArticleFromBookmark(articleId, commentId)); +}; diff --git a/projects/mobile/src/api/request/feed-management/source.ts b/projects/mobile/src/api/request/feed-management/source.ts new file mode 100644 index 0000000..8a442c8 --- /dev/null +++ b/projects/mobile/src/api/request/feed-management/source.ts @@ -0,0 +1,27 @@ +import { endpoint } from "@/api/endpoint"; +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { SourceDetails, SourceOverview } from "@/api/schema/feed-management/source"; +import { ArticleFilters, useDeleteQuery, useGetQuery, usePaginatedInfiniteQuery, usePostQuery } from "@/api/shared"; + +export const useSourceDetails = (sourceId: string) => { + return useGetQuery(endpoint.feedManagement.getSourceDetails(sourceId)); +}; + +export const useSourceOverviewList = (filters: ArticleFilters = {}) => { + return usePaginatedInfiniteQuery(endpoint.feedManagement.getSourceOverviewList, filters); +}; + +export const useSourceArticleOverviewList = (sourceId: string, filters: ArticleFilters = {}) => { + return usePaginatedInfiniteQuery( + endpoint.feedManagement.getSourceArticleOverviewList(sourceId), + filters + ); +}; + +export const useFollowSource = (sourceId: string) => { + return usePostQuery(endpoint.feedManagement.followSource(sourceId)); +}; + +export const useUnfollowSource = (sourceId: string) => { + return useDeleteQuery(endpoint.feedManagement.unfollowSource(sourceId)); +}; diff --git a/projects/mobile/src/api/request/identity-and-access/login.ts b/projects/mobile/src/api/request/identity-and-access/login.ts new file mode 100644 index 0000000..71c18f7 --- /dev/null +++ b/projects/mobile/src/api/request/identity-and-access/login.ts @@ -0,0 +1,15 @@ +import { endpoint } from "@/api/endpoint"; +import { LoginPayload, LoginResponse } from "@/api/schema/identity-and-access/login"; +import { useGetQuery, usePostQuery } from "@/api/shared"; + +export const useLogin = () => { + return usePostQuery(endpoint.identityAndAccess.login); +}; + +export const useLogout = () => { + return usePostQuery(endpoint.identityAndAccess.logout); +}; + +export const useUnlockAccount = (token: string) => { + return useGetQuery(endpoint.identityAndAccess.unlockAccount(token)); +}; diff --git a/projects/mobile/src/api/request/identity-and-access/password.ts b/projects/mobile/src/api/request/identity-and-access/password.ts new file mode 100644 index 0000000..be22a56 --- /dev/null +++ b/projects/mobile/src/api/request/identity-and-access/password.ts @@ -0,0 +1,19 @@ +import { endpoint } from "@/api/endpoint"; +import { + RequestPasswordPayload, + ResetPasswordPayload, + UpdatePasswordPayload, +} from "@/api/schema/identity-and-access/password"; +import { usePostQuery, usePutQuery } from "@/api/shared"; + +export const usePasswordForgotten = () => { + return usePostQuery(endpoint.identityAndAccess.requestPassword); +}; + +export const usePasswordReset = (token: string) => { + return usePostQuery(endpoint.identityAndAccess.resetPassword(token)); +}; + +export const usePasswordUpdate = () => { + return usePutQuery(endpoint.identityAndAccess.updatePassword); +}; diff --git a/projects/mobile/src/api/request/identity-and-access/register.ts b/projects/mobile/src/api/request/identity-and-access/register.ts new file mode 100644 index 0000000..a5258f3 --- /dev/null +++ b/projects/mobile/src/api/request/identity-and-access/register.ts @@ -0,0 +1,11 @@ +import { endpoint } from "@/api/endpoint"; +import { RegisterPayload } from "@/api/schema/identity-and-access/register"; +import { useGetQuery, usePostQuery } from "@/api/shared"; + +export const useRegister = () => { + return usePostQuery(endpoint.identityAndAccess.register); +}; + +export const useConfirmAccount = (token: string) => { + return useGetQuery(endpoint.identityAndAccess.confirmAccount(token)); +}; diff --git a/projects/mobile/src/api/schema/feed-management/article.ts b/projects/mobile/src/api/schema/feed-management/article.ts new file mode 100644 index 0000000..450b99e --- /dev/null +++ b/projects/mobile/src/api/schema/feed-management/article.ts @@ -0,0 +1,45 @@ +import { SourceReference } from "@/api/schema/feed-management/source"; + +export type ArticleOverview = { + id: string; + title: string; + link: string; + categories: string[]; + excerpt: string; + source: SourceReference; + publishedAt: string; + image?: string; + readingTime: number; + bookmarked: boolean; +}; + +export type Article = { + id: string; + title: string; + link: string; + categories: string[]; + body: string; + source: SourceReference; + hash: string; + credibility: { + bias: "neutral" | "slightly" | "partisan" | "extreme"; + reliability: "trusted" | "reliable" | "average" | "unreliable" | "low_trust"; + transparency: "low" | "medium" | "high"; + }; + sentiment: "negative" | "positive" | "neutral"; + metadata?: { + title?: string; + description?: string; + image?: string; + video?: string; + audio?: string; + locale?: string; + }; + readingTime: number; + publishedAt: string; + crawledAt: string; + updatedAt: string; + bookmarked: boolean; +}; + +export type TrendingArticle = ArticleOverview; diff --git a/projects/mobile/src/api/schema/feed-management/bookmark.ts b/projects/mobile/src/api/schema/feed-management/bookmark.ts new file mode 100644 index 0000000..85113bc --- /dev/null +++ b/projects/mobile/src/api/schema/feed-management/bookmark.ts @@ -0,0 +1,30 @@ +import Joi from "joi"; + +import { ArticleOverview } from "@/api/schema/feed-management/article"; + +export type BookmarkPayload = { + name: string; + description?: string; + isPublic: boolean; +}; + +export type Bookmark = { + id: string; + name: string; + createdAt: string; + description?: string; + articlesCount: number; + isPublic: boolean; + updatedAt?: string; +}; + +export type BookmarkedArticle = ArticleOverview; + +export const BookmarkPayloadSchema = Joi.object({ + name: Joi.string().required().messages({ + "string.empty": "Le nom est requis", + "any.required": "Le nom est requis", + }), + description: Joi.string().optional(), + isPublic: Joi.boolean().optional(), +}); diff --git a/projects/mobile/src/api/schema/feed-management/comment.ts b/projects/mobile/src/api/schema/feed-management/comment.ts new file mode 100644 index 0000000..ca6b4ad --- /dev/null +++ b/projects/mobile/src/api/schema/feed-management/comment.ts @@ -0,0 +1,14 @@ +export type Comment = { + id: string; + content: string; + user: { + id: string; + name: string; + }; + sentiment: "positive" | "neutral" | "negative"; + createdAt: string; +}; + +export type CommentPayload = { + content: string; +}; diff --git a/projects/mobile/src/api/schema/feed-management/source.ts b/projects/mobile/src/api/schema/feed-management/source.ts new file mode 100644 index 0000000..66f737f --- /dev/null +++ b/projects/mobile/src/api/schema/feed-management/source.ts @@ -0,0 +1,54 @@ +export type SourceReference = { + id: string; + name: string; + displayName?: string; + image: string; + url: string; +}; + +export type SourceOverview = { + id: string; + name: string; + displayName?: string; + image: string; + url: string; + followed: boolean; +}; + +export type CategoryShare = { + name: string; + count: number; + percentage: number; +}; + +export type PublicationEntry = { + date: string; + count: number; +}; + +export type SourceDetails = { + id: string; + name: string; + url: string; + credibility: { + bias: "neutral" | "slightly" | "partisan" | "extreme"; + reliability: "trusted" | "reliable" | "average" | "unreliable" | "low_trust"; + transparency: "low" | "medium" | "high"; + }; + publicationGraph: { + items: PublicationEntry[]; + total: number; + }; + categoryShares: { + items: CategoryShare[]; + total: number; + }; + articlesCount: number; + crawledAt: string; + displayName?: string; + description?: string; + updatedAt?: string; + metadataAvailable: number; + followed: boolean; + image: string; +}; diff --git a/projects/mobile/src/api/schema/identity-and-access/login.ts b/projects/mobile/src/api/schema/identity-and-access/login.ts new file mode 100644 index 0000000..94a37e1 --- /dev/null +++ b/projects/mobile/src/api/schema/identity-and-access/login.ts @@ -0,0 +1,31 @@ +import Joi from "joi"; + +export type LoginPayload = { + username: string; + password: string; +}; + +export type LoginResponse = { + token: string; + refresh_token: string; +}; + +export type RefreshTokenPayload = { + refresh_token: string; +}; + +export type RefreshTokenResponse = { + token: string; +}; + +export const LoginPayloadSchema = Joi.object({ + username: Joi.string().required().messages({ + "string.empty": "L'email est requis", + "any.required": "L'email est requis", + }), + password: Joi.string().min(4).required().messages({ + "string.empty": "Le mot de passe est requis", + "string.min": "Le mot de passe doit comporter au moins 4 caractères", + "any.required": "Le mot de passe est requis", + }), +}); diff --git a/projects/mobile/src/api/schema/identity-and-access/password.ts b/projects/mobile/src/api/schema/identity-and-access/password.ts new file mode 100644 index 0000000..85faf03 --- /dev/null +++ b/projects/mobile/src/api/schema/identity-and-access/password.ts @@ -0,0 +1,53 @@ +import Joi from "joi"; + +export type RequestPasswordPayload = { + email: string; +}; + +export type ResetPasswordPayload = { + password: string; + confirm: string; +}; + +export type UpdatePasswordPayload = { + current: string; + password: string; + confirm: string; +}; + +export const RequestPasswordPayloadSchema = Joi.object({ + email: Joi.string().required().messages({ + "string.empty": "L'email est requis", + "any.required": "L'email est requis", + }), +}); + +export const ResetPasswordPayloadSchema = Joi.object({ + password: Joi.string().min(6).required().messages({ + "string.empty": "Le mot de passe est requis", + "string.min": "Le mot de passe doit comporter au moins 6 caractères", + "any.required": "Le mot de passe est requis", + }), + confirm: Joi.string().valid(Joi.ref("password")).required().messages({ + "any.only": "Les mots de passe ne correspondent pas", + "string.empty": "La confirmation du mot de passe est requise", + "any.required": "La confirmation du mot de passe est requise", + }), +}); + +export const UpdatePasswordPayloadSchema = Joi.object({ + current: Joi.string().required().messages({ + "string.empty": "Le mot de passe actuel est requis", + "any.required": "Le mot de passe actuel est requis", + }), + password: Joi.string().min(6).required().messages({ + "string.empty": "Le nouveau mot de passe est requis", + "string.min": "Le nouveau mot de passe doit comporter au moins 6 caractères", + "any.required": "Le nouveau mot de passe est requis", + }), + confirm: Joi.string().valid(Joi.ref("password")).required().messages({ + "any.only": "Les mots de passe ne correspondent pas", + "string.empty": "La confirmation du nouveau mot de passe est requise", + "any.required": "La confirmation du nouveau mot de passe est requise", + }), +}); diff --git a/projects/mobile/src/api/schema/identity-and-access/register.ts b/projects/mobile/src/api/schema/identity-and-access/register.ts new file mode 100644 index 0000000..a97fc90 --- /dev/null +++ b/projects/mobile/src/api/schema/identity-and-access/register.ts @@ -0,0 +1,23 @@ +import Joi from "joi"; + +export type RegisterPayload = { + name: string; + email: string; + password: string; +}; + +export const RegisterPayloadSchema = Joi.object({ + name: Joi.string().required().messages({ + "string.empty": "Le nom est requis", + "any.required": "Le nom est requis", + }), + email: Joi.string().required().messages({ + "string.empty": "L'email est requis", + "any.required": "L'email est requis", + }), + password: Joi.string().min(6).required().messages({ + "string.empty": "Le mot de passe est requis", + "string.min": "Le mot de passe doit comporter au moins 4 caractères", + "any.required": "Le mot de passe est requis", + }), +}); diff --git a/projects/mobile/src/api/shared.ts b/projects/mobile/src/api/shared.ts new file mode 100644 index 0000000..ece1da2 --- /dev/null +++ b/projects/mobile/src/api/shared.ts @@ -0,0 +1,150 @@ +import { skipToken, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import qs from "qs"; + +import client from "@/api/client"; + +export type PaginationFilters = { + page?: number; + limit?: number; + lastId?: string; +}; + +export type ArticleFilters = PaginationFilters & { + dateRange?: { + start: number; + end: number; + }; + search?: string; + sortDirection?: "asc" | "desc"; +}; + +export type PaginationInfo = { + current: number; + limit: number; + lastId?: string; + offset: number; +}; + +export type ClientDetailErrorResponse = { + type: string; + title: string; + detail: string; + status: number; +}; + +export type ClientErrorResponse = { + code: string; + message: string; +}; + +export type ErrorResponse = AxiosError; + +export type PaginatedResponse = { + items: TItem[]; + pagination: PaginationInfo; +}; + +export const safeMessage = (error: AxiosError | Error): string => { + if (error instanceof AxiosError && error.response) { + const response = error.response.data; + + if ("message" in response) { + return response.message; + } else if ("detail" in response) { + return response.detail; + } + } + + return "Une erreur est survenue"; +}; + +export const usePaginatedInfiniteQuery = (endpoint: string, filters: PaginationFilters = {}) => { + return useInfiniteQuery, ErrorResponse>({ + initialData: undefined, + initialPageParam: null, + queryKey: [endpoint, filters], + queryFn: async ({ pageParam = null }) => { + const query = qs.stringify({ ...filters, lastId: pageParam }, { skipNulls: true }); + const url = `${endpoint}?${query}`; + const response = await client.get>(url); + return response.data; + }, + getNextPageParam: (lastPage: PaginatedResponse) => { + const { lastId } = lastPage.pagination; + return lastId ? lastId : null; + }, + staleTime: 1_000 * 60 * 10, + }); +}; + +export const usePaginatedQuery = (endpoint: string, filters: PaginationFilters = {}) => { + return useQuery, ErrorResponse>({ + queryKey: [endpoint, filters], + queryFn: async (): Promise> => { + const query = qs.stringify({ ...filters, lastId: null }, { skipNulls: true }); + const url = `${endpoint}?${query}`; + const response = await client.get>(url); + return response.data; + }, + staleTime: 1_000 * 60 * 10, + }); +}; + +export const useGetQuery = (endpoint: string, enabled: boolean = true) => { + return useQuery({ + queryKey: [endpoint], + queryFn: enabled + ? async (): Promise => { + const response = await client.get(endpoint); + return response.data; + } + : skipToken, + staleTime: 1_000 * 60 * 10, + }); +}; + +export const usePostQuery = (endpoint: string, keys: string[] = []) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data: TPayload): Promise => { + const response = await client.post(endpoint, data); + return response.data; + }, + onSuccess: async () => { + for (const key of keys) { + await queryClient.invalidateQueries({ queryKey: [key] }); + } + }, + }); +}; + +export const usePutQuery = (endpoint: string, keys: string[] = []) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data: TPayload): Promise => { + const response = await client.put(endpoint, data); + return response.data; + }, + onSuccess: async () => { + for (const key of keys) { + await queryClient.invalidateQueries({ queryKey: [key] }); + } + }, + }); +}; + +export const useDeleteQuery = (endpoint: string, keys: string[] = []) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (): Promise => { + const response = await client.delete(endpoint); + return response.data; + }, + onSuccess: async () => { + for (const key of keys) { + await queryClient.invalidateQueries({ queryKey: [key] }); + } + }, + }); +}; diff --git a/projects/mobile/src/app/(authed)/(tabs)/_layout.tsx b/projects/mobile/src/app/(authed)/(tabs)/_layout.tsx new file mode 100644 index 0000000..0a7047a --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/_layout.tsx @@ -0,0 +1,82 @@ +import React from "react"; + +import { BookMarked, Globe, Home, User } from "@tamagui/lucide-icons"; +import { Tabs } from "expo-router"; +import { useColorScheme } from "react-native"; +import { Paragraph } from "tamagui"; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + ( + + Actualités + + ), + tabBarIcon: ({ color, size }) => , + }} + /> + ( + + Sources + + ), + tabBarIcon: ({ color, size }) => , + }} + /> + ( + + Signets + + ), + tabBarIcon: ({ color, size }) => , + }} + /> + ( + + Profil + + ), + tabBarIcon: ({ color, size }) => , + }} + /> + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/account/_layout.tsx b/projects/mobile/src/app/(authed)/(tabs)/account/_layout.tsx new file mode 100644 index 0000000..73ffdbf --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/account/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function Layout() { + return ; +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/account/index.tsx b/projects/mobile/src/app/(authed)/(tabs)/account/index.tsx new file mode 100644 index 0000000..2d4317a --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/account/index.tsx @@ -0,0 +1,30 @@ +import { ChevronRight, Settings } from "@tamagui/lucide-icons"; +import { useRouter } from "expo-router"; +import { Label, ListItem, ScrollView, Separator, YGroup } from "tamagui"; + +import { ScreenView } from "@/ui/components/layout"; + +export default function Index() { + const router = useRouter(); + + return ( + + + + + + + + router.push("/account/settings")} + icon={Settings} + iconAfter={ChevronRight} + title="Settings" + /> + + + + + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/account/settings/index.tsx b/projects/mobile/src/app/(authed)/(tabs)/account/settings/index.tsx new file mode 100644 index 0000000..9e17bdb --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/account/settings/index.tsx @@ -0,0 +1,35 @@ +import { ActivityIndicator } from "react-native"; +import { Button, YStack } from "tamagui"; + +import { useLogout } from "@/api/request/identity-and-access/login"; +import { useAuth } from "@/providers/auth-provider"; +import { ScreenView } from "@/ui/components/layout"; + +export default function Index() { + const authState = useAuth(); + const { mutate, isPending } = useLogout(); + + const handleLogout = async () => { + mutate(undefined, { + onSuccess: () => authState.logout(), + onError: () => authState.logout(), + }); + }; + + return ( + + + + + + + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/articles/[id].tsx b/projects/mobile/src/app/(authed)/(tabs)/articles/[id].tsx new file mode 100644 index 0000000..0c133bd --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/articles/[id].tsx @@ -0,0 +1,90 @@ +import { Bookmark, MoreVertical, Share } from "@tamagui/lucide-icons"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import * as WebBrowser from "expo-web-browser"; +import Toast from "react-native-toast-message"; +import { Button, H5, ScrollView, Separator, XStack, YStack } from "tamagui"; + +import { useArticleDetails } from "@/api/request/feed-management/article"; +import { Article } from "@/api/schema/feed-management/article"; +import { safeMessage } from "@/api/shared"; +import { useRelativeTime } from "@/hooks/use-relative-time"; +import { ArticleCategoryPill, ArticleCoverImage } from "@/ui/components/content/article"; +import { SourceReferencePill } from "@/ui/components/content/source"; +import { BackButton } from "@/ui/components/controls/BackButton"; +import { IconButton } from "@/ui/components/controls/IconButton"; +import { ScreenView } from "@/ui/components/layout"; +import { LoadingView } from "@/ui/components/LoadingView"; +import { Caption, Text } from "@/ui/components/typography"; + +export default function ArticleDetails() { + const router = useRouter(); + const { id } = useLocalSearchParams(); + const { data, isLoading, error } = useArticleDetails(id as string); + const article: Article | undefined = data ?? undefined; + const relativeTime = useRelativeTime(article?.publishedAt); + + const handleReadIntegrality = async () => { + await WebBrowser.openBrowserAsync(article!.link); + }; + + if (error) { + Toast.show({ + type: "error", + text1: "Erreur", + text2: safeMessage(error), + }); + router.replace("/(authed)/(tabs)/articles"); + } + + if (isLoading || article === undefined) { + return ; + } + + return ( + + router.dismissTo("/(authed)/(tabs)/articles")} />} + trailingActions={ + <> + {}} icon={} /> + {}} icon={} /> + {}} icon={} /> + + } + /> + + + {article.metadata?.image && ( + + )} + + + + {article.categories.map((category, index) => ( + + ))} + +
+ {article.title} +
+ + + + + {relativeTime} + + {article.readingTime} minutes de lecture + + + + + {article.body.trim()} + +
+ +
+
+ ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/articles/_layout.tsx b/projects/mobile/src/app/(authed)/(tabs)/articles/_layout.tsx new file mode 100644 index 0000000..73ffdbf --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/articles/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function Layout() { + return ; +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/articles/index.tsx b/projects/mobile/src/app/(authed)/(tabs)/articles/index.tsx new file mode 100644 index 0000000..57e0c49 --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/articles/index.tsx @@ -0,0 +1,51 @@ +import React from "react"; + +import { ScrollView, YStack } from "tamagui"; + +import { useArticleOverviewList } from "@/api/request/feed-management/article"; +import { useSourceOverviewList } from "@/api/request/feed-management/source"; +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { SourceOverview } from "@/api/schema/feed-management/source"; +import { useFlattenedItems } from "@/hooks/use-flattened-items"; +import { ArticleList, ArticleSkeletonList } from "@/ui/components/content/article"; +import { SourceList, SourceSkeletonList } from "@/ui/components/content/source"; +import { ScreenView } from "@/ui/components/layout"; +import { Heading } from "@/ui/components/typography"; + +export default function Index() { + const { data: articles, isLoading: articlesLoading } = useArticleOverviewList({ limit: 10 }); + const { data: sources, isLoading: sourcesLoading } = useSourceOverviewList(); + const articleOverviews: ArticleOverview[] = useFlattenedItems(articles); + const sourcesOverviews: SourceOverview[] = useFlattenedItems(sources); + + return ( + + Actualités + + + + + + {articlesLoading && } + {!articlesLoading && ( + + )} + + + + + {sourcesLoading && } + {!sourcesLoading && ( + + )} + + + + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/articles/trending.tsx b/projects/mobile/src/app/(authed)/(tabs)/articles/trending.tsx new file mode 100644 index 0000000..dd727f1 --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/articles/trending.tsx @@ -0,0 +1,40 @@ +import React from "react"; + +import { useRouter } from "expo-router"; + +import { useInfiniteArticleOverviewList } from "@/api/request/feed-management/article"; +import { TrendingArticle } from "@/api/schema/feed-management/article"; +import { useFlattenedItems } from "@/hooks/use-flattened-items"; +import { ArticleList, ArticleSkeletonList } from "@/ui/components/content/article"; +import { BackButton } from "@/ui/components/controls/BackButton"; +import { ScreenView } from "@/ui/components/layout"; + +export default function Trending() { + const router = useRouter(); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteArticleOverviewList( + { limit: 20 } + ); + const articles: TrendingArticle[] = useFlattenedItems(data); + + return ( + + router.dismissTo("/(authed)/(tabs)/articles")} />} + title="Actualités" + /> + + {isLoading && } + {!isLoading && ( + + )} + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/bookmarks/[id].tsx b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/[id].tsx new file mode 100644 index 0000000..42de48c --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/[id].tsx @@ -0,0 +1,18 @@ +import { useLocalSearchParams } from "expo-router"; +import { Paragraph } from "tamagui"; + +import { ScreenView } from "@/ui/components/layout"; +import { Heading } from "@/ui/components/typography"; + +export default function Details() { + const { id } = useLocalSearchParams(); + // const { data, isLoading } = useBookmarkedArticlesList(id as string); + // const articles: BookmarkedArticle[] = useFlattenedItems(data); + + return ( + + Bookmark Infos + {id} + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/bookmarks/_layout.tsx b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/_layout.tsx new file mode 100644 index 0000000..73ffdbf --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function Layout() { + return ; +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/bookmarks/index.tsx b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/index.tsx new file mode 100644 index 0000000..7a53c97 --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/bookmarks/index.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +import { Plus, Search } from "@tamagui/lucide-icons"; +import { YStack } from "tamagui"; + +import { useBookmarkList } from "@/api/request/feed-management/bookmark"; +import { Bookmark } from "@/api/schema/feed-management/bookmark"; +import { useFlattenedItems } from "@/hooks/use-flattened-items"; +import { BookmarkList } from "@/ui/components/content/bookmark"; +import { IconButton } from "@/ui/components/controls/IconButton"; +import { ScreenView } from "@/ui/components/layout"; +import { LoadingView } from "@/ui/components/LoadingView"; + +export default function Index() { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useBookmarkList(); + const bookmarks: Bookmark[] = useFlattenedItems(data); + + return ( + + {}} icon={} />} + trailingActions={ {}} icon={} />} + /> + + + {isLoading && } + {!isLoading && ( + + )} + + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/sources/[name].tsx b/projects/mobile/src/app/(authed)/(tabs)/sources/[name].tsx new file mode 100644 index 0000000..ab09f1e --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/sources/[name].tsx @@ -0,0 +1,15 @@ +import { useLocalSearchParams } from "expo-router"; + +import { ScreenView } from "@/ui/components/layout"; +import { Heading, Text } from "@/ui/components/typography"; + +export default function SourceDetails() { + const { name } = useLocalSearchParams(); + + return ( + + Source Details + {name} + + ); +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/sources/_layout.tsx b/projects/mobile/src/app/(authed)/(tabs)/sources/_layout.tsx new file mode 100644 index 0000000..73ffdbf --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/sources/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from "expo-router"; + +export default function Layout() { + return ; +} diff --git a/projects/mobile/src/app/(authed)/(tabs)/sources/index.tsx b/projects/mobile/src/app/(authed)/(tabs)/sources/index.tsx new file mode 100644 index 0000000..272a345 --- /dev/null +++ b/projects/mobile/src/app/(authed)/(tabs)/sources/index.tsx @@ -0,0 +1,19 @@ +import { useSourceOverviewList } from "@/api/request/feed-management/source"; +import { SourceOverview } from "@/api/schema/feed-management/source"; +import { useFlattenedItems } from "@/hooks/use-flattened-items"; +import { SourceList, SourceSkeletonList } from "@/ui/components/content/source"; +import { ScreenView } from "@/ui/components/layout"; + +export default function Sources() { + const { data, isLoading } = useSourceOverviewList(); + const sources: SourceOverview[] = useFlattenedItems(data); + + return ( + + + + {isLoading && } + {!isLoading && } + + ); +} diff --git a/projects/mobile/src/app/(authed)/_layout.tsx b/projects/mobile/src/app/(authed)/_layout.tsx new file mode 100644 index 0000000..5269578 --- /dev/null +++ b/projects/mobile/src/app/(authed)/_layout.tsx @@ -0,0 +1,17 @@ +import { Redirect, Stack } from "expo-router"; + +import { useAuth } from "@/providers/auth-provider"; + +export default function AuthedLayout() { + const auth = useAuth(); + + if (!auth.isReady) { + return null; + } + + if (!auth.isLoggedIn) { + return ; + } + + return ; +} diff --git a/projects/mobile/src/app/(unauthed)/_layout.tsx b/projects/mobile/src/app/(unauthed)/_layout.tsx new file mode 100644 index 0000000..a524f81 --- /dev/null +++ b/projects/mobile/src/app/(unauthed)/_layout.tsx @@ -0,0 +1,17 @@ +import { Redirect, Stack } from "expo-router"; + +import { useAuth } from "@/providers/auth-provider"; + +export default function AuthedLayout() { + const auth = useAuth(); + + if (!auth.isReady) { + return null; + } + + if (auth.isLoggedIn) { + return ; + } + + return ; +} diff --git a/projects/mobile/src/app/(unauthed)/password-request.tsx b/projects/mobile/src/app/(unauthed)/password-request.tsx new file mode 100644 index 0000000..a249d18 --- /dev/null +++ b/projects/mobile/src/app/(unauthed)/password-request.tsx @@ -0,0 +1,69 @@ +import React from "react"; + +import { joiResolver } from "@hookform/resolvers/joi"; +import { Link, useRouter } from "expo-router"; +import { useForm } from "react-hook-form"; +import Toast from "react-native-toast-message"; +import { YStack } from "tamagui"; + +import { usePasswordForgotten } from "@/api/request/identity-and-access/password"; +import { RequestPasswordPayload, RequestPasswordPayloadSchema } from "@/api/schema/identity-and-access/password"; +import { ErrorResponse, safeMessage } from "@/api/shared"; +import { FormEmailInput } from "@/ui/components/controls/forms"; +import { SubmitButton } from "@/ui/components/controls/SubmitButton"; +import { ScreenView } from "@/ui/components/layout"; +import { Heading, Text } from "@/ui/components/typography"; + +export default function PasswordRequest() { + const { mutate, isPending } = usePasswordForgotten(); + const router = useRouter(); + + const { control, handleSubmit, formState } = useForm({ + resolver: joiResolver(RequestPasswordPayloadSchema), + }); + + const onSubmit = (data: RequestPasswordPayload) => { + mutate(data, { + onSuccess: () => { + Toast.show({ + text1: "Succès", + text2: "Un mail avec les instructions vous a été envoyé", + type: "success", + }); + router.push("/(unauthed)/signin"); + }, + onError: (error: ErrorResponse) => { + Toast.show({ + text1: "Erreur de connexion", + text2: safeMessage(error), + type: "error", + }); + }, + }); + }; + + return ( + + + + Mot de passe oublié ? + + Veuillez entrer votre adresse e-mail pour recevoir un lien de réinitialisation de mot de passe. + + + + + + + Vous avez pas de compte ? Se connecter + + + + + ); +} diff --git a/projects/mobile/src/app/(unauthed)/signin.tsx b/projects/mobile/src/app/(unauthed)/signin.tsx new file mode 100644 index 0000000..d4b1c5b --- /dev/null +++ b/projects/mobile/src/app/(unauthed)/signin.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +import { joiResolver } from "@hookform/resolvers/joi"; +import { Link, useRouter } from "expo-router"; +import { useForm } from "react-hook-form"; +import Toast from "react-native-toast-message"; +import { YStack } from "tamagui"; + +import { useLogin } from "@/api/request/identity-and-access/login"; +import { LoginPayload, LoginPayloadSchema, LoginResponse } from "@/api/schema/identity-and-access/login"; +import { ErrorResponse, safeMessage } from "@/api/shared"; +import { useAuth } from "@/providers/auth-provider"; +import { FormEmailInput, FormPasswordInput } from "@/ui/components/controls/forms"; +import { SubmitButton } from "@/ui/components/controls/SubmitButton"; +import { ScreenView } from "@/ui/components/layout"; +import { Caption, Heading, Text } from "@/ui/components/typography"; + +export default function SignIn() { + const { mutate, isPending } = useLogin(); + const auth = useAuth(); + const router = useRouter(); + + if (auth.isLoggedIn) { + router.replace("/(authed)/(tabs)/articles"); + } + + const { control, handleSubmit, formState } = useForm({ + resolver: joiResolver(LoginPayloadSchema), + }); + + const onSubmit = (data: LoginPayload) => { + mutate(data, { + onSuccess: async (data: LoginResponse) => { + auth.login(data.token, data.refresh_token); + Toast.show({ text1: "Connexion réussie", type: "success" }); + }, + onError: (error: ErrorResponse) => { + Toast.show({ + text1: "Erreur de connexion", + text2: safeMessage(error), + type: "error", + }); + }, + }); + }; + + return ( + + + + Connexion + Bienvenue sur CongoNews, la plateforme d'actualités intelligente + + + + + + + + Mot de passe oublié ? + + + + + + En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu + notre politique de confidentialité. + + + Vous n'avez pas de compte ? Créer un compte + + + + + ); +} diff --git a/projects/mobile/src/app/(unauthed)/signup.tsx b/projects/mobile/src/app/(unauthed)/signup.tsx new file mode 100644 index 0000000..0ff2a29 --- /dev/null +++ b/projects/mobile/src/app/(unauthed)/signup.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +import { joiResolver } from "@hookform/resolvers/joi"; +import { User } from "@tamagui/lucide-icons"; +import { Link, useRouter } from "expo-router"; +import { useForm } from "react-hook-form"; +import Toast from "react-native-toast-message"; +import { YStack } from "tamagui"; + +import { useRegister } from "@/api/request/identity-and-access/register"; +import { RegisterPayload, RegisterPayloadSchema } from "@/api/schema/identity-and-access/register"; +import { ErrorResponse, safeMessage } from "@/api/shared"; +import { FormEmailInput, FormPasswordInput, FormTextInput } from "@/ui/components/controls/forms"; +import { SubmitButton } from "@/ui/components/controls/SubmitButton"; +import { ScreenView } from "@/ui/components/layout"; +import { Caption, Heading, Text } from "@/ui/components/typography"; + +export default function SingUp() { + const router = useRouter(); + const { mutate, isPending } = useRegister(); + + const { control, handleSubmit, formState } = useForm({ + resolver: joiResolver(RegisterPayloadSchema), + }); + + const onSubmit = (data: RegisterPayload) => { + mutate(data, { + onSuccess: () => { + Toast.show({ + text1: "Félicitations !", + text2: "les détails de votre compte vous ont été envoyés par e-mail.", + type: "success", + }); + router.replace("/(unauthed)/signin"); + }, + onError: (error: ErrorResponse) => { + Toast.show({ + text1: "Erreur", + text2: safeMessage(error), + type: "error", + }); + }, + }); + }; + + return ( + + + + Inscription + Rejoignez la communauté CongoNews et restez informé des dernières actualités + + + + + + + + + En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu + notre politique de confidentialité. + + + Vous avez un compte ? Connectez-vous + + + + + ); +} diff --git a/projects/mobile/src/app/(unauthed)/welcome.tsx b/projects/mobile/src/app/(unauthed)/welcome.tsx new file mode 100644 index 0000000..ebd7e2f --- /dev/null +++ b/projects/mobile/src/app/(unauthed)/welcome.tsx @@ -0,0 +1,39 @@ +import { Link, useRouter } from "expo-router"; +import { Button, YStack } from "tamagui"; + +import { AppIcon } from "@/ui/components/AppIcon"; +import { ScreenView } from "@/ui/components/layout"; +import { Caption, Display, Text } from "@/ui/components/typography"; + +export default function Welcome() { + const router = useRouter(); + + return ( + + + + + Bienvenue sur CongoNews + + La première plateforme d'actualités intelligente qui vous aide à rester informé sur + congolaise et internationale. + + + + + + + Ouvrir un compte + + + + + En continuant, vous acceptez les conditions d'utilisation de CongoNews et reconnaissez avoir lu + notre politique de confidentialité. + + + + ); +} diff --git a/projects/mobile/src/app/+not-found.tsx b/projects/mobile/src/app/+not-found.tsx new file mode 100644 index 0000000..2be952e --- /dev/null +++ b/projects/mobile/src/app/+not-found.tsx @@ -0,0 +1,33 @@ +import { Link, Stack } from "expo-router"; +import { View, YStack } from "tamagui"; + +import { AppIcon } from "@/ui/components/AppIcon"; +import { ScreenView } from "@/ui/components/layout"; +import { Heading, Text } from "@/ui/components/typography"; + +export default function NotFoundScreen() { + return ( + + + + + + + + + Une erreur s'est produite + + + Nous avons une difficulté à charger la page que vous recherchez. + + + + + Recommencer + + + + + + ); +} diff --git a/projects/mobile/src/app/_layout.tsx b/projects/mobile/src/app/_layout.tsx new file mode 100644 index 0000000..0a4f33a --- /dev/null +++ b/projects/mobile/src/app/_layout.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import * as Sentry from "@sentry/react-native"; +import { Stack } from "expo-router"; +import { useColorScheme } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Toast from "react-native-toast-message"; +import { Theme } from "tamagui"; + +import { RootProviders } from "@/providers/root-providers"; + +export { ErrorBoundary } from "expo-router"; + +Sentry.init({ + dsn: process.env.EXPO_PUBLIC_SENTRY_DSN, + sendDefaultPii: true, + debug: __DEV__, + tracesSampleRate: 1.0, + tracePropagationTargets: [/.*?/], + spotlight: __DEV__, +}); + +function RootLayout() { + const colorScheme = useColorScheme(); + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + + ); +} + +export default Sentry.wrap(RootLayout); diff --git a/projects/mobile/src/app/index.tsx b/projects/mobile/src/app/index.tsx new file mode 100644 index 0000000..108aa32 --- /dev/null +++ b/projects/mobile/src/app/index.tsx @@ -0,0 +1,13 @@ +import { Redirect } from "expo-router"; + +import { useAuth } from "@/providers/auth-provider"; + +export default function Index() { + const auth = useAuth(); + + if (!auth.isReady) { + return null; + } + + return auth.isLoggedIn ? : ; +} diff --git a/projects/mobile/src/assets/fonts/SpaceMono-Regular.ttf b/projects/mobile/src/assets/fonts/SpaceMono-Regular.ttf new file mode 100755 index 0000000..28d7ff7 Binary files /dev/null and b/projects/mobile/src/assets/fonts/SpaceMono-Regular.ttf differ diff --git a/projects/mobile/src/assets/illustrations/BookmarkIllustration.tsx b/projects/mobile/src/assets/illustrations/BookmarkIllustration.tsx new file mode 100644 index 0000000..d0c9dc5 --- /dev/null +++ b/projects/mobile/src/assets/illustrations/BookmarkIllustration.tsx @@ -0,0 +1,309 @@ +import * as React from "react"; + +import Svg, { Circle, G, Path, Rect, SvgProps } from "react-native-svg"; + +/** + * @see https://storyset.com/illustration/bookmarks/pana + * @param props + * @constructor + */ +export default function BookmarkIllustration(props: SvgProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/projects/mobile/src/assets/images/adaptive-icon.png b/projects/mobile/src/assets/images/adaptive-icon.png new file mode 100644 index 0000000..8e37f86 Binary files /dev/null and b/projects/mobile/src/assets/images/adaptive-icon.png differ diff --git a/projects/mobile/src/assets/images/favicon.png b/projects/mobile/src/assets/images/favicon.png new file mode 100644 index 0000000..8e37f86 Binary files /dev/null and b/projects/mobile/src/assets/images/favicon.png differ diff --git a/projects/mobile/src/assets/images/icon.png b/projects/mobile/src/assets/images/icon.png new file mode 100644 index 0000000..8e37f86 Binary files /dev/null and b/projects/mobile/src/assets/images/icon.png differ diff --git a/projects/mobile/src/assets/images/logo.png b/projects/mobile/src/assets/images/logo.png new file mode 100644 index 0000000..d333443 Binary files /dev/null and b/projects/mobile/src/assets/images/logo.png differ diff --git a/projects/mobile/src/assets/images/splash-icon.png b/projects/mobile/src/assets/images/splash-icon.png new file mode 100644 index 0000000..8e37f86 Binary files /dev/null and b/projects/mobile/src/assets/images/splash-icon.png differ diff --git a/projects/mobile/src/assets/screenshots/1.png b/projects/mobile/src/assets/screenshots/1.png new file mode 100644 index 0000000..81deb10 Binary files /dev/null and b/projects/mobile/src/assets/screenshots/1.png differ diff --git a/projects/mobile/src/assets/screenshots/2.png b/projects/mobile/src/assets/screenshots/2.png new file mode 100644 index 0000000..99e89b5 Binary files /dev/null and b/projects/mobile/src/assets/screenshots/2.png differ diff --git a/projects/mobile/src/assets/screenshots/3.png b/projects/mobile/src/assets/screenshots/3.png new file mode 100644 index 0000000..af7f4c0 Binary files /dev/null and b/projects/mobile/src/assets/screenshots/3.png differ diff --git a/projects/mobile/src/hooks/use-flattened-items.ts b/projects/mobile/src/hooks/use-flattened-items.ts new file mode 100644 index 0000000..fd59b7b --- /dev/null +++ b/projects/mobile/src/hooks/use-flattened-items.ts @@ -0,0 +1,26 @@ +import { useMemo } from "react"; + +interface Page { + items: T[]; +} + +interface PaginatedResult { + pages?: Page[]; + items?: T[]; +} + +export const useFlattenedItems = (data: PaginatedResult | undefined | null): T[] => { + return useMemo((): T[] => { + if (!data) { + return []; + } + + if (data.pages && Array.isArray(data.pages) && data.pages.length > 0) { + return data.pages.flatMap(page => page.items || []); + } else if (data.items && Array.isArray(data.items)) { + return data.items; + } else { + return []; + } + }, [data]); +}; diff --git a/projects/mobile/src/hooks/use-relative-time.ts b/projects/mobile/src/hooks/use-relative-time.ts new file mode 100644 index 0000000..6d09007 --- /dev/null +++ b/projects/mobile/src/hooks/use-relative-time.ts @@ -0,0 +1,61 @@ +import { useEffect, useState } from "react"; + +import { formatDistanceToNowStrict, Locale } from "date-fns"; +import { fr } from "date-fns/locale"; + +export const useRelativeTime = ( + dateInput: string | Date | number | null | undefined, + options?: { + addSuffix?: boolean; + unit?: "second" | "minute" | "hour" | "day" | "month" | "year"; + locale?: Locale; + roundingMethod?: "floor" | "ceil" | "round"; + includeSeconds?: boolean; + }, + updateInterval: number = 60000 +): string => { + const [relativeTime, setRelativeTime] = useState(""); + + useEffect(() => { + if (dateInput === null || dateInput === undefined) { + setRelativeTime(""); + return; + } + + const date = new Date(dateInput); + + // Check if the date is valid + if (isNaN(date.getTime())) { + setRelativeTime("Invalid Date"); + return; + } + + const updateTime = () => { + // Default options if none provided, ensures suffix is added + const effectiveOptions = { + locale: fr, + addSuffix: true, + ...options, + }; + + try { + const formattedTime = formatDistanceToNowStrict(date, effectiveOptions); + setRelativeTime(formattedTime); + } catch (error) { + console.error("Error formatting relative time:", error); + setRelativeTime(dateInput.toString()); // Handle potential errors during formatting + } + }; + + // Initial update + updateTime(); + + // Set up interval for periodic updates + const intervalId = setInterval(updateTime, updateInterval); + + // Clean up the interval when the component unmounts or dateInput changes + return () => clearInterval(intervalId); + }, [dateInput, options, updateInterval]); + + return relativeTime; +}; diff --git a/projects/mobile/src/providers/auth-provider.tsx b/projects/mobile/src/providers/auth-provider.tsx new file mode 100644 index 0000000..d365bb5 --- /dev/null +++ b/projects/mobile/src/providers/auth-provider.tsx @@ -0,0 +1,86 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; + +import { useRouter, SplashScreen } from "expo-router"; + +import { clearTokens, setTokens, getAccessToken, getRefreshToken } from "@/store/auth"; + +SplashScreen.preventAutoHideAsync(); + +type AuthState = { + isReady: boolean; + isLoggedIn: boolean; + login: (accessToken: string, refreshToken: string) => void; + logout: () => void; + accessToken: string | null; + refreshToken: string | null; +}; + +const AuthContext = createContext({ + isReady: false, + isLoggedIn: false, + login: () => {}, + logout: () => {}, + accessToken: null, + refreshToken: null, +}); + +export function useAuth() { + return useContext(AuthContext); +} + +export function AuthProvider({ children }: React.PropsWithChildren) { + const [isReady, setIsReady] = useState(false); + const [accessToken, setAccessToken] = useState(null); + const [refreshToken, setRefreshToken] = useState(null); + const router = useRouter(); + + const isLoggedIn = !!(accessToken && refreshToken); + + const login = (access: string, refresh: string) => { + setAccessToken(access); + setRefreshToken(refresh); + setTokens(access, refresh); + router.replace("/(authed)/(tabs)/articles"); + }; + + const logout = () => { + setAccessToken(null); + setRefreshToken(null); + clearTokens(); + router.replace("/signin"); + }; + + useEffect(() => { + const loadTokens = async () => { + try { + const [storedAccess, storedRefresh] = await Promise.all([getAccessToken(), getRefreshToken()]); + + if (storedAccess && storedRefresh) { + setAccessToken(storedAccess); + setRefreshToken(storedRefresh); + } + } catch (error) { + console.error("Unable to retrieve auth tokens", error); + } finally { + setIsReady(true); + await SplashScreen.hideAsync(); + } + }; + loadTokens(); + }, []); + + return ( + + {children} + + ); +} diff --git a/projects/mobile/src/providers/fonts-loader-provider.tsx b/projects/mobile/src/providers/fonts-loader-provider.tsx new file mode 100644 index 0000000..3060351 --- /dev/null +++ b/projects/mobile/src/providers/fonts-loader-provider.tsx @@ -0,0 +1,44 @@ +import type React from "react"; +import { useEffect } from "react"; + +import { + Inter_100Thin, + Inter_200ExtraLight, + Inter_300Light, + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, + Inter_800ExtraBold, + Inter_900Black, + useFonts, +} from "@expo-google-fonts/inter"; +import { SplashScreen } from "expo-router"; + +SplashScreen.preventAutoHideAsync(); + +export const FontsLoaderProvider = ({ children }: React.PropsWithChildren) => { + const [fontsLoaded, fontError] = useFonts({ + Inter_100Thin, + Inter_200ExtraLight, + Inter_300Light, + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, + Inter_800ExtraBold, + Inter_900Black, + }); + + useEffect(() => { + if (fontsLoaded || fontError) { + SplashScreen.hideAsync(); + } + }, [fontsLoaded, fontError]); + + if (!fontsLoaded && !fontError) { + return null; + } + + return <>{children}; +}; diff --git a/projects/mobile/src/providers/network-provider.tsx b/projects/mobile/src/providers/network-provider.tsx new file mode 100644 index 0000000..73f8b0d --- /dev/null +++ b/projects/mobile/src/providers/network-provider.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; + +import * as Network from "expo-network"; +import { NetworkStateEvent } from "expo-network"; + +type NetworkState = { + isConnected: boolean; +}; + +const NetworkContext = createContext({ + isConnected: true, +}); + +export const useNetwork = () => useContext(NetworkContext); + +export const NetworkProvider = ({ children }: React.PropsWithChildren) => { + const [isConnected, setIsConnected] = useState(true); + + useEffect(() => { + let subscription: { remove: () => any }; + + const subscribeToNetworkChanges = async () => { + const state = await Network.getNetworkStateAsync(); + updateConnection(state); + + subscription = Network.addNetworkStateListener(updateConnection); + }; + + const updateConnection = (state: NetworkStateEvent) => { + const connected = state.isConnected && state.isInternetReachable === true; + setIsConnected(connected === undefined ? false : connected); + }; + + subscribeToNetworkChanges(); + + return () => { + subscription && subscription.remove(); + }; + }, []); + + return {children}; +}; diff --git a/projects/mobile/src/providers/root-providers.tsx b/projects/mobile/src/providers/root-providers.tsx new file mode 100644 index 0000000..b9f312a --- /dev/null +++ b/projects/mobile/src/providers/root-providers.tsx @@ -0,0 +1,26 @@ +import type React from "react"; + +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { SafeAreaProvider } from "react-native-safe-area-context"; + +import { AuthProvider } from "@/providers/auth-provider"; +import { FontsLoaderProvider } from "@/providers/fonts-loader-provider"; +import { NetworkProvider } from "@/providers/network-provider"; +import { TamaguiConfigProvider } from "@/providers/tamagui-config-provider"; +import { TanstackQueryProvider } from "@/providers/tanstack-query-provider"; + +export const RootProviders = ({ children }: React.PropsWithChildren) => ( + + + + + + + {children} + + + + + + +); diff --git a/projects/mobile/src/providers/tamagui-config-provider.tsx b/projects/mobile/src/providers/tamagui-config-provider.tsx new file mode 100644 index 0000000..a0b7709 --- /dev/null +++ b/projects/mobile/src/providers/tamagui-config-provider.tsx @@ -0,0 +1,9 @@ +import type React from "react"; + +import { TamaguiProvider } from "tamagui"; + +import { config } from "~/tamagui.config"; + +export const TamaguiConfigProvider = ({ children }: React.PropsWithChildren) => ( + {children} +); diff --git a/projects/mobile/src/providers/tanstack-query-provider.tsx b/projects/mobile/src/providers/tanstack-query-provider.tsx new file mode 100644 index 0000000..83eae1b --- /dev/null +++ b/projects/mobile/src/providers/tanstack-query-provider.tsx @@ -0,0 +1,9 @@ +import type React from "react"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +export const queryClient = new QueryClient(); + +export const TanstackQueryProvider = ({ children }: React.PropsWithChildren) => ( + {children} +); diff --git a/projects/mobile/src/store/auth.ts b/projects/mobile/src/store/auth.ts new file mode 100644 index 0000000..ad71463 --- /dev/null +++ b/projects/mobile/src/store/auth.ts @@ -0,0 +1,27 @@ +import * as SecureStore from "expo-secure-store"; + +export const getAccessToken = () => SecureStore.getItemAsync("user_access_token"); +export const getRefreshToken = () => SecureStore.getItemAsync("user_refresh_token"); + +export const setTokens = async (access: string, refresh: string) => { + try { + await Promise.all([ + SecureStore.setItemAsync("user_access_token", access), + SecureStore.setItemAsync("user_refresh_token", refresh), + ]); + } catch (error) { + console.log(access, refresh); + console.error("Unable to save auth tokens", error); + } +}; + +export const clearTokens = async () => { + try { + await Promise.all([ + SecureStore.deleteItemAsync("user_access_token"), + SecureStore.deleteItemAsync("user_refresh_token"), + ]); + } catch (error) { + console.error("Unable to clear auth tokens", error); + } +}; diff --git a/projects/mobile/src/ui/components/AppIcon.tsx b/projects/mobile/src/ui/components/AppIcon.tsx new file mode 100644 index 0000000..574871d --- /dev/null +++ b/projects/mobile/src/ui/components/AppIcon.tsx @@ -0,0 +1,20 @@ +import { Image } from "tamagui"; + +type AppLogoProps = { + width?: number; + height?: number; +}; + +export const AppIcon = (props: AppLogoProps) => { + const { width = 80, height = 80 } = props; + + return ( + + ); +}; diff --git a/projects/mobile/src/ui/components/LoadingView.tsx b/projects/mobile/src/ui/components/LoadingView.tsx new file mode 100644 index 0000000..4faa99d --- /dev/null +++ b/projects/mobile/src/ui/components/LoadingView.tsx @@ -0,0 +1,11 @@ +import { ActivityIndicator } from "react-native"; +import { View } from "tamagui"; + +import { Caption } from "@/ui/components/typography"; + +export const LoadingView = () => ( + + + Chargement... + +); diff --git a/projects/mobile/src/ui/components/content/article/ArticleCategoryPill.tsx b/projects/mobile/src/ui/components/content/article/ArticleCategoryPill.tsx new file mode 100644 index 0000000..cecbbc4 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleCategoryPill.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +import { Caption } from "@/ui/components/typography"; + +type ArticleCategoryPillProps = { + category: string; +}; + +export const ArticleCategoryPill = (props: ArticleCategoryPillProps) => { + const { category } = props; + + return {category}; +}; diff --git a/projects/mobile/src/ui/components/content/article/ArticleCoverImage.tsx b/projects/mobile/src/ui/components/content/article/ArticleCoverImage.tsx new file mode 100644 index 0000000..42f465e --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleCoverImage.tsx @@ -0,0 +1,19 @@ +import { GetProps, Image, styled } from "tamagui"; + +const StyledImage = styled(Image, { + borderRadius: "$4", + backgroundColor: "$gray3", + objectFit: "cover", +}); + +type ArticleCoverImageProps = GetProps & { + uri: string; + width: string | number; + height: number; +}; + +export const ArticleCoverImage = (props: ArticleCoverImageProps) => { + const { width, height, uri, ...rest } = props; + + return ; +}; diff --git a/projects/mobile/src/ui/components/content/article/ArticleList.tsx b/projects/mobile/src/ui/components/content/article/ArticleList.tsx new file mode 100644 index 0000000..1654613 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleList.tsx @@ -0,0 +1,115 @@ +import React, { useCallback } from "react"; + +import { ActivityIndicator, Dimensions, FlatList, FlatListProps } from "react-native"; +import { View, XStack, YStack } from "tamagui"; + +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { ArticleMagazineCard } from "@/ui/components/content/article/ArticleMagazineCard"; +import { ArticleOverviewCard } from "@/ui/components/content/article/ArticleOverviewCard"; +import { ArticleTextOnlyCard } from "@/ui/components/content/article/ArticleTextOnlyCard"; +import { Text } from "@/ui/components/typography"; + +const { width: screenWidth } = Dimensions.get("window"); + +const HorizontalSeparator = () => ; +const VerticalSeparator = () => ; + +const LoadingIndicator = () => ( + <> + + + + +); + +export type ArticleListDisplayMode = "card" | "magazine" | "text-only"; + +type ArticleListProps = Omit, "renderItem"> & { + data: ArticleOverview[]; + horizontal?: boolean; + infiniteScroll?: boolean; + displayMode?: ArticleListDisplayMode; + hasNextPage?: boolean; + isFetchingNextPage?: boolean; + fetchNextPage?: () => void; +}; + +type ArticleListComponent = React.FC & { + HorizontalSeparator: typeof HorizontalSeparator; + VerticalSeparator: typeof VerticalSeparator; + LoadingIndicator: typeof LoadingIndicator; +}; + +const keyExtractor = (item: ArticleOverview) => item.id; + +const selectDisplayComponent = (mode: ArticleListDisplayMode) => { + switch (mode) { + case "card": + return ArticleOverviewCard; + case "magazine": + return ArticleMagazineCard; + case "text-only": + return ArticleTextOnlyCard; + default: + throw new Error(`Unknown display mode: ${mode}`); + } +}; + +const ArticleList: ArticleListComponent = (props: ArticleListProps) => { + const { + data, + displayMode = "magazine", + horizontal = false, + infiniteScroll = false, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + refreshing, + ...rest + } = props; + + const renderItem = useCallback( + ({ item }: { item: ArticleOverview }) => { + const itemWidth = horizontal ? screenWidth * 0.7 : undefined; + const DisplayComponent = selectDisplayComponent(displayMode); + + return ( + + + + ); + }, + [horizontal, displayMode] + ); + + const handleOnEndReached = useCallback(async () => { + if (infiniteScroll && hasNextPage && !isFetchingNextPage && fetchNextPage) { + fetchNextPage(); + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage, infiniteScroll]); + + return ( + Pas d’articles disponibles pour le moment.} + /> + ); +}; + +ArticleList.HorizontalSeparator = HorizontalSeparator; +ArticleList.VerticalSeparator = VerticalSeparator; +ArticleList.LoadingIndicator = LoadingIndicator; + +export { ArticleList }; diff --git a/projects/mobile/src/ui/components/content/article/ArticleMagazineCard.tsx b/projects/mobile/src/ui/components/content/article/ArticleMagazineCard.tsx new file mode 100644 index 0000000..4bc65b5 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleMagazineCard.tsx @@ -0,0 +1,45 @@ +import React from "react"; + +import { Link } from "expo-router"; +import { Card, XStack, YStack } from "tamagui"; + +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { useRelativeTime } from "@/hooks/use-relative-time"; +import { ArticleCoverImage } from "@/ui/components/content/article/ArticleCoverImage"; +import { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill"; +import { Caption, Text } from "@/ui/components/typography"; + +type ArticleMagazineCardProps = { + data: ArticleOverview; +}; + +export const ArticleMagazineCard = (props: ArticleMagazineCardProps) => { + const { data } = props; + const relativeTime = useRelativeTime(data.publishedAt); + + return ( + + + + + + {data.title} + + + {data.excerpt} + + + + {data.image && } + + + + + + + {relativeTime} + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/article/ArticleOverviewCard.tsx b/projects/mobile/src/ui/components/content/article/ArticleOverviewCard.tsx new file mode 100644 index 0000000..6047f38 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleOverviewCard.tsx @@ -0,0 +1,44 @@ +import React from "react"; + +import { Link } from "expo-router"; +import { Card, XStack, YStack } from "tamagui"; + +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { useRelativeTime } from "@/hooks/use-relative-time"; +import { ArticleCoverImage } from "@/ui/components/content/article/ArticleCoverImage"; +import { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill"; +import { Caption, Text } from "@/ui/components/typography"; + +type ArticleOverviewCardProps = { + data: ArticleOverview; +}; + +export const ArticleOverviewCard = (props: ArticleOverviewCardProps) => { + const { data } = props; + const relativeTime = useRelativeTime(data.publishedAt); + + return ( + + + <> + {data.image && } + + + {data.title} + + + {data.excerpt} + + + + + + + + + {relativeTime} + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/article/ArticleSkeleton.tsx b/projects/mobile/src/ui/components/content/article/ArticleSkeleton.tsx new file mode 100644 index 0000000..f338d26 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleSkeleton.tsx @@ -0,0 +1,129 @@ +import React, { useCallback } from "react"; + +import ContentLoader, { Circle, Rect } from "react-content-loader/native"; +import { Dimensions, FlatList } from "react-native"; +import { View } from "tamagui"; + +import { ArticleList, ArticleListDisplayMode } from "@/ui/components/content/article/ArticleList"; + +const { width: screenWidth } = Dimensions.get("window"); +const data: number[] = new Array(5).fill(0); + +type ArticleSkeletonListProps = { + horizontal?: boolean; + displayMode?: ArticleListDisplayMode; +}; + +const OverviewCardSkeleton = (props: any) => ( + + + + + + + + + + + + +); + +const MagazineCardSkeleton = (props: any) => ( + + + + + + + + + + + + + +); + +const TextOnlyCardSkeleton = (props: any) => ( + + + + + + + + + + + +); + +const keyExtractor = (_: number, index: number) => index.toString(); + +const selectSkeletonComponent = (displayMode: ArticleListDisplayMode) => { + switch (displayMode) { + case "magazine": + return MagazineCardSkeleton; + case "text-only": + return TextOnlyCardSkeleton; + default: + return OverviewCardSkeleton; + } +}; + +export const ArticleSkeletonList = (props: ArticleSkeletonListProps) => { + const { horizontal = false, displayMode = "magazine" } = props; + + const ItemSeparator = horizontal ? ArticleList.HorizontalSeparator : ArticleList.VerticalSeparator; + + const renderItem = useCallback(() => { + const itemWidth = horizontal ? screenWidth * 0.7 : screenWidth; + const SkeletonComponent = selectSkeletonComponent(displayMode); + + return ( + + + + ); + }, [horizontal, displayMode]); + + return ( + + ); +}; diff --git a/projects/mobile/src/ui/components/content/article/ArticleTextOnlyCard.tsx b/projects/mobile/src/ui/components/content/article/ArticleTextOnlyCard.tsx new file mode 100644 index 0000000..6e6b4b8 --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/ArticleTextOnlyCard.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +import { Link } from "expo-router"; +import { Card, XStack, YStack } from "tamagui"; + +import { ArticleOverview } from "@/api/schema/feed-management/article"; +import { useRelativeTime } from "@/hooks/use-relative-time"; +import { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill"; +import { Caption, Text } from "@/ui/components/typography"; + +type ArticleTextOnlyCardProps = { + data: ArticleOverview; +}; + +export const ArticleTextOnlyCard = (props: ArticleTextOnlyCardProps) => { + const { data } = props; + const relativeTime = useRelativeTime(data.publishedAt); + + return ( + + + + + + {data.title} + + + {data.excerpt} + + + + + + + + + {relativeTime} + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/article/index.tsx b/projects/mobile/src/ui/components/content/article/index.tsx new file mode 100644 index 0000000..49a666a --- /dev/null +++ b/projects/mobile/src/ui/components/content/article/index.tsx @@ -0,0 +1,7 @@ +export { ArticleCategoryPill } from "@/ui/components/content/article/ArticleCategoryPill"; +export { ArticleCoverImage } from "@/ui/components/content/article/ArticleCoverImage"; +export { ArticleList } from "@/ui/components/content/article/ArticleList"; +export { ArticleSkeletonList } from "@/ui/components/content/article/ArticleSkeleton"; +export { ArticleMagazineCard } from "@/ui/components/content/article/ArticleMagazineCard"; +export { ArticleOverviewCard } from "@/ui/components/content/article/ArticleOverviewCard"; +export { ArticleTextOnlyCard } from "@/ui/components/content/article/ArticleTextOnlyCard"; diff --git a/projects/mobile/src/ui/components/content/bookmark/BookmarkCard.tsx b/projects/mobile/src/ui/components/content/bookmark/BookmarkCard.tsx new file mode 100644 index 0000000..e6c285e --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/BookmarkCard.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import { Link } from "expo-router"; +import { Card, XStack, YStack } from "tamagui"; + +import { Bookmark } from "@/api/schema/feed-management/bookmark"; +import { useRelativeTime } from "@/hooks/use-relative-time"; +import { Caption, Text } from "@/ui/components/typography"; + +type BookmarkCardProps = { + data: Bookmark; +}; + +export const BookmarkCard = (props: BookmarkCardProps) => { + const { data } = props; + const relativeTime = useRelativeTime(data.createdAt); + + return ( + + + + + + + + {data.name} + + {data.description && ( + + {data.description} + + )} + + + + + + + {data.isPublic} + {data.articlesCount} articles + {relativeTime} + + + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/bookmark/BookmarkEmptyState.tsx b/projects/mobile/src/ui/components/content/bookmark/BookmarkEmptyState.tsx new file mode 100644 index 0000000..d6b761f --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/BookmarkEmptyState.tsx @@ -0,0 +1,17 @@ +import { YStack } from "tamagui"; + +import BookmarkIllustration from "@/assets/illustrations/BookmarkIllustration"; +import { CreateBookmarkSheet } from "@/ui/components/content/bookmark/CreateBookmarkSheet"; +import { Heading, Text } from "@/ui/components/typography"; + +export const BookmarkEmptyState = () => { + return ( + + + Empty Bookmarks + Create a bookmark to save your favorite articles and access them later. + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/bookmark/BookmarkList.tsx b/projects/mobile/src/ui/components/content/bookmark/BookmarkList.tsx new file mode 100644 index 0000000..b556e1a --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/BookmarkList.tsx @@ -0,0 +1,70 @@ +import React, { useCallback } from "react"; + +import { ActivityIndicator, FlatList, FlatListProps } from "react-native"; +import { YStack } from "tamagui"; + +import { Bookmark } from "@/api/schema/feed-management/bookmark"; +import { BookmarkCard } from "@/ui/components/content/bookmark/BookmarkCard"; +import { BookmarkEmptyState } from "@/ui/components/content/bookmark/BookmarkEmptyState"; + +const VerticalSeparator = () => ; + +const LoadingIndicator = () => ( + <> + + + + +); + +type BookmarkListProps = Omit, "renderItem"> & { + data: Bookmark[]; + infiniteScroll?: boolean; + hasNextPage?: boolean; + isFetchingNextPage?: boolean; + fetchNextPage?: () => void; +}; + +type BookmarkListComponent = React.FC & { + VerticalSeparator: typeof VerticalSeparator; + LoadingIndicator: typeof LoadingIndicator; +}; + +const keyExtractor = (item: Bookmark) => item.id; + +const renderItem = ({ item }: { item: Bookmark }) => { + return ; +}; + +const BookmarkList: BookmarkListComponent = (props: BookmarkListProps) => { + const { data, infiniteScroll = false, hasNextPage, isFetchingNextPage, fetchNextPage, refreshing, ...rest } = props; + + const handleOnEndReached = useCallback(async () => { + if (infiniteScroll && hasNextPage && !isFetchingNextPage && fetchNextPage) { + fetchNextPage(); + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage, infiniteScroll]); + + return ( + } + ListFooterComponent={infiniteScroll && refreshing ? LoadingIndicator : undefined} + /> + ); +}; + +BookmarkList.VerticalSeparator = VerticalSeparator; +BookmarkList.LoadingIndicator = LoadingIndicator; + +export { BookmarkList }; diff --git a/projects/mobile/src/ui/components/content/bookmark/CreateBookmarkSheet.tsx b/projects/mobile/src/ui/components/content/bookmark/CreateBookmarkSheet.tsx new file mode 100644 index 0000000..22c9202 --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/CreateBookmarkSheet.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; + +import { joiResolver } from "@hookform/resolvers/joi"; +import { Sheet } from "@tamagui/sheet"; +import { useForm } from "react-hook-form"; +import Toast from "react-native-toast-message"; +import { Button, YStack } from "tamagui"; + +import { useCreateBookmark } from "@/api/request/feed-management/bookmark"; +import { BookmarkPayload, BookmarkPayloadSchema } from "@/api/schema/feed-management/bookmark"; +import { ErrorResponse, safeMessage } from "@/api/shared"; +import { FormSwitch, FormTextArea, FormTextInput } from "@/ui/components/controls/forms"; +import { SubmitButton } from "@/ui/components/controls/SubmitButton"; + +export const CreateBookmarkSheet = () => { + const { mutate, isPending } = useCreateBookmark(); + const [open, setOpen] = useState(false); + + const { control, handleSubmit, formState } = useForm({ + resolver: joiResolver(BookmarkPayloadSchema), + }); + + const onSubmit = (data: BookmarkPayload) => { + mutate(data, { + onSuccess: () => { + Toast.show({ + text1: "Félicitations !", + text2: "Votre signet a été créé avec succès.", + type: "success", + }); + setOpen(false); + }, + onError: (error: ErrorResponse) => { + Toast.show({ + text1: "Erreur", + text2: safeMessage(error), + type: "error", + }); + }, + }); + }; + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/bookmark/UpdateBookmarkSheet.tsx b/projects/mobile/src/ui/components/content/bookmark/UpdateBookmarkSheet.tsx new file mode 100644 index 0000000..96c52e6 --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/UpdateBookmarkSheet.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react"; + +import { joiResolver } from "@hookform/resolvers/joi"; +import { Sheet } from "@tamagui/sheet"; +import { useForm } from "react-hook-form"; +import Toast from "react-native-toast-message"; +import { Button, YStack } from "tamagui"; + +import { useUpdateBookmark } from "@/api/request/feed-management/bookmark"; +import { Bookmark, BookmarkPayload, BookmarkPayloadSchema } from "@/api/schema/feed-management/bookmark"; +import { ErrorResponse, safeMessage } from "@/api/shared"; +import { FormSwitch } from "@/ui/components/controls/forms/Switch"; +import { FormTextArea } from "@/ui/components/controls/forms/TextArea"; +import { FormTextInput } from "@/ui/components/controls/forms/TextInput"; +import { SubmitButton } from "@/ui/components/controls/SubmitButton"; + +type UpdateBookmarkSheetProps = { + data: Bookmark; +}; + +export const UpdateBookmarkSheet = (props: UpdateBookmarkSheetProps) => { + const { data } = props; + const { mutate, isPending } = useUpdateBookmark(data.id); + const [open, setOpen] = useState(false); + + const { control, handleSubmit, formState, reset } = useForm({ + resolver: joiResolver(BookmarkPayloadSchema), + }); + + useEffect(() => { + reset({ ...data }); + }, [data, reset]); + + const onSubmit = (data: BookmarkPayload) => { + mutate(data, { + onSuccess: () => { + Toast.show({ + text1: "Félicitations !", + text2: "Votre signet a été modifié avec succès.", + type: "success", + }); + setOpen(false); + }, + onError: (error: ErrorResponse) => { + Toast.show({ + text1: "Erreur", + text2: safeMessage(error), + type: "error", + }); + }, + }); + }; + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/bookmark/index.ts b/projects/mobile/src/ui/components/content/bookmark/index.ts new file mode 100644 index 0000000..21225e3 --- /dev/null +++ b/projects/mobile/src/ui/components/content/bookmark/index.ts @@ -0,0 +1,5 @@ +export { BookmarkCard } from "@/ui/components/content/bookmark/BookmarkCard"; +export { BookmarkEmptyState } from "@/ui/components/content/bookmark/BookmarkEmptyState"; +export { BookmarkList } from "@/ui/components/content/bookmark/BookmarkList"; +export { CreateBookmarkSheet } from "@/ui/components/content/bookmark/CreateBookmarkSheet"; +export { UpdateBookmarkSheet } from "@/ui/components/content/bookmark/UpdateBookmarkSheet"; diff --git a/projects/mobile/src/ui/components/content/source/SourceFollowButton.tsx b/projects/mobile/src/ui/components/content/source/SourceFollowButton.tsx new file mode 100644 index 0000000..2a585db --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceFollowButton.tsx @@ -0,0 +1,62 @@ +import { useCallback, useState } from "react"; + +import { ActivityIndicator, Alert } from "react-native"; +import { Button, GetProps } from "tamagui"; + +import { useFollowSource, useUnfollowSource } from "@/api/request/feed-management/source"; + +type SourceFollowButtonProps = GetProps & { + id: string; + name: string; + followed: boolean; +}; + +export const SourceFollowButton = (props: SourceFollowButtonProps) => { + const { id, followed, name, ...rest } = props; + const [isFollowed, setIsFollowed] = useState(followed); + const { mutate: follow, isPending: following } = useFollowSource(id); + const { mutate: unfollow, isPending: unfollowing } = useUnfollowSource(id); + const loading = following || unfollowing; + + const handlePress = useCallback(() => { + if (isFollowed) { + Alert.alert( + "Confirmation", + `Êtes-vous sûr de vouloir ne plus suivre ${name} ?`, + [ + { + text: "Annuler", + style: "cancel", + }, + { + text: "Ne plus suivre", + style: "destructive", + onPress: () => { + unfollow(); + setIsFollowed(prev => !prev); + }, + }, + ], + { cancelable: false } + ); + } else { + follow(); + setIsFollowed(prev => !prev); + } + }, [isFollowed, name, unfollow, follow, setIsFollowed]); + + return ( + + ); +}; diff --git a/projects/mobile/src/ui/components/content/source/SourceList.tsx b/projects/mobile/src/ui/components/content/source/SourceList.tsx new file mode 100644 index 0000000..89d8c33 --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceList.tsx @@ -0,0 +1,55 @@ +import React, { useCallback } from "react"; + +import { FlatList, FlatListProps } from "react-native"; +import { Paragraph, XStack, YStack } from "tamagui"; + +import { SourceOverview } from "@/api/schema/feed-management/source"; +import { SourceOverviewCard } from "@/ui/components/content/source/SourceOverviewCard"; + +const HorizontalSeparator = () => ; +const VerticalSeparator = () => ; + +type SourceOverviewListProps = Omit, "renderItem"> & { + data: SourceOverview[]; + horizontal?: boolean; + infiniteScroll?: boolean; +}; + +type SourceOverviewListComponent = React.FC & { + HorizontalSeparator: typeof HorizontalSeparator; + VerticalSeparator: typeof VerticalSeparator; +}; + +const keyExtractor = (item: SourceOverview) => item.name; + +const SourceList: SourceOverviewListComponent = (props: SourceOverviewListProps) => { + const { data, horizontal = false, ...rest } = props; + + const renderItem = useCallback( + ({ item }: { item: SourceOverview }) => { + return ; + }, + [horizontal] + ); + + return ( + Pas de sources disponibles pour le moment.} + /> + ); +}; + +SourceList.HorizontalSeparator = HorizontalSeparator; +SourceList.VerticalSeparator = VerticalSeparator; + +export { SourceList }; diff --git a/projects/mobile/src/ui/components/content/source/SourceOverviewCard.tsx b/projects/mobile/src/ui/components/content/source/SourceOverviewCard.tsx new file mode 100644 index 0000000..64744e8 --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceOverviewCard.tsx @@ -0,0 +1,76 @@ +import { Link } from "expo-router"; +import { GetProps, styled, XStack, YStack } from "tamagui"; + +import { SourceOverview } from "@/api/schema/feed-management/source"; +import { SourceFollowButton } from "@/ui/components/content/source/SourceFollowButton"; +import { SourceProfileImage } from "@/ui/components/content/source/SourceProfileImage"; +import { Text } from "@/ui/components/typography"; + +const SourceCardFrame = styled(YStack, { + alignItems: "center", + gap: "$2", + borderRadius: "$4", + + variants: { + horizontal: { + true: { + maxWidth: 100, + flexShrink: 0, + }, + false: { + flexDirection: "row", + justifyContent: "space-between", + width: "100%", + gap: "$4", + paddingVertical: "$2", + }, + }, + }, +} as const); + +type SourceCardProps = GetProps & { + data: SourceOverview; + horizontal?: boolean; +}; + +export const SourceOverviewCard = (props: SourceCardProps) => { + const { data, horizontal = true, ...rest } = props; + + const nameFontSize = horizontal ? "$3" : "$4"; + + return ( + + + + + + + {horizontal ? ( + + {data.displayName ?? data.name} + + ) : ( + + + + {data.displayName ?? data.name} + + + + + {data.url} + + + )} + + + + + ); +}; diff --git a/projects/mobile/src/ui/components/content/source/SourceProfileImage.tsx b/projects/mobile/src/ui/components/content/source/SourceProfileImage.tsx new file mode 100644 index 0000000..ca2eeb4 --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceProfileImage.tsx @@ -0,0 +1,32 @@ +import type React from "react"; + +import { GetProps, Image, styled } from "tamagui"; + +const StyledImage = styled(Image, { + borderRadius: "$12", + backgroundColor: "white", +}); + +type SourceAvatarProps = GetProps & { + image: string; + name: string; + size?: number; +}; + +export const SourceProfileImage = (props: SourceAvatarProps) => { + const { image, name, size = 50, ...rest } = props; + + return ( + + ); +}; diff --git a/projects/mobile/src/ui/components/content/source/SourceReferencePill.tsx b/projects/mobile/src/ui/components/content/source/SourceReferencePill.tsx new file mode 100644 index 0000000..60a29b8 --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceReferencePill.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { Link } from "expo-router"; +import { Avatar, GetProps, XStack } from "tamagui"; + +import { SourceReference } from "@/api/schema/feed-management/source"; +import { Text } from "@/ui/components/typography"; + +type SourceReferencePillProps = GetProps & { + data: SourceReference; +}; + +export function SourceReferencePill(props: SourceReferencePillProps) { + const { data, ...rest } = props; + + return ( + + + + + + + + {data.displayName ?? data.name} + + + + ); +} diff --git a/projects/mobile/src/ui/components/content/source/SourceSkeleton.tsx b/projects/mobile/src/ui/components/content/source/SourceSkeleton.tsx new file mode 100644 index 0000000..8555bfe --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/SourceSkeleton.tsx @@ -0,0 +1,82 @@ +import React, { useCallback } from "react"; + +import ContentLoader, { Circle, Rect } from "react-content-loader/native"; +import { FlatList } from "react-native"; +import { YStack } from "tamagui"; + +import { SourceList } from "@/ui/components/content/source/SourceList"; + +const data: number[] = new Array(5).fill(0); + +type SourceSkeletonListProps = { + horizontal?: boolean; +}; + +const VerticalSkeleton = (props: any) => ( + + + + + + +); + +const HorizontalSkeleton = (props: any) => ( + + + + + +); + +const keyExtractor = (_: number, index: number) => index.toString(); + +const selectSkeletonComponent = (horizontal: boolean) => { + return horizontal ? ( + + ) : ( + + + + ); +}; + +export const SourceSkeletonList = (props: SourceSkeletonListProps) => { + const { horizontal = false } = props; + + const ItemSeparator = horizontal ? SourceList.HorizontalSeparator : SourceList.VerticalSeparator; + + const renderItem = useCallback(() => { + return selectSkeletonComponent(horizontal); + }, [horizontal]); + + return ( + + ); +}; diff --git a/projects/mobile/src/ui/components/content/source/index.ts b/projects/mobile/src/ui/components/content/source/index.ts new file mode 100644 index 0000000..244fccb --- /dev/null +++ b/projects/mobile/src/ui/components/content/source/index.ts @@ -0,0 +1,6 @@ +export { SourceList } from "@/ui/components/content/source/SourceList"; +export { SourceFollowButton } from "@/ui/components/content/source/SourceFollowButton"; +export { SourceReferencePill } from "@/ui/components/content/source/SourceReferencePill"; +export { SourceSkeletonList } from "@/ui/components/content/source/SourceSkeleton"; +export { SourceProfileImage } from "@/ui/components/content/source/SourceProfileImage"; +export { SourceOverviewCard } from "@/ui/components/content/source/SourceOverviewCard"; diff --git a/projects/mobile/src/ui/components/controls/BackButton.tsx b/projects/mobile/src/ui/components/controls/BackButton.tsx new file mode 100644 index 0000000..1c68312 --- /dev/null +++ b/projects/mobile/src/ui/components/controls/BackButton.tsx @@ -0,0 +1,25 @@ +import { ArrowLeft } from "@tamagui/lucide-icons"; +import { Button, ButtonProps } from "tamagui"; + +type BackButtonProps = { + onPress: () => void; +}; + +export const BackButton = (props: BackButtonProps & ButtonProps) => { + const { onPress, ...rest } = props; + + return ( +