diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..ccf4dad --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,83 @@ +name: PHPUnit + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + php-matrix: + name: PHP matrix + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.php-matrix.outputs.versions }} + steps: + - uses: actions/checkout@v5 + + - uses: typisttech/php-matrix-action@v3 + id: php-matrix + + unit: + name: Unit (PHP ${{ matrix.php }}) + needs: php-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ${{ fromJSON(needs.php-matrix.outputs.versions) }} + steps: + - uses: actions/checkout@v5 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer + + - uses: ramsey/composer-install@v3 + + - run: composer test:unit + + integration: + name: Integration (PHP ${{ matrix.php }}) + needs: php-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ${{ fromJSON(needs.php-matrix.outputs.versions) }} + services: + mariadb: + image: mariadb:11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: wordpress_test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + env: + WP_TESTS_DB_HOST: 127.0.0.1 + WP_TESTS_DB_NAME: wordpress_test + WP_TESTS_DB_USER: root + WP_TESTS_DB_PASSWORD: root + steps: + - uses: actions/checkout@v5 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mysqli + coverage: none + tools: composer + + - uses: ramsey/composer-install@v3 + + - run: composer test:integration diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..4b8ff70 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,40 @@ +name: Playwright + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + playwright: + name: Playwright + runs-on: ubuntu-latest + env: + WP_BASE_URL: http://localhost:8888 + WP_CLI: npx wp-env run cli wp + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - run: npm ci + + - run: npx playwright install --with-deps chromium + + - run: npm run env:start + + - run: npm run test:e2e + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: tests/E2E/.report + retention-days: 7 diff --git a/.gitignore b/.gitignore index 57872d0..20e7113 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ /vendor/ +/.phpunit.result.cache +node_modules/ +/tests/E2E/.state/ +/tests/E2E/.results/ +/tests/E2E/.report/ diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 0000000..e389c93 --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "phpVersion": "8.3", + "plugins": [ + ".", + "https://downloads.wordpress.org/plugin/woocommerce.10.9.4.zip", + "pronamic/woocommerce-subscriptions#v9.0.0" + ], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_DISPLAY": false + }, + "mappings": { + "wp-content/mu-plugins": "./tests/E2E/support/mu-plugins" + } +} diff --git a/README.md b/README.md index c0710e7..aac0f4d 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,132 @@ Customizations at WordPress relative to website libresign.coop - Go to Configurations of this plugin - Create the GitHub token and add at configuration page - Set the organization and repository that have the deploy action + +## Architecture + +`src/` holds the decisions and `includes/` plus the main plugin file hold the +wiring: the hooks, the options and the effects. A decision receives values and +returns values — the navigation receives the query vars instead of reading +`global $wp`, the webhook gate receives the headers instead of a +`WP_REST_Request` — so it is covered by a data provider and no test double. + +``` +src/ +├── Account/Navigation.php # entries, labels and the active one +├── Account/RootEndpoint.php # account screens served from the site root +├── Github/DeployDispatch.php # when publishing asks GitHub for a deploy +├── Github/SiteDeploy.php # which run publishes the site +├── Github/WebhookDecision.php # what to answer a delivery +├── Github/WebhookGate.php # inspection of a delivery +├── Github/WebhookRequest.php # headers and body of a delivery +├── Github/WebhookSignature.php # the HMAC GitHub signs with +├── Github/WorkflowRun.php # the run a payload describes +├── Settings/Secret.php # the cipher of the token and the secret +└── Subscription/StatusChange.php # changes that ask for a confirmation +``` + +The plugin is installed by cloning the repository, so Composer never runs on the +server: `src/Autoloader.php` maps the namespace to `src/` and is the only file +the plugin requires by hand. + +## Development + +Every check is a Composer script: + +```bash +composer lint # php -l on every file +composer cs # PHPCS +composer stan # PHPStan +composer test # PHPUnit +composer ci # all of the above, in this order +``` + +### Tests + +`composer install` brings in WordPress itself (`vendor/wordpress`) and the +WordPress test suite, so the only thing the tests need from outside is a +MySQL/MariaDB server and a database they are allowed to wipe on every run. + +| Variable | Default | +|---|---| +| `WP_TESTS_DB_NAME` | `wordpress_test` | +| `WP_TESTS_DB_USER` | `root` | +| `WP_TESTS_DB_PASSWORD` | `root` | +| `WP_TESTS_DB_HOST` | `mariadb` | +| `WP_TESTS_TABLE_PREFIX` | `wptests_` | +| `WP_CORE_DIR` | `vendor/wordpress` | + +The defaults are the ones of the local SaaS stack, where both the database and +Composer already live inside the containers: + +```bash +docker exec wordpress-docker-mariadb-1 \ + mariadb -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS wordpress_test;' + +docker exec -w /var/www/html/wp-content/plugins/libresign-wp-customizations \ + wordpress-docker-wordpress-1 composer test +``` + +`tests/Unit/` mirrors `src/` and `tests/Integration/` mirrors the plugin files, +in both cases file by file with `Test.php` appended: +`src/Github/WebhookGate.php` is covered by +`tests/Unit/Github/WebhookGateTest.php`, and the endpoint wiring it serves, +`includes/github-site-webhook.php`, by +`tests/Integration/Includes/GithubSiteWebhookTest.php`. A decision is covered by +a unit test, and the wiring by an integration test going through WordPress: +options, hooks, the REST server or the database. + +Nothing is mocked. Outgoing HTTP is answered through the `pre_http_request` +filter (`tests/Support/FakeHttp.php`), which is WordPress' own extension point, +and any request that is not answered that way fails the test instead of +reaching the network. + +WooCommerce is not installed in this suite, so the screens that only exist with +WooCommerce loaded are covered by the browser tests instead. + +### Browser tests + +`tests/E2E/` mirrors `src/` the same way, with `.spec.ts` in place of +`Test.php`: `src/Account/Navigation.php` is covered end to end by +`tests/E2E/Account/Navigation.spec.ts`. What lives here is what PHPUnit cannot +reach without WooCommerce, the rewrite rules and a browser — the account +navigation, the account screens served from the site root and the confirmation +of a subscription status change. + +The suite runs against a WordPress that is already up, described by four +variables: + +| Variable | Default | +|---|---| +| `WP_BASE_URL` | `http://localhost` | +| `WP_CLI` | `docker exec -i -u www-data wordpress-docker-wordpress-1 wp --path=/var/www/html` | +| `WP_E2E_CUSTOMER_PASSWORD` | `libresign-e2e` | +| `WP_E2E_ALLOW_ANY_SITE` | unset | + +The defaults are the local SaaS stack again, so there it takes no arguments: + +```bash +npm install +npx playwright install chromium +npm run test:e2e +``` + +`tests/E2E/support/seed.php` puts the site in the state the specs expect, and +runs again before each test that changes the subscription. It is destructive and +nothing is restored afterwards: it makes My Account the front page, rebuilds the +rewrite rules, and creates `libresign_e2e_customer` with the password above, +which this repository publishes. That is why it refuses to run against anything +but `localhost` unless `WP_E2E_ALLOW_ANY_SITE=1` says so — point it at a site you +can throw away, never at production or staging. + +Anywhere else, point the first two at the site under test. `.wp-env.json` +describes a disposable one, which is what CI runs: + +```bash +npm run env:start +WP_BASE_URL=http://localhost:8888 WP_CLI="npx wp-env run cli wp" npm run test:e2e +``` + +WooCommerce only offers the billing screen when one of the available gateways +keeps payment methods. The local stack has Stripe for that; `.wp-env.json` maps +a mu-plugin that declares one. diff --git a/composer.json b/composer.json index 922664a..c9f7d99 100644 --- a/composer.json +++ b/composer.json @@ -12,24 +12,53 @@ "php-stubs/woocommerce-stubs": "^9.0", "phpcompatibility/phpcompatibility-wp": "^2.1", "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "roots/wordpress": "^7.0", "squizlabs/php_codesniffer": "^3.13", "szepeviktor/phpstan-wordpress": "^2.0", - "wp-coding-standards/wpcs": "^3.2" + "wp-coding-standards/wpcs": "^3.2", + "wp-phpunit/wp-phpunit": "^7.0", + "yoast/phpunit-polyfills": "^4.0" + }, + "autoload": { + "psr-4": { + "LibreSign\\WPCustomizations\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "LibreSign\\WPCustomizations\\Tests\\": "tests/" + } }, "config": { + "platform": { + "php": "8.3" + }, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "roots/wordpress-core-installer": true } }, + "extra": { + "wordpress-install-dir": "vendor/wordpress" + }, "scripts": { - "lint": "parallel-lint --exclude vendor .", + "lint": "parallel-lint --exclude vendor --exclude node_modules .", "cs": "phpcs", "cs:fix": "phpcbf", "stan": "phpstan analyse", + "test": [ + "@test:unit", + "@test:integration" + ], + "test:unit": "phpunit", + "test:integration": "phpunit -c phpunit-integration.xml.dist", "ci": [ "@lint", "@cs", - "@stan" + "@stan", + "@test" ] } } diff --git a/composer.lock b/composer.lock index c8a35da..69a4d61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "469d5c8e46eb384b802b9d91e6a9b7b1", + "content-hash": "d9742c9c41772fe849372858661d4ea4", "packages": [], "packages-dev": [ { @@ -103,6 +103,311 @@ ], "time": "2026-05-06T08:26:05+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": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^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.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "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.x-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.9.0" + }, + "time": "2026-09-13T18:51:52+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": "php-parallel-lint/php-parallel-lint", "version": "v1.4.0", @@ -713,83 +1018,1747 @@ "time": "2026-09-12T21:39:33+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.13.6", + "name": "phpstan/phpstan-phpunit", + "version": "2.0.18", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", - "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f5dc20ff8082d02339b60cab68ec3eb0d859fb30", + "reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "phar-io/version": "^3.2", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.2.3" + }, + "conflict": { + "phpunit/phpunit": "<7.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], - "type": "library", "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" - } + "MIT" ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "description": "PHPUnit extensions and rules for PHPStan", "keywords": [ - "phpcs", - "standards", "static analysis" ], "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.18" }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2026-08-06T00:17:32+00:00" + "time": "2026-07-04T12:16:09+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "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": "9.2.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/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-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", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.36", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/abab27ed286d3e1246fbbfe6b56bfd732d945ec9", + "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.9", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-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/9.6.36" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-08-11T06:25:15+00:00" + }, + { + "name": "roots/wordpress", + "version": "7.1", + "source": { + "type": "git", + "url": "https://github.com/roots/wordpress.git", + "reference": "eb3d108f216618a858dcaed691e00e68f0ccff9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/wordpress/zipball/eb3d108f216618a858dcaed691e00e68f0ccff9f", + "reference": "eb3d108f216618a858dcaed691e00e68f0ccff9f", + "shasum": "" + }, + "require": { + "roots/wordpress-core-installer": "^4.0", + "roots/wordpress-no-content": "self.version" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-2.0-or-later" + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org/", + "keywords": [ + "blog", + "cms", + "wordpress" + ], + "support": { + "issues": "https://github.com/roots/wordpress/issues", + "source": "https://github.com/roots/wordpress/tree/7.1" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + } + ], + "time": "2026-03-22T13:26:54+00:00" + }, + { + "name": "roots/wordpress-core-installer", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/roots/wordpress-core-installer.git", + "reference": "85d3589252eb93eee836facf33b54ce30c565bea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/wordpress-core-installer/zipball/85d3589252eb93eee836facf33b54ce30c565bea", + "reference": "85d3589252eb93eee836facf33b54ce30c565bea", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": ">=7.2.24" + }, + "conflict": { + "composer/installers": "<1.0.6" + }, + "replace": { + "johnpbloch/wordpress-core-installer": "*" + }, + "require-dev": { + "composer/composer": "^2.0", + "phpunit/phpunit": "^8.5" + }, + "type": "composer-plugin", + "extra": { + "class": "Roots\\Composer\\WordPressCorePlugin" + }, + "autoload": { + "psr-4": { + "Roots\\Composer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "John P. Bloch", + "email": "me@johnpbloch.com" + }, + { + "name": "Roots", + "email": "team@roots.io" + } + ], + "description": "A Composer custom installer to handle installing WordPress as a dependency", + "keywords": [ + "wordpress" + ], + "support": { + "issues": "https://github.com/roots/wordpress-core-installer/issues", + "source": "https://github.com/roots/wordpress-core-installer/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + } + ], + "time": "2026-03-22T03:17:23+00:00" + }, + { + "name": "roots/wordpress-no-content", + "version": "7.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress.git", + "reference": "7.1" + }, + "dist": { + "type": "zip", + "url": "https://downloads.wordpress.org/release/wordpress-7.1-no-content.zip", + "reference": "7.1", + "shasum": "7b28e5af89345d79a30b44a16cbe3244379d1765" + }, + "require": { + "php": ">= 7.4" + }, + "provide": { + "wordpress/core-implementation": "7.1" + }, + "suggest": { + "ext-curl": "Performs remote request operations.", + "ext-dom": "Used to validate Text Widget content and to automatically configuring IIS7+.", + "ext-exif": "Works with metadata stored in images.", + "ext-fileinfo": "Used to detect mimetype of file uploads.", + "ext-hash": "Used for hashing, including passwords and update packages.", + "ext-imagick": "Provides better image quality for media uploads.", + "ext-json": "Used for communications with other servers.", + "ext-libsodium": "Validates Signatures and provides securely random bytes.", + "ext-mbstring": "Used to properly handle UTF8 text.", + "ext-mysqli": "Connects to MySQL for database interactions.", + "ext-openssl": "Permits SSL-based connections to other hosts.", + "ext-pcre": "Increases performance of pattern matching in code searches.", + "ext-xml": "Used for XML parsing, such as from a third-party site.", + "ext-zip": "Used for decompressing Plugins, Themes, and WordPress update packages." + }, + "type": "wordpress-core", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress Community", + "homepage": "https://wordpress.org/about/" + } + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org/", + "keywords": [ + "blog", + "cms", + "wordpress" + ], + "support": { + "docs": "https://developer.wordpress.org/", + "forum": "https://wordpress.org/support/", + "irc": "irc://irc.freenode.net/wordpress", + "issues": "https://core.trac.wordpress.org/", + "rss": "https://wordpress.org/news/feed/", + "source": "https://core.trac.wordpress.org/browser", + "wiki": "https://codex.wordpress.org/" + }, + "funding": [ + { + "url": "https://wordpressfoundation.org/donate/", + "type": "other" + } + ], + "time": "2026-08-19T20:16:40+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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 PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "funding": [ + { + "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/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-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": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.9" + }, + "funding": [ + { + "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/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-11T04:55:59+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "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/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "c85be6922b7fd365942b986b9a50397d65407611" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c85be6922b7fd365942b986b9a50397d65407611", + "reference": "c85be6922b7fd365942b986b9a50397d65407611", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.7" + }, + "funding": [ + { + "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/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-11T05:25:24+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.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 a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-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", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.6", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-08-06T00:17:32+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -854,6 +2823,56 @@ }, "time": "2026-05-22T16:22:09+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "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.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.4.1", @@ -919,6 +2938,117 @@ } ], "time": "2026-07-27T11:53:23+00:00" + }, + { + "name": "wp-phpunit/wp-phpunit", + "version": "7.1.0", + "source": { + "type": "git", + "url": "https://github.com/wp-phpunit/wp-phpunit.git", + "reference": "797edc710afc958854852d859c93d9aa1de6447d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-phpunit/wp-phpunit/zipball/797edc710afc958854852d859c93d9aa1de6447d", + "reference": "797edc710afc958854852d859c93d9aa1de6447d", + "shasum": "" + }, + "type": "library", + "autoload": { + "files": [ + "__loaded.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Evan Mattson", + "email": "me@aaemnnost.tv" + }, + { + "name": "WordPress Community", + "homepage": "https://wordpress.org/about/" + } + ], + "description": "WordPress core PHPUnit library", + "homepage": "https://github.com/wp-phpunit", + "keywords": [ + "phpunit", + "test", + "wordpress" + ], + "support": { + "docs": "https://github.com/wp-phpunit/docs", + "issues": "https://github.com/wp-phpunit/issues", + "source": "https://github.com/wp-phpunit/wp-phpunit" + }, + "time": "2026-08-20T00:59:59+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/134921bfca9b02d8f374c48381451da1d98402f9", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0 || ^11.0 || ^12.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2025-02-09T18:58:54+00:00" } ], "aliases": [], @@ -930,5 +3060,8 @@ "php": ">=8.3 <8.6" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.3" + }, "plugin-api-version": "2.9.0" } diff --git a/includes/account-navigation.php b/includes/account-navigation.php index ae7e05f..5f4f103 100644 --- a/includes/account-navigation.php +++ b/includes/account-navigation.php @@ -9,6 +9,8 @@ * @package LibreSign_WP_Customizations */ +use LibreSign\WPCustomizations\Account\Navigation; + defined( 'ABSPATH' ) || exit; /** @@ -20,51 +22,11 @@ function libresign_get_current_query_vars() { return isset( $wp->query_vars ) ? $wp->query_vars : array(); } -/** - * Customer navigation entries kept, in display order. - */ -function libresign_get_account_menu_order() { - return array( 'subscriptions', 'orders', 'payment-methods', 'edit-account', 'customer-logout' ); -} - -/** - * Navigation entries renamed because their meaning changed here. - * - * Whatever is left out keeps the WooCommerce label, and with it the translation - * WooCommerce already ships for every locale. - */ -function libresign_get_account_menu_labels() { - return array( - 'subscriptions' => __( 'My subscription', 'libresign-wp-customizations' ), - 'orders' => __( 'Invoices', 'libresign-wp-customizations' ), - 'payment-methods' => __( 'Billing', 'libresign-wp-customizations' ), - ); -} - -/** - * Detail endpoints neither core nor Subscriptions already highlights, mapped to their entry. - */ -function libresign_get_account_menu_item_aliases() { - return array( - 'subscriptions' => array( 'subscription-payment-method' ), - 'payment-methods' => array( 'edit-address' ), - ); -} - /** * Reorder and relabel the My Account navigation, skipping unregistered endpoints. */ function libresign_filter_account_menu_items( $items ) { - $labels = libresign_get_account_menu_labels(); - $menu = array(); - - foreach ( libresign_get_account_menu_order() as $endpoint ) { - if ( isset( $items[ $endpoint ] ) ) { - $menu[ $endpoint ] = isset( $labels[ $endpoint ] ) ? $labels[ $endpoint ] : $items[ $endpoint ]; - } - } - - return $menu; + return Navigation::filter_items( (array) $items ); } add_filter( 'woocommerce_account_menu_items', 'libresign_filter_account_menu_items', 20 ); @@ -72,26 +34,7 @@ function libresign_filter_account_menu_items( $items ) { * Highlight the navigation entry a detail screen belongs to. */ function libresign_filter_account_menu_item_classes( $classes, $endpoint ) { - if ( in_array( 'is-active', $classes, true ) ) { - return $classes; - } - - $aliases = libresign_get_account_menu_item_aliases(); - - if ( empty( $aliases[ $endpoint ] ) ) { - return $classes; - } - - $query_vars = libresign_get_current_query_vars(); - - foreach ( $aliases[ $endpoint ] as $alias ) { - if ( isset( $query_vars[ $alias ] ) ) { - $classes[] = 'is-active'; - break; - } - } - - return $classes; + return Navigation::filter_item_classes( (array) $classes, $endpoint, libresign_get_current_query_vars() ); } add_filter( 'woocommerce_account_menu_item_classes', 'libresign_filter_account_menu_item_classes', 10, 2 ); @@ -99,25 +42,7 @@ function libresign_filter_account_menu_item_classes( $classes, $endpoint ) { * Page title of an endpoint whose navigation label was renamed. */ function libresign_filter_account_endpoint_title( $title, $endpoint ) { - $labels = libresign_get_account_menu_labels(); - - if ( ! isset( $labels[ $endpoint ] ) ) { - return $title; - } - - $query_vars = libresign_get_current_query_vars(); - $page_number = isset( $query_vars[ $endpoint ] ) ? intval( $query_vars[ $endpoint ] ) : 0; - - if ( $page_number < 2 ) { - return $labels[ $endpoint ]; - } - - return sprintf( - /* translators: 1: navigation label, 2: page number */ - __( '%1$s (page %2$d)', 'libresign-wp-customizations' ), - $labels[ $endpoint ], - $page_number - ); + return Navigation::endpoint_title( $title, $endpoint, libresign_get_current_query_vars() ); } /** @@ -144,7 +69,7 @@ function libresign_filter_view_order_endpoint_title( $title ) { * Keep the endpoint page titles in sync with the navigation labels. */ function libresign_register_account_endpoint_titles() { - foreach ( array_keys( libresign_get_account_menu_labels() ) as $endpoint ) { + foreach ( array_keys( Navigation::labels() ) as $endpoint ) { add_filter( 'woocommerce_endpoint_' . $endpoint . '_title', 'libresign_filter_account_endpoint_title', 20, 2 ); } diff --git a/includes/github-site-webhook.php b/includes/github-site-webhook.php index 551bf0a..a2e65c3 100644 --- a/includes/github-site-webhook.php +++ b/includes/github-site-webhook.php @@ -5,6 +5,10 @@ * @package LibreSign_WP_Customizations */ +use LibreSign\WPCustomizations\Github\SiteDeploy; +use LibreSign\WPCustomizations\Github\WebhookGate; +use LibreSign\WPCustomizations\Github\WebhookRequest; + defined( 'ABSPATH' ) || exit; const LIBRESIGN_GITHUB_SITE_WEBHOOK_NAMESPACE = 'libresign/v1'; @@ -37,38 +41,13 @@ function libresign_github_site_webhook_endpoint_url() { return rest_url( ltrim( LIBRESIGN_GITHUB_SITE_WEBHOOK_NAMESPACE . LIBRESIGN_GITHUB_SITE_WEBHOOK_ROUTE, '/' ) ); } -/** - * Decrypt an encrypted plugin option value. - * - * @param string $value Raw stored value. - * @return string - */ -function libresign_decrypt_plugin_secret( $value ) { - $value = trim( (string) $value ); - if ( '' === $value ) { - return ''; - } - - $decoded = base64_decode( $value, true ); - if ( false === $decoded ) { - return $value; - } - - $key = hash( 'sha256', AUTH_KEY . SECURE_AUTH_SALT ); - $iv = substr( hash( 'sha256', NONCE_SALT ), 0, 16 ); - - $decrypted = openssl_decrypt( $decoded, 'AES-256-CBC', $key, 0, $iv ); - - return false === $decrypted ? $value : trim( (string) $decrypted ); -} - /** * Resolve the configured GitHub webhook secret. * * @return string */ function libresign_github_webhook_secret() { - return libresign_decrypt_plugin_secret( get_option( 'libresign_github_webhook_secret', '' ) ); + return libresign_plugin_secret()->decrypt( get_option( 'libresign_github_webhook_secret', '' ) ); } /** @@ -115,6 +94,17 @@ function libresign_site_deploy_branch_name() { return '' === $name ? 'gh-pages' : $name; } +/** + * @return SiteDeploy + */ +function libresign_site_deploy() { + return new SiteDeploy( + libresign_site_deploy_repository_name(), + libresign_site_deploy_branch_name(), + libresign_site_deploy_workflow_name() + ); +} + /** * Build a standardized ignored response. * @@ -129,94 +119,6 @@ function libresign_github_site_webhook_ignored_response( $data = array(), $statu return $response; } -/** - * Verify the GitHub webhook HMAC signature. - * - * @param string $body Raw request body. - * @param string $signature Signature header value. - * @param string $secret Shared secret. - * @return bool - */ -function libresign_verify_github_webhook_signature( $body, $signature, $secret ) { - $secret = trim( (string) $secret ); - $signature = trim( (string) $signature ); - - if ( '' === $body || '' === $secret || '' === $signature ) { - return false; - } - - if ( 0 === stripos( $signature, 'sha256=' ) ) { - $signature = substr( $signature, 7 ); - } - - if ( ! ctype_xdigit( $signature ) ) { - return false; - } - - $expected = hash_hmac( 'sha256', $body, $secret ); - - return hash_equals( $expected, strtolower( $signature ) ); -} - -/** - * Check whether the webhook user agent looks like GitHub Hookshot. - * - * @param string $user_agent User agent header. - * @return bool - */ -function libresign_is_github_hookshot_user_agent( $user_agent ) { - return 0 === strpos( trim( (string) $user_agent ), 'GitHub-Hookshot/' ); -} - -/** - * Extract the workflow name from the payload. - * - * @param array $payload Parsed payload. - * @return string - */ -function libresign_site_deploy_workflow_name_from_payload( $payload ) { - $workflow_run_name = isset( $payload['workflow_run']['name'] ) ? trim( (string) $payload['workflow_run']['name'] ) : ''; - if ( '' !== $workflow_run_name ) { - return $workflow_run_name; - } - - $workflow_name = isset( $payload['workflow']['name'] ) ? trim( (string) $payload['workflow']['name'] ) : ''; - - return $workflow_name; -} - -/** - * Determine whether the payload represents the production site deploy event. - * - * @param array $payload Parsed payload. - * @return bool - */ -function libresign_is_production_site_deploy_workflow_run( $payload ) { - $repository = isset( $payload['repository']['full_name'] ) ? trim( (string) $payload['repository']['full_name'] ) : ''; - $action = isset( $payload['action'] ) ? trim( (string) $payload['action'] ) : ''; - $conclusion = isset( $payload['workflow_run']['conclusion'] ) ? trim( (string) $payload['workflow_run']['conclusion'] ) : ''; - $head_branch = isset( $payload['workflow_run']['head_branch'] ) ? trim( (string) $payload['workflow_run']['head_branch'] ) : ''; - $workflow_name = libresign_site_deploy_workflow_name_from_payload( $payload ); - - if ( libresign_site_deploy_repository_name() !== $repository ) { - return false; - } - - if ( 'completed' !== $action ) { - return false; - } - - if ( 'success' !== $conclusion ) { - return false; - } - - if ( libresign_site_deploy_branch_name() !== $head_branch ) { - return false; - } - - return libresign_site_deploy_workflow_name() === $workflow_name; -} - /** * Mark a webhook delivery as processed, returning false when duplicated. * @@ -265,36 +167,27 @@ function libresign_record_site_fragment_sync_result( $status, $payload ) { * @return WP_REST_Response|WP_Error */ function libresign_receive_github_site_deploy_webhook( $request ) { - $secret = libresign_github_webhook_secret(); - if ( '' === $secret ) { - return new WP_Error( - 'libresign_github_webhook_secret_missing', - __( 'The GitHub webhook secret is not configured.', 'libresign-wp-customizations' ), - array( 'status' => 503 ) - ); - } - - $user_agent = (string) $request->get_header( 'user-agent' ); - if ( ! libresign_is_github_hookshot_user_agent( $user_agent ) ) { - return new WP_Error( - 'libresign_github_webhook_invalid_agent', - __( 'The webhook request does not look like a GitHub delivery.', 'libresign-wp-customizations' ), - array( 'status' => 403 ) - ); - } + $webhook_gate = new WebhookGate( libresign_github_webhook_secret(), libresign_site_deploy() ); + + $decision = $webhook_gate->decide( + new WebhookRequest( + (string) $request->get_header( 'user-agent' ), + (string) $request->get_header( 'x-github-event' ), + (string) $request->get_header( 'x-hub-signature-256' ), + (string) $request->get_header( 'x-github-delivery' ), + (string) $request->get_body() + ) + ); - $body = (string) $request->get_body(); - $signature = (string) $request->get_header( 'x-hub-signature-256' ); - if ( ! libresign_verify_github_webhook_signature( $body, $signature, $secret ) ) { + if ( $decision->is_rejected() ) { return new WP_Error( - 'libresign_github_webhook_invalid_signature', - __( 'Invalid GitHub webhook signature.', 'libresign-wp-customizations' ), - array( 'status' => 403 ) + $decision->code(), + $decision->message(), + array( 'status' => $decision->status() ) ); } - $event = strtolower( trim( (string) $request->get_header( 'x-github-event' ) ) ); - if ( 'ping' === $event ) { + if ( $decision->is_pong() ) { return rest_ensure_response( array( 'status' => 'pong', @@ -303,37 +196,13 @@ function libresign_receive_github_site_deploy_webhook( $request ) { ); } - if ( 'workflow_run' !== $event ) { - return libresign_github_site_webhook_ignored_response( - array( - 'reason' => 'unsupported_event', - 'event' => $event, - ) - ); - } - - $payload = json_decode( $body, true ); - if ( ! is_array( $payload ) ) { - return new WP_Error( - 'libresign_github_webhook_invalid_payload', - __( 'The GitHub webhook payload must be valid JSON.', 'libresign-wp-customizations' ), - array( 'status' => 400 ) - ); + if ( $decision->is_ignored() ) { + return libresign_github_site_webhook_ignored_response( $decision->data() ); } - if ( ! libresign_is_production_site_deploy_workflow_run( $payload ) ) { - return libresign_github_site_webhook_ignored_response( - array( - 'reason' => 'not_production_deploy', - 'repository' => isset( $payload['repository']['full_name'] ) ? (string) $payload['repository']['full_name'] : '', - 'workflow_name' => libresign_site_deploy_workflow_name_from_payload( $payload ), - 'head_branch' => isset( $payload['workflow_run']['head_branch'] ) ? (string) $payload['workflow_run']['head_branch'] : '', - 'conclusion' => isset( $payload['workflow_run']['conclusion'] ) ? (string) $payload['workflow_run']['conclusion'] : '', - ) - ); - } + $workflow_run = $decision->workflow_run(); + $delivery_id = (string) $request->get_header( 'x-github-delivery' ); - $delivery_id = (string) $request->get_header( 'x-github-delivery' ); if ( ! libresign_mark_github_delivery_once( $delivery_id ) ) { return libresign_github_site_webhook_ignored_response( array( @@ -343,14 +212,13 @@ function libresign_receive_github_site_deploy_webhook( $request ) { ); } - $workflow_run = isset( $payload['workflow_run'] ) && is_array( $payload['workflow_run'] ) ? $payload['workflow_run'] : array(); - $sync_result = libresign_sync_site_fragments_from_origin( + $sync_result = libresign_sync_site_fragments_from_origin( libresign_site_origin(), array( 'header', 'footer' ), array( - 'generated_at' => isset( $workflow_run['updated_at'] ) ? (string) $workflow_run['updated_at'] : current_time( 'mysql', true ), - 'source_sha' => isset( $workflow_run['head_sha'] ) ? (string) $workflow_run['head_sha'] : '', - 'source_url' => isset( $workflow_run['html_url'] ) ? (string) $workflow_run['html_url'] : '', + 'generated_at' => '' === $workflow_run->updated_at() ? current_time( 'mysql', true ) : $workflow_run->updated_at(), + 'source_sha' => $workflow_run->head_sha(), + 'source_url' => $workflow_run->html_url(), ) ); @@ -370,11 +238,11 @@ function libresign_receive_github_site_deploy_webhook( $request ) { 'synced', array( 'delivery_id' => $delivery_id, - 'repository' => libresign_site_deploy_repository_name(), - 'workflow' => libresign_site_deploy_workflow_name_from_payload( $payload ), - 'head_branch' => isset( $workflow_run['head_branch'] ) ? (string) $workflow_run['head_branch'] : '', - 'source_sha' => isset( $workflow_run['head_sha'] ) ? (string) $workflow_run['head_sha'] : '', - 'source_url' => isset( $workflow_run['html_url'] ) ? (string) $workflow_run['html_url'] : '', + 'repository' => $workflow_run->repository(), + 'workflow' => $workflow_run->workflow_name(), + 'head_branch' => $workflow_run->head_branch(), + 'source_sha' => $workflow_run->head_sha(), + 'source_url' => $workflow_run->html_url(), 'synced' => $sync_result['synced'], ) ); @@ -383,8 +251,8 @@ function libresign_receive_github_site_deploy_webhook( $request ) { array( 'status' => 'synced', 'delivery_id' => $delivery_id, - 'repository' => libresign_site_deploy_repository_name(), - 'workflow' => libresign_site_deploy_workflow_name_from_payload( $payload ), + 'repository' => $workflow_run->repository(), + 'workflow' => $workflow_run->workflow_name(), 'origin' => $sync_result['origin'], 'synced' => $sync_result['synced'], ) diff --git a/libresign-wp-customizations.php b/libresign-wp-customizations.php index 2ca610e..7e32f2d 100644 --- a/libresign-wp-customizations.php +++ b/libresign-wp-customizations.php @@ -23,11 +23,28 @@ * GitHub Plugin URI: https://github.com/LibreSign/libresign-wp-customizations */ +use LibreSign\WPCustomizations\Account\RootEndpoint; +use LibreSign\WPCustomizations\Autoloader; +use LibreSign\WPCustomizations\Github\DeployDispatch; +use LibreSign\WPCustomizations\Settings\Secret; +use LibreSign\WPCustomizations\Subscription\StatusChange; + defined( 'ABSPATH' ) || exit; +require_once __DIR__ . '/src/Autoloader.php'; + +Autoloader::register(); + const LIBRESIGN_WP_REWRITE_VERSION = '9'; const LIBRESIGN_FAQ_URL = 'https://libresign.coop/faq/'; +/** + * Cipher of the settings stored encrypted. + */ +function libresign_plugin_secret() { + return Secret::from_salts( AUTH_KEY, SECURE_AUTH_SALT, NONCE_SALT ); +} + /** * Get the WooCommerce My Account page ID. */ @@ -124,59 +141,65 @@ function libresign_wp_add_noindex_meta_tag() { * Deploy the site after change the status of post */ function libresign_trigger_github_action_on_publish($new_status, $old_status, $post) { - if ($new_status === 'publish' || $old_status === 'publish' && $post->post_type === 'post') { - $encripted = get_option('libresign_github_deploy_token'); - $key = hash('sha256', AUTH_KEY . SECURE_AUTH_SALT); - $iv = substr(hash('sha256', NONCE_SALT), 0, 16); - $deploy_token = openssl_decrypt(base64_decode($encripted), 'AES-256-CBC', $key, 0, $iv); - $organizationRepository = get_option('libresign_github_deploy_organization_repository'); - $response = wp_remote_post('https://api.github.com/repos/' . $organizationRepository . '/dispatches', [ - 'body' => json_encode([ - 'event_type' => 'deploy-site', - ]), - 'headers' => [ - 'Authorization' => 'Bearer ' . $deploy_token, - 'Accept' => 'application/vnd.github+json', - 'User-Agent' => 'WordPress Hook', - ], - 'timeout' => 15, - ]); + if (!DeployDispatch::triggers_deploy($new_status, $old_status, $post->post_type)) { + return; + } - $post_data = [ - 'post_id' => $post->ID, - 'post_title' => get_the_title($post->ID), - 'language' => function_exists('pll_get_post_language') ? pll_get_post_language($post->ID) : 'indefinido', - ]; + $deploy_token = libresign_plugin_secret()->decrypt_or_discard(get_option('libresign_github_deploy_token')); + if ($deploy_token === '') { + return; + } + + $organizationRepository = get_option('libresign_github_deploy_organization_repository'); + $response = wp_remote_post('https://api.github.com/repos/' . $organizationRepository . '/dispatches', [ + 'body' => wp_json_encode([ + 'event_type' => 'deploy-site', + ]), + 'headers' => [ + 'Authorization' => 'Bearer ' . $deploy_token, + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'WordPress Hook', + ], + 'timeout' => 15, + ]); - if (is_wp_error($response)) { + $post_data = [ + 'post_id' => $post->ID, + 'post_title' => get_the_title($post->ID), + 'language' => function_exists('pll_get_post_language') ? pll_get_post_language($post->ID) : 'indefinido', + ]; + + if (is_wp_error($response)) { + $post_data = array_merge($post_data, [ + 'type' => 'error', + 'message' => $response->get_error_message(), + ]); + } else { + $response_code = (int) wp_remote_retrieve_response_code($response); + if ($response_code === 204) { $post_data = array_merge($post_data, [ - 'type' => 'error', - 'message' => $response->get_error_message(), + 'type' => 'success', + 'message' => DeployDispatch::success_message($organizationRepository), ]); } else { - $code = wp_remote_retrieve_response_code($response); - if ($code === 204) { - $post_data = array_merge($post_data, [ - 'type' => 'success', - 'message' => 'Ação de deploy enviada com sucesso. Acompanhe aqui', - ]); - } else { - $body = json_decode($response['body'], true); - $post_data = array_merge($post_data, [ - 'type' => 'error', - 'message' => "Erro ao acionar deploy.
Código: $code.
Message: {$body['message']}", - ]); - } + $response_body = json_decode(wp_remote_retrieve_body($response), true); + $post_data = array_merge($post_data, [ + 'type' => 'error', + 'message' => DeployDispatch::failure_message( + $response_code, + isset($response_body['message']) ? $response_body['message'] : '' + ), + ]); } + } - $transient_key = 'libresign_github_action_status_' . get_current_user_id(); - $status_list = get_transient($transient_key); - if (!is_array($status_list)) { - $status_list = []; - } - $status_list[] = $post_data; - set_transient($transient_key, $status_list, 60); + $transient_key = 'libresign_github_action_status_' . get_current_user_id(); + $status_list = get_transient($transient_key); + if (!is_array($status_list)) { + $status_list = []; } + $status_list[] = $post_data; + set_transient($transient_key, $status_list, 60); } add_action('transition_post_status', 'libresign_trigger_github_action_on_publish', 10, 3); @@ -384,18 +407,29 @@ class="regular-text" } /** - * Encode the deploy token at database + * Encrypt a setting before it is stored. + * + * The field is rendered empty on every visit, so a submission with nothing in + * it means the value was left alone and the stored one is kept. + */ +function libresign_encrypt_setting($value, $option) { + $value = trim((string) $value); + + if ('' === $value) { + return get_option($option); + } + + return libresign_plugin_secret()->encrypt($value); +} + +/** + * Register the settings of the configuration page. */ -add_action('admin_init', function () { +function libresign_register_settings() { register_setting('libresign_settings_group', 'libresign_github_deploy_token', [ 'type' => 'string', 'sanitize_callback' => function ($value) { - if (!empty(trim($value))) { - $key = hash('sha256', AUTH_KEY . SECURE_AUTH_SALT); - $iv = substr(hash('sha256', NONCE_SALT), 0, 16); - return base64_encode(openssl_encrypt($value, 'AES-256-CBC', $key, 0, $iv)); - } - return get_option('libresign_github_deploy_token'); + return libresign_encrypt_setting($value, 'libresign_github_deploy_token'); }, ]); register_setting('libresign_settings_group', 'libresign_github_deploy_organization_repository', [ @@ -405,12 +439,7 @@ class="regular-text" register_setting('libresign_settings_group', 'libresign_github_webhook_secret', [ 'type' => 'string', 'sanitize_callback' => function ($value) { - if (!empty(trim($value))) { - $key = hash('sha256', AUTH_KEY . SECURE_AUTH_SALT); - $iv = substr(hash('sha256', NONCE_SALT), 0, 16); - return base64_encode(openssl_encrypt($value, 'AES-256-CBC', $key, 0, $iv)); - } - return get_option('libresign_github_webhook_secret'); + return libresign_encrypt_setting($value, 'libresign_github_webhook_secret'); }, ]); register_setting('libresign_settings_group', 'libresign_site_origin', [ @@ -434,7 +463,8 @@ class="regular-text" return '' === $value ? 'gh-pages' : $value; }, ]); -}); +} +add_action('admin_init', 'libresign_register_settings'); /** * Register WooCommerce account endpoints at the site root when the account page is the front page. @@ -624,28 +654,12 @@ function libresign_is_root_my_account_endpoint_request() { return false; } - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - $request_path = trim( (string) wp_parse_url( $request_uri, PHP_URL_PATH ), '/' ); - - if ( '' === $request_path ) { - return false; - } + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - $segments = explode( '/', $request_path ); - $first_slug = reset( $segments ); - $query_vars = WC()->query->get_query_vars(); - - if ( 'my-account' === $first_slug ) { - return true; - } - - foreach ( $query_vars as $query_var ) { - if ( ! empty( $query_var ) && $query_var === $first_slug ) { - return true; - } - } - - return false; + return RootEndpoint::matches( + (string) wp_parse_url( $request_uri, PHP_URL_PATH ), + WC()->query->get_query_vars() + ); } /** @@ -732,26 +746,6 @@ function libresign_render_nextcloud_account_button() { } add_action( 'woocommerce_before_account_navigation', 'libresign_render_nextcloud_account_button', 20 ); -/** - * Confirmation strings for each subscription status change that requires an extra step. - */ -function libresign_get_subscription_confirmation_strings( $new_status ) { - $strings = array( - 'cancelled' => array( - 'question' => __( 'Are you sure you want to cancel your subscription? This action cannot be undone.', 'libresign-wp-customizations' ), - 'confirm' => __( 'Yes, cancel subscription', 'libresign-wp-customizations' ), - 'dismiss' => __( 'No, keep subscription', 'libresign-wp-customizations' ), - ), - 'active' => array( - 'question' => __( 'Are you sure you want to reactivate your subscription?', 'libresign-wp-customizations' ), - 'confirm' => __( 'Yes, reactivate subscription', 'libresign-wp-customizations' ), - 'dismiss' => __( 'No, go back', 'libresign-wp-customizations' ), - ), - ); - - return $strings[ $new_status ] ?? null; -} - /** * Resolve a subscription the current user is allowed to update to the given status, or null. */ @@ -789,7 +783,7 @@ function libresign_intercept_subscription_status_change() { $new_status = sanitize_text_field( wp_unslash( $_GET['change_subscription_to'] ) ); - if ( ! libresign_get_subscription_confirmation_strings( $new_status ) ) { + if ( ! StatusChange::confirmation_strings( $new_status ) ) { return; } @@ -831,7 +825,7 @@ function libresign_render_subscription_change_confirmation() { } $new_status = sanitize_text_field( wp_unslash( $_GET['libresign_confirm_change'] ) ); - $strings = libresign_get_subscription_confirmation_strings( $new_status ); + $strings = StatusChange::confirmation_strings( $new_status ); if ( ! $strings ) { return; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7256f2d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5385 @@ +{ + "name": "libresign-wp-customizations", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "libresign-wp-customizations", + "license": "GPL-2.0-or-later", + "devDependencies": { + "@playwright/test": "^1.50.0", + "@wordpress/env": "^10.16.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.4.tgz", + "integrity": "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.2.tgz", + "integrity": "sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, + "node_modules/@php-wasm/cli-util": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/cli-util/-/cli-util-3.1.54.tgz", + "integrity": "sha512-FFDZDzIJz0cePI2NlWIEC1OZxZvifi5EmyPik8Qh1z+H2mVPyMjgi1xYlQXTLc/J0CPH427VfmOMwy9XqpVY7Q==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/util": "3.1.54", + "fast-xml-parser": "^5.8.0", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/logger": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/logger/-/logger-3.1.54.tgz", + "integrity": "sha512-ZrgBSSHitJkR+DU1U5dz/ezXzvU9PlhNT2GTSQJnRliNVPba84G0hudRpmnT+CH4aVThAefE3PazpGD/SG1kWA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-3.1.54.tgz", + "integrity": "sha512-7OuYnmNCiMgH01JOPAxX1QqDut66+Wz140O8AM1GZVKismjLJhxlL+vx2vDJjUQH6puvhgFwK2QOU4wpWDSUkw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/cli-util": "3.1.54", + "@php-wasm/logger": "3.1.54", + "@php-wasm/node-5-2": "3.1.54", + "@php-wasm/node-7-4": "3.1.54", + "@php-wasm/node-8-0": "3.1.54", + "@php-wasm/node-8-1": "3.1.54", + "@php-wasm/node-8-2": "3.1.54", + "@php-wasm/node-8-3": "3.1.54", + "@php-wasm/node-8-4": "3.1.54", + "@php-wasm/node-8-5": "3.1.54", + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54", + "fs-ext-extra-prebuilt": "2.2.7", + "wasm-feature-detect": "1.8.0", + "ws": "8.21.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-5-2": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-5-2/-/node-5-2-3.1.54.tgz", + "integrity": "sha512-dqGbGACtrUYn7F3urhFsXwY8SIMdok5iqzDOsi766C8RDHHEZV1+INVaDdPVkgFfwpszfQTHrRRareqNlUWS9g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-7-4": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-7-4/-/node-7-4-3.1.54.tgz", + "integrity": "sha512-X9i3UCopzF1sfZ2Ix50cT06LTZgAYbpi1g9mlhrDE5+JyyezV/YmJzY/8BIeiD1VN80rr26um+k+2CYqsgabxQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-0": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-0/-/node-8-0-3.1.54.tgz", + "integrity": "sha512-wh57ixwcacO6rqKir6Cp5NyYbncIqyt3sUD7gdXVt9/ZGI5q5ysuR5UQ4mfERKBA2rfnaCpYKpQgVPmrs92oaQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-1": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-1/-/node-8-1-3.1.54.tgz", + "integrity": "sha512-MLJ9lLGMnfitXwf6diQYJNG+k7Pv3Fzh/ZcbYsqwdITBPd4gy05BDwirIoboh3tvu3NLMhzydVz4dpJ5UYbSKw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-2": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-2/-/node-8-2-3.1.54.tgz", + "integrity": "sha512-a01+IU7ZPxYPvgwnIYzC6ob4Eu0MFwbQ1OH0JKzm/pdFnaiRNC3bvHF4bp3pjaL6266vb8D3ojBvP3hMYiX3AA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-3": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-3/-/node-8-3-3.1.54.tgz", + "integrity": "sha512-vZhfYqySKtpxb1+JvOHdGvZG82vhD7WgH5PYm7xjOYxQiXftqOtBLjwN5ZPg4562MMKrWFOLKHnW2JrxtvfyoQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-4": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-4/-/node-8-4-3.1.54.tgz", + "integrity": "sha512-I0CKNOqxq2MfW+GDiKnKRMdVKZlYqtkHtmnAHbdMDBoE72DO73m/79z7CI0HNzVcQsHfCkbXj2XxS3/yEztjkA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-5": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-5/-/node-8-5-3.1.54.tgz", + "integrity": "sha512-n0R+XHuv7rkCmZ+y156khHjrni1Ij/jqVU41MDsie/aCC+gRqBLM+BXOOJ5CSejkFd0pj83kZcFJHrqVy8I/nw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/progress": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-3.1.54.tgz", + "integrity": "sha512-JBxgmvy8zUHSl1l7DI5b0dFNp0ZhqdSRunPey6TLbKbM3wZ1Gh6XLYlu+hF/ZVOySnC5NKidg67W1IdgtRPZEg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.54" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/scopes": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/scopes/-/scopes-3.1.54.tgz", + "integrity": "sha512-H2nmmCtrStLyUnAhTr3gmvwcOIZR4hGgL09IVs0BkFMv3bwyAKBrrmf7tSsmf7++ACC8pGX2GkbzvVdlb5GkbA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/stream-compression": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/stream-compression/-/stream-compression-3.1.54.tgz", + "integrity": "sha512-+HDzZh+vzDqBNv4ggCfDz4qgigheNhRtNpsVLBI/NqxuPN3lCTvrWgswliPBZg5AA+iKkGGoaoj/IMOR+AFgeQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/util": "3.1.54" + } + }, + "node_modules/@php-wasm/universal": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-3.1.54.tgz", + "integrity": "sha512-ZHBzAFhLQJRXnF5rjd3DsbDnf2YZhlEQDf0g3M8mIylzYVX/NZk3en3v/hKA3vhaMVxg6KIr2URA2mzPKKboYg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.54", + "@php-wasm/progress": "3.1.54", + "@php-wasm/stream-compression": "3.1.54", + "@php-wasm/util": "3.1.54", + "ini": "4.1.2" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/util": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-3.1.54.tgz", + "integrity": "sha512-UdDiYnXDJ6g+mamRukQ4k0UoJBq9BQdaXpSemqfNAqX8CHfjuhW9T6iMJCCtLwY0aXvKc8a4qVoGQrrc+SQHow==", + "dev": true, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/web-service-worker": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/web-service-worker/-/web-service-worker-3.1.54.tgz", + "integrity": "sha512-3DQ6LLmVYiqC1gauyXhbLlns6NMo8pLKBGOf1hpKH+RzGA8pwAMyD0jvFPAo6C/62Xp1A//Cqt0+mYhE94Z6Hg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/scopes": "3.1.54", + "@php-wasm/universal": "3.1.54" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/xdebug-bridge": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@php-wasm/xdebug-bridge/-/xdebug-bridge-3.1.54.tgz", + "integrity": "sha512-Fm6LchCegBPAWrcom6iiWG3ZgmMO6D+ROJ7jpz2gF8W5AmQ7rMI5sKp8l5A2V0ObWabU+uTsuMB5lGr+e0XTNQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.54", + "@php-wasm/universal": "3.1.54", + "ws": "8.21.0", + "xml2js": "0.6.2", + "yargs": "17.7.2" + }, + "bin": { + "xdebug-bridge": "xdebug-bridge.js" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/xdebug-bridge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@php-wasm/xdebug-bridge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@php-wasm/xdebug-bridge/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.163", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.163.tgz", + "integrity": "sha512-+4zuoEB3S8RIhimtOFT7zAEk2SbpwrKjjGl9CyYnQR6k08uynxfauIIB0pDU1ssr05F8oyGiETwpn+8eXZMqPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", + "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@wordpress/env": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.39.0.tgz", + "integrity": "sha512-Hgl2RQAAzXFMqkpegGWT1/KkX88OVikRroPidWkij1WtU8p+AZniTcncWmlWqbdLdfGbPqQS5ZkqDZCzrQjgnA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@inquirer/prompts": "^7.2.0", + "@wp-playground/cli": "^3.0.0", + "chalk": "^4.0.0", + "copy-dir": "^1.3.0", + "cross-spawn": "^7.0.6", + "docker-compose": "^0.24.3", + "extract-zip": "^1.6.7", + "got": "^11.8.5", + "js-yaml": "^3.13.1", + "ora": "^4.0.2", + "rimraf": "^5.0.10", + "simple-git": "^3.5.0", + "terminal-link": "^2.0.0", + "yargs": "^17.3.0" + }, + "bin": { + "wp-env": "bin/wp-env" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wp-playground/blueprints": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-3.1.54.tgz", + "integrity": "sha512-lsFx6igM+QLEwdiCncGg6e9GwxcIubSk+iJ+h0xgeq+e/3V90ORVMDXNRE5SfNTA0gIyxNXINz8J+p/XR07N/w==", + "dev": true, + "dependencies": { + "@php-wasm/logger": "3.1.54", + "@php-wasm/progress": "3.1.54", + "@php-wasm/stream-compression": "3.1.54", + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54", + "@php-wasm/web-service-worker": "3.1.54", + "@wp-playground/common": "3.1.54", + "@wp-playground/storage": "3.1.54", + "@wp-playground/wordpress": "3.1.54", + "ajv": "8.18.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/cli": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/cli/-/cli-3.1.54.tgz", + "integrity": "sha512-XxwG1WlOdz7xxv8eM7FDOg3rEBwsMEtVAa7Z8UOZqzHMzbmFTH2eFnAu0Ri6rg24aasVPRE72kdeNWkyCvTAog==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/cli-util": "3.1.54", + "@php-wasm/logger": "3.1.54", + "@php-wasm/node": "3.1.54", + "@php-wasm/progress": "3.1.54", + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54", + "@php-wasm/xdebug-bridge": "3.1.54", + "@wp-playground/blueprints": "3.1.54", + "@wp-playground/common": "3.1.54", + "@wp-playground/storage": "3.1.54", + "@wp-playground/tools": "3.1.54", + "@wp-playground/wordpress": "3.1.54", + "express": "4.22.2", + "fs-extra": "11.1.1", + "tmp-promise": "3.0.3", + "wasm-feature-detect": "1.8.0", + "yargs": "17.7.2" + }, + "bin": { + "wp-playground-cli": "wp-playground.js" + } + }, + "node_modules/@wp-playground/cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wp-playground/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wp-playground/cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wp-playground/common": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/common/-/common-3.1.54.tgz", + "integrity": "sha512-Ev3s1664g9WK9v7jfS1IuiDQZyCSY7o4LlvEVy/85xlPNbwaAmJuIFBdX2amHWxXEv6CUWGsEBJFF94P/wne/g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/storage": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/storage/-/storage-3.1.54.tgz", + "integrity": "sha512-B5lMfaTGdJ2//aY6dg1F3F+Zn3F6Ilh3k7aHV6Vmx0gZJ+r7N0c/c7teoLT799gz8OGEcK+N0BLpsyELMQ6xoA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/stream-compression": "3.1.54", + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54", + "@zip.js/zip.js": "2.7.57", + "isomorphic-git": "1.37.6", + "octokit": "3.1.2", + "pako": "^1.0.10", + "sha.js": "2.4.12" + } + }, + "node_modules/@wp-playground/tools": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/tools/-/tools-3.1.54.tgz", + "integrity": "sha512-12g4WnTtrQc9d7/rRY0onBgCArLcoNbV4APWcwbTdrJBmrWc8kvOTYrs0g3lm9vTqm38JHDN8ZJgL3Y9PGtnCg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/util": "3.1.54", + "@wp-playground/blueprints": "3.1.54" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/wordpress": { + "version": "3.1.54", + "resolved": "https://registry.npmjs.org/@wp-playground/wordpress/-/wordpress-3.1.54.tgz", + "integrity": "sha512-a1McQQPimZBL+pDfsg/O2GrSC/y3yBMr/c5uXCEn5dYrseqJ2Tni2AzE1oEW8IxPX/gAq+9XRiLCm7sxRdyVYg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.54", + "@php-wasm/universal": "3.1.54", + "@php-wasm/util": "3.1.54", + "@wp-playground/common": "3.1.54", + "zstddec": "^0.2.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.57", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz", + "integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz", + "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/docker-compose": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", + "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-ext-extra-prebuilt": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fs-ext-extra-prebuilt/-/fs-ext-extra-prebuilt-2.2.7.tgz", + "integrity": "sha512-Q7rayYRBDIvDF01HWOwSSjoaP+05N1g+o3BXL1Zf8Frw2JkjSmi4EtvCBITuW30l6hB2m2TW1pehdh8wyU/+gw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.24.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-git": { + "version": "1.37.6", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.6.tgz", + "integrity": "sha512-qr1NFCPsVTZ6YGqTXw0CzamnsHyH9QQ1OTEfeXIweSljRUMzuHFCJdUn0wc6OcjtTDns6knxjPb7N6LmJeftOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/isomorphic-git/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/isomorphic-git/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nan": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz", + "integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/octokit": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", + "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zstddec": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0.tgz", + "integrity": "sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==", + "dev": true, + "license": "MIT AND BSD-3-Clause" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f46bd38 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "libresign-wp-customizations", + "private": true, + "license": "GPL-2.0-or-later", + "description": "Browser tests for the LibreSign WordPress customizations", + "scripts": { + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "env:start": "wp-env start", + "env:stop": "wp-env stop" + }, + "devDependencies": { + "@playwright/test": "^1.50.0", + "@wordpress/env": "^10.16.0" + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a55a2cd..4d195d8 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -4,6 +4,7 @@ . /vendor/* + /node_modules/* @@ -38,6 +39,9 @@ + + /tests/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6349570..13a94d2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,14 +1,24 @@ includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon parameters: level: 5 paths: - libresign-wp-customizations.php - includes + - src + - tests + excludePaths: + analyse: + - tests/E2E/support + analyseAndScan: + # Answers the WordPress functions the plugin reaches while it loads, + # so the unit suite runs without WordPress. Analysing it would take + # these stand-ins for the real functions everywhere else. + - tests/bootstrap-unit.php bootstrapFiles: - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php - ignoreErrors: - - - identifier: constant.notFound - message: '#^Constant (AUTH_KEY|SECURE_AUTH_SALT|NONCE_SALT) not found\.$#' + scanDirectories: + - vendor/wp-phpunit/wp-phpunit/includes diff --git a/phpunit-integration.xml.dist b/phpunit-integration.xml.dist new file mode 100644 index 0000000..f5dab11 --- /dev/null +++ b/phpunit-integration.xml.dist @@ -0,0 +1,22 @@ + + + + + tests/Integration + + + + + libresign-wp-customizations.php + includes + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c68846f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + tests/Unit + + + + + libresign-wp-customizations.php + includes + src + + + diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..86d1370 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test'; + +import { CUSTOMER_STATE } from './tests/E2E/support/site'; + +export default defineConfig( { + testDir: 'tests/E2E', + outputDir: 'tests/E2E/.results', + workers: 1, + fullyParallel: false, + forbidOnly: Boolean( process.env.CI ), + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI + ? [ + [ 'github' ], + [ + 'html', + { open: 'never', outputFolder: 'tests/E2E/.report' }, + ], + ] + : [ [ 'list' ] ], + use: { + baseURL: process.env.WP_BASE_URL || 'http://localhost', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'setup', + testMatch: /.*\.setup\.ts/, + use: { ...devices[ 'Desktop Chrome' ] }, + }, + { + name: 'customer', + dependencies: [ 'setup' ], + use: { + ...devices[ 'Desktop Chrome' ], + storageState: CUSTOMER_STATE, + }, + }, + ], +} ); diff --git a/src/Account/Navigation.php b/src/Account/Navigation.php new file mode 100644 index 0000000..c3b4fc3 --- /dev/null +++ b/src/Account/Navigation.php @@ -0,0 +1,118 @@ + + */ + public static function labels(): array { + return array( + 'subscriptions' => __( 'My subscription', 'libresign-wp-customizations' ), + 'orders' => __( 'Invoices', 'libresign-wp-customizations' ), + 'payment-methods' => __( 'Billing', 'libresign-wp-customizations' ), + ); + } + + /** + * Detail endpoints neither core nor Subscriptions highlights, mapped to the + * entry they belong to. + * + * @return array + */ + public static function aliases(): array { + return array( + 'subscriptions' => array( 'subscription-payment-method' ), + 'payment-methods' => array( 'edit-address' ), + ); + } + + /** + * @param array $items Navigation handed over by WooCommerce. + * @return array + */ + public static function filter_items( array $items ): array { + $labels = self::labels(); + $menu = array(); + + foreach ( self::order() as $endpoint ) { + if ( isset( $items[ $endpoint ] ) ) { + $menu[ $endpoint ] = isset( $labels[ $endpoint ] ) ? $labels[ $endpoint ] : $items[ $endpoint ]; + } + } + + return $menu; + } + + /** + * @param string[] $classes Classes of the entry. + * @param array $query_vars Query vars of the request being rendered. + * @return string[] + */ + public static function filter_item_classes( array $classes, string $endpoint, array $query_vars ): array { + if ( in_array( 'is-active', $classes, true ) ) { + return $classes; + } + + $aliases = self::aliases(); + + if ( empty( $aliases[ $endpoint ] ) ) { + return $classes; + } + + foreach ( $aliases[ $endpoint ] as $alias ) { + if ( isset( $query_vars[ $alias ] ) ) { + $classes[] = 'is-active'; + break; + } + } + + return $classes; + } + + /** + * @param array $query_vars Query vars of the request being rendered. + */ + public static function endpoint_title( string $title, string $endpoint, array $query_vars ): string { + $labels = self::labels(); + + if ( ! isset( $labels[ $endpoint ] ) ) { + return $title; + } + + $page_number = isset( $query_vars[ $endpoint ] ) ? intval( $query_vars[ $endpoint ] ) : 0; + + if ( $page_number < 2 ) { + return $labels[ $endpoint ]; + } + + return sprintf( + /* translators: 1: navigation label, 2: page number */ + __( '%1$s (page %2$d)', 'libresign-wp-customizations' ), + $labels[ $endpoint ], + $page_number + ); + } +} diff --git a/src/Account/RootEndpoint.php b/src/Account/RootEndpoint.php new file mode 100644 index 0000000..8722aab --- /dev/null +++ b/src/Account/RootEndpoint.php @@ -0,0 +1,43 @@ + $query_vars Endpoints registered by WooCommerce. + */ + public static function matches( string $request_path, array $query_vars ): bool { + $request_path = trim( $request_path, '/' ); + + if ( '' === $request_path ) { + return false; + } + + $segments = explode( '/', $request_path ); + $first_slug = reset( $segments ); + + if ( 'my-account' === $first_slug ) { + return true; + } + + foreach ( $query_vars as $query_var ) { + if ( ! empty( $query_var ) && $query_var === $first_slug ) { + return true; + } + } + + return false; + } +} diff --git a/src/Autoloader.php b/src/Autoloader.php new file mode 100644 index 0000000..15079b8 --- /dev/null +++ b/src/Autoloader.php @@ -0,0 +1,37 @@ +aqui'; + } + + public static function failure_message( int $code, string $api_message ): string { + return "Erro ao acionar deploy.
Código: {$code}.
Message: {$api_message}"; + } +} diff --git a/src/Github/SiteDeploy.php b/src/Github/SiteDeploy.php new file mode 100644 index 0000000..fe38c65 --- /dev/null +++ b/src/Github/SiteDeploy.php @@ -0,0 +1,31 @@ +repository = $repository; + $this->branch_name = $branch_name; + $this->workflow_name = $workflow_name; + } + + public function is_production_run( WorkflowRun $workflow_run ): bool { + return $this->repository === $workflow_run->repository() + && 'completed' === $workflow_run->action() + && 'success' === $workflow_run->conclusion() + && $this->branch_name === $workflow_run->head_branch() + && $this->workflow_name === $workflow_run->workflow_name(); + } +} diff --git a/src/Github/WebhookDecision.php b/src/Github/WebhookDecision.php new file mode 100644 index 0000000..60ef907 --- /dev/null +++ b/src/Github/WebhookDecision.php @@ -0,0 +1,112 @@ + + */ + private array $response_data = array(); + + private ?WorkflowRun $workflow_run = null; + + private function __construct( string $outcome ) { + $this->outcome = $outcome; + } + + public static function reject( string $error_code, string $error_message, int $http_status ): self { + $decision = new self( self::REJECT ); + $decision->error_code = $error_code; + $decision->error_message = $error_message; + $decision->http_status = $http_status; + + return $decision; + } + + public static function pong(): self { + return new self( self::PONG ); + } + + /** + * @param array $details Details worth reporting back. + */ + public static function ignore( string $ignored_reason, array $details = array() ): self { + $decision = new self( self::IGNORE ); + $decision->response_data = array_merge( array( 'reason' => $ignored_reason ), $details ); + + return $decision; + } + + public static function deploy( WorkflowRun $workflow_run ): self { + $decision = new self( self::DEPLOY ); + $decision->workflow_run = $workflow_run; + + return $decision; + } + + public function is_rejected(): bool { + return self::REJECT === $this->outcome; + } + + public function is_pong(): bool { + return self::PONG === $this->outcome; + } + + public function is_ignored(): bool { + return self::IGNORE === $this->outcome; + } + + public function is_deploy(): bool { + return self::DEPLOY === $this->outcome; + } + + public function code(): string { + return $this->error_code; + } + + public function message(): string { + return $this->error_message; + } + + public function status(): int { + return $this->http_status; + } + + /** + * @return array + */ + public function data(): array { + return $this->response_data; + } + + /** + * @throws LogicException When the decision is not a deploy. + */ + public function workflow_run(): WorkflowRun { + if ( null === $this->workflow_run ) { + throw new LogicException( 'Only a deploy decision carries a workflow run.' ); + } + + return $this->workflow_run; + } +} diff --git a/src/Github/WebhookGate.php b/src/Github/WebhookGate.php new file mode 100644 index 0000000..c7e067b --- /dev/null +++ b/src/Github/WebhookGate.php @@ -0,0 +1,85 @@ +secret = $secret; + $this->site_deploy = $site_deploy; + } + + public function decide( WebhookRequest $request ): WebhookDecision { + if ( '' === trim( $this->secret ) ) { + return WebhookDecision::reject( + 'libresign_github_webhook_secret_missing', + __( 'The GitHub webhook secret is not configured.', 'libresign-wp-customizations' ), + 503 + ); + } + + if ( ! $request->is_from_github() ) { + return WebhookDecision::reject( + 'libresign_github_webhook_invalid_agent', + __( 'The webhook request does not look like a GitHub delivery.', 'libresign-wp-customizations' ), + 403 + ); + } + + if ( ! $request->has_valid_signature( $this->secret ) ) { + return WebhookDecision::reject( + 'libresign_github_webhook_invalid_signature', + __( 'Invalid GitHub webhook signature.', 'libresign-wp-customizations' ), + 403 + ); + } + + if ( 'ping' === $request->event() ) { + return WebhookDecision::pong(); + } + + if ( 'workflow_run' !== $request->event() ) { + return WebhookDecision::ignore( 'unsupported_event', array( 'event' => $request->event() ) ); + } + + $payload = $request->payload(); + + if ( null === $payload ) { + return WebhookDecision::reject( + 'libresign_github_webhook_invalid_payload', + __( 'The GitHub webhook payload must be valid JSON.', 'libresign-wp-customizations' ), + 400 + ); + } + + $workflow_run = WorkflowRun::from_payload( $payload ); + + if ( ! $this->site_deploy->is_production_run( $workflow_run ) ) { + return WebhookDecision::ignore( + 'not_production_deploy', + array( + 'repository' => $workflow_run->repository(), + 'workflow_name' => $workflow_run->workflow_name(), + 'head_branch' => $workflow_run->head_branch(), + 'conclusion' => $workflow_run->conclusion(), + ) + ); + } + + return WebhookDecision::deploy( $workflow_run ); + } +} diff --git a/src/Github/WebhookRequest.php b/src/Github/WebhookRequest.php new file mode 100644 index 0000000..bcf4f46 --- /dev/null +++ b/src/Github/WebhookRequest.php @@ -0,0 +1,58 @@ +user_agent = $user_agent; + $this->event = $event; + $this->signature = $signature; + $this->delivery_id = $delivery_id; + $this->body = $body; + } + + public function is_from_github(): bool { + return 0 === strpos( trim( $this->user_agent ), 'GitHub-Hookshot/' ); + } + + public function has_valid_signature( string $secret ): bool { + return WebhookSignature::matches( $this->body, $this->signature, $secret ); + } + + public function event(): string { + return strtolower( trim( $this->event ) ); + } + + public function delivery_id(): string { + return $this->delivery_id; + } + + /** + * @return array|null + */ + public function payload(): ?array { + $payload = json_decode( $this->body, true ); + + return is_array( $payload ) ? $payload : null; + } +} diff --git a/src/Github/WebhookSignature.php b/src/Github/WebhookSignature.php new file mode 100644 index 0000000..02a73b4 --- /dev/null +++ b/src/Github/WebhookSignature.php @@ -0,0 +1,32 @@ + + */ + private array $payload; + + /** + * @param array $payload Parsed payload. + */ + private function __construct( array $payload ) { + $this->payload = $payload; + } + + /** + * @param array $payload Parsed payload. + */ + public static function from_payload( array $payload ): self { + return new self( $payload ); + } + + public function repository(): string { + return $this->text( array( 'repository', 'full_name' ) ); + } + + public function action(): string { + return $this->text( array( 'action' ) ); + } + + public function conclusion(): string { + return $this->text( array( 'workflow_run', 'conclusion' ) ); + } + + public function head_branch(): string { + return $this->text( array( 'workflow_run', 'head_branch' ) ); + } + + public function head_sha(): string { + return $this->text( array( 'workflow_run', 'head_sha' ) ); + } + + public function html_url(): string { + return $this->text( array( 'workflow_run', 'html_url' ) ); + } + + public function updated_at(): string { + return $this->text( array( 'workflow_run', 'updated_at' ) ); + } + + public function workflow_name(): string { + $run_name = $this->text( array( 'workflow_run', 'name' ) ); + + return '' === $run_name ? $this->text( array( 'workflow', 'name' ) ) : $run_name; + } + + /** + * @param string[] $field_path Keys to walk. + */ + private function text( array $field_path ): string { + $field_value = $this->payload; + + foreach ( $field_path as $field_name ) { + if ( ! is_array( $field_value ) || ! isset( $field_value[ $field_name ] ) ) { + return ''; + } + + $field_value = $field_value[ $field_name ]; + } + + return is_scalar( $field_value ) ? trim( (string) $field_value ) : ''; + } +} diff --git a/src/Settings/Secret.php b/src/Settings/Secret.php new file mode 100644 index 0000000..9e4847c --- /dev/null +++ b/src/Settings/Secret.php @@ -0,0 +1,94 @@ +encryption_key = $encryption_key; + $this->initialization_vector = $initialization_vector; + } + + public static function from_salts( string $auth_key, string $secure_auth_salt, string $nonce_salt ): self { + return new self( + hash( 'sha256', $auth_key . $secure_auth_salt ), + substr( hash( 'sha256', $nonce_salt ), 0, 16 ) + ); + } + + /** + * An already encrypted value is left as it is: WordPress runs the sanitize + * callback of an option again when update_option() hands a value that does + * not exist yet to add_option(), and encrypting twice stores a value that + * nothing can read back. + */ + public function encrypt( string $plain_value ): string { + $plain_value = trim( $plain_value ); + + if ( $this->decrypt( $plain_value ) !== $plain_value ) { + return $plain_value; + } + + return base64_encode( + (string) openssl_encrypt( $plain_value, self::CIPHER, $this->encryption_key, 0, $this->initialization_vector ) + ); + } + + /** + * Read a stored value, returning it untouched when it cannot be deciphered, + * so a value saved in plain text keeps working. Only for a value that stays + * on the site: decrypt_or_discard() is the one for a value that is sent. + */ + public function decrypt( string $stored_value ): string { + $decoded_value = $this->decode( $stored_value ); + + if ( false === $decoded_value ) { + return trim( $stored_value ); + } + + return $this->decipher( $decoded_value ) ?? trim( $stored_value ); + } + + /** + * A value that is not base64 was never encrypted and is used as it is. A + * value that is base64 but does not decipher was encrypted with salts this + * installation no longer has: it is not the value and nobody outside may + * see it, so it is dropped instead of being sent. + */ + public function decrypt_or_discard( string $stored_value ): string { + $decoded_value = $this->decode( $stored_value ); + + if ( false === $decoded_value ) { + return trim( $stored_value ); + } + + return (string) $this->decipher( $decoded_value ); + } + + /** + * @return string|false + */ + private function decode( string $stored_value ) { + $stored_value = trim( $stored_value ); + + return '' === $stored_value ? false : base64_decode( $stored_value, true ); + } + + private function decipher( string $decoded_value ): ?string { + $decrypted_value = openssl_decrypt( $decoded_value, self::CIPHER, $this->encryption_key, 0, $this->initialization_vector ); + + return false === $decrypted_value ? null : trim( $decrypted_value ); + } +} diff --git a/src/Subscription/StatusChange.php b/src/Subscription/StatusChange.php new file mode 100644 index 0000000..f7d259e --- /dev/null +++ b/src/Subscription/StatusChange.php @@ -0,0 +1,33 @@ + array( + 'question' => __( 'Are you sure you want to cancel your subscription? This action cannot be undone.', 'libresign-wp-customizations' ), + 'confirm' => __( 'Yes, cancel subscription', 'libresign-wp-customizations' ), + 'dismiss' => __( 'No, keep subscription', 'libresign-wp-customizations' ), + ), + 'active' => array( + 'question' => __( 'Are you sure you want to reactivate your subscription?', 'libresign-wp-customizations' ), + 'confirm' => __( 'Yes, reactivate subscription', 'libresign-wp-customizations' ), + 'dismiss' => __( 'No, go back', 'libresign-wp-customizations' ), + ), + ); + + return $strings[ $new_status ] ?? null; + } +} diff --git a/tests/E2E/Account/Navigation.spec.ts b/tests/E2E/Account/Navigation.spec.ts new file mode 100644 index 0000000..47b085c --- /dev/null +++ b/tests/E2E/Account/Navigation.spec.ts @@ -0,0 +1,103 @@ +import { expect, test } from '@playwright/test'; + +import { seededSite } from '../support/site'; + +const ENTRY = '.woocommerce-MyAccount-navigation-link'; + +test.describe( 'My Account navigation', () => { + test( 'keeps five entries, in the order the plugin sets', async ( { + page, + } ) => { + await page.goto( '/' ); + + const entries = page.locator( ENTRY ); + await expect( entries ).toHaveCount( 5 ); + + const order = [ + 'subscriptions', + 'orders', + 'payment-methods', + 'edit-account', + 'customer-logout', + ]; + + for ( const [ position, endpoint ] of order.entries() ) { + await expect( entries.nth( position ) ).toHaveClass( + new RegExp( `${ ENTRY.slice( 1 ) }--${ endpoint }(\\s|$)` ) + ); + } + } ); + + test( 'renames the three entries it relabels', async ( { page } ) => { + await page.goto( '/' ); + + await expect( page.locator( `${ ENTRY }--subscriptions a` ) ).toHaveText( + 'My subscription' + ); + await expect( page.locator( `${ ENTRY }--orders a` ) ).toHaveText( + 'Invoices' + ); + await expect( + page.locator( `${ ENTRY }--payment-methods a` ) + ).toHaveText( 'Billing' ); + } ); + + test( 'titles a screen with the label its entry carries', async ( { + page, + } ) => { + await page.goto( '/orders/' ); + + await expect( + page.getByRole( 'heading', { name: 'Invoices' } ) + ).toBeVisible(); + } ); + + test( 'shows the payment methods and the addresses on the billing screen', async ( { + page, + } ) => { + await page.goto( '/payment-methods/' ); + + await expect( + page.locator( '.libresign-account-section-title' ) + ).toHaveText( [ 'Payment methods', 'Addresses' ] ); + + await expect( + page.getByText( + 'These addresses are used on your invoices and subscription charges.' + ) + ).toBeVisible(); + } ); + + test( 'sends the standalone addresses list to the billing screen', async ( { + page, + } ) => { + await page.goto( '/edit-address/' ); + + await expect( page ).toHaveURL( '/payment-methods/' ); + } ); + + test( 'keeps billing highlighted while an address is being edited', async ( { + page, + } ) => { + await page.goto( '/edit-address/billing/' ); + + await expect( + page.locator( `${ ENTRY }--payment-methods` ) + ).toHaveClass( /is-active/ ); + } ); + + test( 'points the account screens at the signature system', async ( { + page, + } ) => { + await page.goto( '/' ); + + const callToAction = page.locator( '.libresign-nextcloud-account-cta' ); + + await expect( callToAction ).toBeVisible(); + await expect( + callToAction.getByRole( 'link', { + name: 'Ir para o sistema de assinaturas', + } ) + ).toHaveAttribute( 'href', seededSite().nextcloudHost ); + } ); +} ); diff --git a/tests/E2E/Account/RootEndpoint.spec.ts b/tests/E2E/Account/RootEndpoint.spec.ts new file mode 100644 index 0000000..82e2445 --- /dev/null +++ b/tests/E2E/Account/RootEndpoint.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; + +test.describe( 'Account screens served from the site root', () => { + test( 'serves the invoices screen at /orders/', async ( { page } ) => { + await page.goto( '/orders/' ); + + await expect( page ).toHaveURL( '/orders/' ); + await expect( + page.locator( '.woocommerce-MyAccount-navigation-link--orders' ) + ).toHaveClass( /is-active/ ); + } ); + + test( 'serves the billing screen at /payment-methods/', async ( { + page, + } ) => { + await page.goto( '/payment-methods/' ); + + await expect( page ).toHaveURL( '/payment-methods/' ); + await expect( + page.locator( + '.woocommerce-MyAccount-navigation-link--payment-methods' + ) + ).toHaveClass( /is-active/ ); + } ); + + test( 'keeps the /my-account/ permalink working as an alias', async ( { + page, + } ) => { + await page.goto( '/my-account/' ); + + await expect( page ).toHaveURL( '/my-account/' ); + await expect( + page.locator( '.woocommerce-MyAccount-navigation' ) + ).toBeVisible(); + } ); + + test.describe( 'signed out', () => { + test.use( { storageState: { cookies: [], origins: [] } } ); + + test( 'serves the lost password screen at /lost-password/', async ( { + page, + } ) => { + await page.goto( '/lost-password/' ); + + await expect( page ).toHaveURL( '/lost-password/' ); + await expect( + page.locator( 'form.lost_reset_password' ) + ).toBeVisible(); + } ); + } ); +} ); diff --git a/tests/E2E/Subscription/StatusChange.spec.ts b/tests/E2E/Subscription/StatusChange.spec.ts new file mode 100644 index 0000000..4068269 --- /dev/null +++ b/tests/E2E/Subscription/StatusChange.spec.ts @@ -0,0 +1,63 @@ +import { expect, Page, test } from '@playwright/test'; + +import { SeededSite, seedSite, subscriptionStatus } from '../support/site'; + +const CANCELLATION_QUESTION = + 'Are you sure you want to cancel your subscription? This action cannot be undone.'; + +const cancelAction = ( page: Page ) => page.locator( 'a.cancel' ); + +test.describe( 'Confirming a subscription status change', () => { + let site: SeededSite; + + test.beforeEach( () => { + site = seedSite(); + } ); + + test( 'asks before cancelling instead of cancelling right away', async ( { + page, + } ) => { + await page.goto( `/view-subscription/${ site.subscriptionId }/` ); + await cancelAction( page ).click(); + + await expect( page.getByText( CANCELLATION_QUESTION ) ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: 'Yes, cancel subscription' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: 'No, keep subscription' } ) + ).toBeVisible(); + + expect( subscriptionStatus( site.subscriptionId ) ).toBe( 'active' ); + } ); + + test( 'leaves the subscription active when the customer backs out', async ( { + page, + } ) => { + await page.goto( `/view-subscription/${ site.subscriptionId }/` ); + await cancelAction( page ).click(); + await page + .getByRole( 'link', { name: 'No, keep subscription' } ) + .click(); + + await expect( page.getByText( CANCELLATION_QUESTION ) ).toBeHidden(); + + expect( subscriptionStatus( site.subscriptionId ) ).toBe( 'active' ); + } ); + + test( 'stops the renewals once the customer confirms', async ( { + page, + } ) => { + await page.goto( `/view-subscription/${ site.subscriptionId }/` ); + await cancelAction( page ).click(); + await page + .getByRole( 'link', { name: 'Yes, cancel subscription' } ) + .click(); + + await expect( cancelAction( page ) ).toBeHidden(); + + expect( subscriptionStatus( site.subscriptionId ) ).toBe( + 'pending-cancel' + ); + } ); +} ); diff --git a/tests/E2E/customer.setup.ts b/tests/E2E/customer.setup.ts new file mode 100644 index 0000000..762b3ed --- /dev/null +++ b/tests/E2E/customer.setup.ts @@ -0,0 +1,18 @@ +import { expect, test as setup } from '@playwright/test'; + +import { CUSTOMER_PASSWORD, CUSTOMER_STATE, seedSite } from './support/site'; + +setup( 'seed the site and sign the customer in', async ( { page } ) => { + const site = seedSite(); + + await page.goto( '/wp-login.php?redirect_to=%2F' ); + await page.locator( '#user_login' ).fill( site.customerLogin ); + await page.locator( '#user_pass' ).fill( CUSTOMER_PASSWORD ); + await page.locator( '#wp-submit' ).click(); + + await expect( + page.locator( '.woocommerce-MyAccount-navigation' ) + ).toBeVisible(); + + await page.context().storageState( { path: CUSTOMER_STATE } ); +} ); diff --git a/tests/E2E/support/mu-plugins/saved-payment-methods-gateway.php b/tests/E2E/support/mu-plugins/saved-payment-methods-gateway.php new file mode 100644 index 0000000..9ea7311 --- /dev/null +++ b/tests/E2E/support/mu-plugins/saved-payment-methods-gateway.php @@ -0,0 +1,30 @@ +id = 'libresign_e2e_saved_payment_methods'; + $this->method_title = 'LibreSign E2E saved payment methods'; + $this->title = 'LibreSign E2E saved payment methods'; + $this->has_fields = false; + $this->enabled = 'yes'; + $this->supports = array( 'products', 'tokenization', 'add_payment_method' ); + } + } + + add_filter( 'woocommerce_payment_gateways', 'libresign_e2e_add_saved_payment_methods_gateway' ); +} +add_action( 'plugins_loaded', 'libresign_e2e_define_saved_payment_methods_gateway' ); diff --git a/tests/E2E/support/seed.php b/tests/E2E/support/seed.php new file mode 100644 index 0000000..5d00cef --- /dev/null +++ b/tests/E2E/support/seed.php @@ -0,0 +1,140 @@ +' ); +} + +if ( ! function_exists( 'wcs_create_subscription' ) ) { + WP_CLI::error( 'WooCommerce Subscriptions is not active on the site under test.' ); +} + +$site_host = (string) wp_parse_url( (string) home_url(), PHP_URL_HOST ); +$site_is_local = in_array( $site_host, array( 'localhost', '127.0.0.1', '::1' ), true ); +$any_site_allowed = isset( $args[1] ) && 'any-site' === $args[1]; + +if ( ! $site_is_local && ! $any_site_allowed ) { + WP_CLI::error( + sprintf( + 'The seed makes My Account the front page of %s, rebuilds its rewrite rules and creates a customer with a password this repository publishes. Set WP_E2E_ALLOW_ANY_SITE=1 to run it outside localhost.', + $site_host + ) + ); +} + +$customer_login = 'libresign_e2e_customer'; +$customer_email = 'e2e-customer@libresign.test'; +$product_sku = 'LIBRESIGN-E2E-PLAN'; + +if ( '' === (string) get_option( 'permalink_structure' ) ) { + update_option( 'permalink_structure', '/%postname%/' ); +} + +$account_page_id = (int) wc_get_page_id( 'myaccount' ); + +if ( $account_page_id <= 0 ) { + WP_CLI::error( 'WooCommerce has no My Account page; install the WooCommerce pages first.' ); +} + +update_option( 'show_on_front', 'page' ); +update_option( 'page_on_front', $account_page_id ); + +delete_option( 'libresign_root_my_account_rewrite_version' ); + +if ( '' === libresign_get_nextcloud_public_host() ) { + update_option( 'nextcloud_public_host', 'https://nextcloud.libresign.test' ); +} + +$customer = get_user_by( 'login', $customer_login ); + +if ( $customer instanceof WP_User ) { + $customer_id = $customer->ID; + $password_already_matches = wp_check_password( $customer_password, $customer->user_pass, $customer_id ); + + if ( ! $password_already_matches ) { + wp_set_password( $customer_password, $customer_id ); + } +} else { + $customer_id = wp_insert_user( + array( + 'user_login' => $customer_login, + 'user_email' => $customer_email, + 'user_pass' => $customer_password, + 'first_name' => 'Ana', + 'last_name' => 'Lima', + 'role' => 'customer', + ) + ); + + if ( is_wp_error( $customer_id ) ) { + WP_CLI::error( $customer_id->get_error_message() ); + } + + $customer_id = (int) $customer_id; +} + +update_user_meta( $customer_id, 'billing_first_name', 'Ana' ); +update_user_meta( $customer_id, 'billing_last_name', 'Lima' ); +update_user_meta( $customer_id, 'billing_email', $customer_email ); +update_user_meta( $customer_id, 'billing_country', 'BR' ); + +$product_id = (int) wc_get_product_id_by_sku( $product_sku ); + +if ( $product_id <= 0 ) { + $new_product = new WC_Product_Subscription(); + $new_product->set_name( 'LibreSign E2E plan' ); + $new_product->set_slug( 'libresign-e2e-plan' ); + $new_product->set_sku( $product_sku ); + $new_product->set_status( 'publish' ); + $new_product->set_catalog_visibility( 'hidden' ); + $new_product->set_regular_price( '10' ); + $new_product->update_meta_data( '_subscription_price', '10' ); + $new_product->update_meta_data( '_subscription_period', 'month' ); + $new_product->update_meta_data( '_subscription_period_interval', '1' ); + $new_product->update_meta_data( '_subscription_length', '0' ); + $product_id = (int) $new_product->save(); +} + +foreach ( wcs_get_users_subscriptions( $customer_id ) as $subscription_of_a_previous_run ) { + $subscription_of_a_previous_run->delete( true ); +} + +$subscription = wcs_create_subscription( + array( + 'customer_id' => $customer_id, + 'status' => 'pending', + 'billing_period' => 'month', + 'billing_interval' => 1, + 'start_date' => gmdate( 'Y-m-d H:i:s', time() - MONTH_IN_SECONDS ), + ) +); + +if ( is_wp_error( $subscription ) ) { + WP_CLI::error( $subscription->get_error_message() ); +} + +$subscription->set_requires_manual_renewal( true ); +$subscription->set_billing_first_name( 'Ana' ); +$subscription->set_billing_last_name( 'Lima' ); +$subscription->set_billing_email( $customer_email ); +$subscription->add_product( wc_get_product( $product_id ), 1 ); +$subscription->calculate_totals(); +$subscription->save(); +$subscription->update_status( 'active' ); + +WP_CLI::line( + (string) wp_json_encode( + array( + 'customerId' => $customer_id, + 'customerLogin' => $customer_login, + 'customerEmail' => $customer_email, + 'productId' => $product_id, + 'subscriptionId' => $subscription->get_id(), + 'accountPageId' => $account_page_id, + 'nextcloudHost' => libresign_get_nextcloud_public_host(), + ) + ) +); diff --git a/tests/E2E/support/site.ts b/tests/E2E/support/site.ts new file mode 100644 index 0000000..fadbb5e --- /dev/null +++ b/tests/E2E/support/site.ts @@ -0,0 +1,70 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +import { runWpCli } from './wp-cli'; + +const SEED_SCRIPT = join( __dirname, 'seed.php' ); +const STATE_DIR = join( __dirname, '..', '.state' ); +const SITE_STATE = join( STATE_DIR, 'site.json' ); + +export const CUSTOMER_STATE = join( STATE_DIR, 'customer.json' ); + +export const CUSTOMER_PASSWORD = + process.env.WP_E2E_CUSTOMER_PASSWORD || 'libresign-e2e'; + +const SEED_TARGET = + process.env.WP_E2E_ALLOW_ANY_SITE === '1' ? 'any-site' : 'local-site-only'; + +export interface SeededSite { + customerId: number; + customerLogin: string; + customerEmail: string; + productId: number; + subscriptionId: number; + accountPageId: number; + nextcloudHost: string; +} + +export function seedSite(): SeededSite { + const output = runWpCli( + [ 'eval-file', '-', CUSTOMER_PASSWORD, SEED_TARGET ], + readFileSync( SEED_SCRIPT, 'utf8' ) + ); + + runWpCli( [ 'rewrite', 'flush', '--hard' ] ); + + const description = output + .split( '\n' ) + .reverse() + .find( ( line ) => line.startsWith( '{' ) ); + + if ( ! description ) { + throw new Error( `The seed printed no site description:\n${ output }` ); + } + + const site = JSON.parse( description ) as SeededSite; + + mkdirSync( dirname( SITE_STATE ), { recursive: true } ); + writeFileSync( SITE_STATE, JSON.stringify( site, null, '\t' ) ); + + return site; +} + +export function seededSite(): SeededSite { + return JSON.parse( readFileSync( SITE_STATE, 'utf8' ) ) as SeededSite; +} + +export function subscriptionStatus( subscriptionId: number ): string { + const output = runWpCli( [ + 'eval', + `echo 'status:' . wcs_get_subscription( ${ subscriptionId } )->get_status();`, + ] ); + + const status = output.match( /status:(\S+)/ ); + + if ( ! status ) { + throw new Error( `WP-CLI reported no status:\n${ output }` ); + } + + return status[ 1 ]; +} diff --git a/tests/E2E/support/wp-cli.ts b/tests/E2E/support/wp-cli.ts new file mode 100644 index 0000000..94c69fc --- /dev/null +++ b/tests/E2E/support/wp-cli.ts @@ -0,0 +1,16 @@ +import { execFileSync } from 'node:child_process'; + +const WP_CLI_ON_THE_LOCAL_STACK = + 'docker exec -i -u www-data wordpress-docker-wordpress-1 wp --path=/var/www/html'; + +export function runWpCli( args: readonly string[], stdin = '' ): string { + const [ command, ...prefix ] = ( + process.env.WP_CLI || WP_CLI_ON_THE_LOCAL_STACK + ).split( /\s+/ ); + + return execFileSync( command, [ ...prefix, ...args ], { + input: stdin, + encoding: 'utf8', + maxBuffer: 16 * 1024 * 1024, + } ); +} diff --git a/tests/Integration/Includes/AccountNavigationTest.php b/tests/Integration/Includes/AccountNavigationTest.php new file mode 100644 index 0000000..02911e6 --- /dev/null +++ b/tests/Integration/Includes/AccountNavigationTest.php @@ -0,0 +1,143 @@ +query_vars = array(); + + parent::tear_down(); + } + + /** + * Pretend the request currently being rendered has these query vars. + * + * @param array $query_vars Query vars. + * @return void + */ + private function render_request_with( $query_vars ) { + global $wp; + $wp->query_vars = $query_vars; + } + + /** + * @dataProvider provide_hooks + * + * @param string $hook Hook name. + * @param string $callback Callback name. + * @param int $priority Expected priority. + */ + public function test_the_navigation_callbacks_are_hooked( $hook, $callback, $priority ) { + $this->assertSame( $priority, has_filter( $hook, $callback ) ); + } + + /** + * @return iterable + */ + public static function provide_hooks() { + yield 'the navigation is filtered' => array( + 'woocommerce_account_menu_items', + 'libresign_filter_account_menu_items', + 20, + ); + yield 'the entry classes are filtered' => array( + 'woocommerce_account_menu_item_classes', + 'libresign_filter_account_menu_item_classes', + 10, + ); + yield 'the endpoint titles are registered on init' => array( + 'init', + 'libresign_register_account_endpoint_titles', + 10, + ); + yield 'the payment methods heading is rendered' => array( + 'woocommerce_before_account_payment_methods', + 'libresign_render_payment_methods_section_title', + 5, + ); + yield 'the addresses are rendered on the billing screen' => array( + 'woocommerce_account_payment-methods_endpoint', + 'libresign_render_addresses_on_payment_methods', + 20, + ); + yield 'the addresses list is redirected' => array( + 'template_redirect', + 'libresign_redirect_addresses_to_billing', + 10, + ); + yield 'the addresses description is replaced' => array( + 'woocommerce_my_account_my_address_description', + 'libresign_filter_my_address_description', + 10, + ); + } + + public function test_the_endpoint_title_filters_are_registered_for_the_renamed_entries() { + $this->assertSame( 20, has_filter( 'woocommerce_endpoint_subscriptions_title', 'libresign_filter_account_endpoint_title' ) ); + $this->assertSame( 20, has_filter( 'woocommerce_endpoint_orders_title', 'libresign_filter_account_endpoint_title' ) ); + $this->assertSame( 20, has_filter( 'woocommerce_endpoint_payment-methods_title', 'libresign_filter_account_endpoint_title' ) ); + $this->assertSame( 20, has_filter( 'woocommerce_endpoint_view-order_title', 'libresign_filter_view_order_endpoint_title' ) ); + } + + public function test_the_endpoint_title_is_built_from_the_request_being_rendered() { + $this->render_request_with( array( 'orders' => '3' ) ); + + $this->assertSame( 'Invoices (page 3)', libresign_filter_account_endpoint_title( 'Orders', 'orders' ) ); + } + + public function test_the_highlighted_entry_is_built_from_the_request_being_rendered() { + $this->render_request_with( array( 'edit-address' => 'billing' ) ); + + $this->assertContains( + 'is-active', + libresign_filter_account_menu_item_classes( array( 'payment-methods' ), 'payment-methods' ) + ); + } + + public function test_the_navigation_is_reordered_when_woocommerce_filters_it() { + $this->assertSame( + array( 'orders', 'customer-logout' ), + array_keys( + libresign_filter_account_menu_items( + array( + 'dashboard' => 'Dashboard', + 'orders' => 'Orders', + 'customer-logout' => 'Log out', + ) + ) + ) + ); + } + + public function test_the_addresses_are_described_in_terms_of_the_subscription() { + $description = apply_filters( 'woocommerce_my_account_my_address_description', 'These addresses will be used on the checkout page by default.' ); + + $this->assertStringContainsString( 'invoices', $description ); + $this->assertStringNotContainsString( 'checkout', $description ); + } + + public function test_a_section_title_is_escaped() { + ob_start(); + libresign_render_account_section_title( 'Payment methods' ); + $html = ob_get_clean(); + + $this->assertSame( + '', + $html + ); + } +} diff --git a/tests/Integration/Includes/GithubSiteWebhookTest.php b/tests/Integration/Includes/GithubSiteWebhookTest.php new file mode 100644 index 0000000..470b88a --- /dev/null +++ b/tests/Integration/Includes/GithubSiteWebhookTest.php @@ -0,0 +1,387 @@ +origin = new FakeHttp(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + + update_option( 'libresign_github_webhook_secret', self::SECRET ); + update_option( 'libresign_github_deploy_organization_repository', 'LibreSign/site' ); + update_option( 'libresign_site_deploy_workflow_name', 'pages build and deployment' ); + update_option( 'libresign_site_deploy_branch_name', 'gh-pages' ); + update_option( 'libresign_site_origin', 'https://libresign.coop' ); + } + + public function tear_down() { + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Payload of a production deploy run. + * + * @param array $overrides Values replacing the defaults. + * @return array + */ + private static function production_payload( $overrides = array() ) { + return array_replace_recursive( + array( + 'action' => 'completed', + 'repository' => array( 'full_name' => 'LibreSign/site' ), + 'workflow_run' => array( + 'name' => 'pages build and deployment', + 'conclusion' => 'success', + 'head_branch' => 'gh-pages', + 'head_sha' => '0f1e2d3', + 'html_url' => 'https://github.com/LibreSign/site/actions/runs/1', + 'updated_at' => '2026-09-14T12:00:00Z', + ), + ), + $overrides + ); + } + + /** + * Build a request signed with the configured secret. + * + * @param string $event X-GitHub-Event header. + * @param string $body Raw request body. + * @param string $delivery_id X-GitHub-Delivery header. + * @return WP_REST_Request + */ + private function signed_request( $event, $body, $delivery_id = 'delivery-1' ) { + $request = new WP_REST_Request( 'POST', self::ROUTE ); + $request->set_header( 'user-agent', 'GitHub-Hookshot/044aadd' ); + $request->set_header( 'x-github-event', $event ); + $request->set_header( 'x-github-delivery', $delivery_id ); + $request->set_header( 'content-type', 'application/json' ); + $request->set_header( 'x-hub-signature-256', 'sha256=' . hash_hmac( 'sha256', $body, self::SECRET ) ); + $request->set_body( $body ); + + return $request; + } + + public function test_the_route_is_registered() { + $this->assertArrayHasKey( self::ROUTE, rest_get_server()->get_routes() ); + } + + public function test_the_endpoint_url_points_at_the_route() { + $this->assertSame( rest_url( 'libresign/v1/site-deploy-webhook' ), libresign_github_site_webhook_endpoint_url() ); + } + + public function test_an_unconfigured_secret_makes_the_endpoint_unavailable() { + delete_option( 'libresign_github_webhook_secret' ); + + $response = rest_get_server()->dispatch( $this->signed_request( 'ping', '{}' ) ); + + $this->assertSame( 503, $response->get_status() ); + $this->assertSame( 'libresign_github_webhook_secret_missing', $response->get_data()['code'] ); + } + + public function test_a_request_from_another_client_is_rejected() { + $request = $this->signed_request( 'ping', '{}' ); + $request->set_header( 'user-agent', 'curl/8.7.1' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 403, $response->get_status() ); + $this->assertSame( 'libresign_github_webhook_invalid_agent', $response->get_data()['code'] ); + } + + public function test_a_wrongly_signed_request_is_rejected() { + $request = $this->signed_request( 'ping', '{}' ); + $request->set_header( 'x-hub-signature-256', 'sha256=' . hash_hmac( 'sha256', '{}', 'another-secret' ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 403, $response->get_status() ); + $this->assertSame( 'libresign_github_webhook_invalid_signature', $response->get_data()['code'] ); + } + + public function test_a_ping_is_answered_with_the_endpoint_url() { + $response = rest_get_server()->dispatch( $this->signed_request( 'ping', '{"zen":"Design for failure."}' ) ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'pong', $response->get_data()['status'] ); + $this->assertSame( libresign_github_site_webhook_endpoint_url(), $response->get_data()['endpoint'] ); + } + + public function test_another_event_is_ignored() { + $response = rest_get_server()->dispatch( $this->signed_request( 'push', '{"ref":"refs/heads/main"}' ) ); + + $this->assertSame( 202, $response->get_status() ); + $this->assertSame( 'ignored', $response->get_data()['status'] ); + $this->assertSame( 'unsupported_event', $response->get_data()['reason'] ); + $this->assertSame( 'push', $response->get_data()['event'] ); + } + + public function test_a_json_body_that_does_not_parse_never_reaches_the_callback() { + $response = rest_get_server()->dispatch( $this->signed_request( 'workflow_run', 'not json' ) ); + + $this->assertSame( 400, $response->get_status() ); + $this->assertSame( 'rest_invalid_json', $response->get_data()['code'] ); + } + + public function test_a_body_that_is_not_json_is_rejected_by_the_callback() { + $request = $this->signed_request( 'workflow_run', 'not json' ); + $request->set_header( 'content-type', 'text/plain' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 400, $response->get_status() ); + $this->assertSame( 'libresign_github_webhook_invalid_payload', $response->get_data()['code'] ); + } + + /** + * @dataProvider provide_non_production_payloads + * + * @param array $overrides Values replacing the production defaults. + */ + public function test_a_run_that_is_not_the_production_deploy_is_ignored( $overrides ) { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $body = wp_json_encode( self::production_payload( $overrides ) ); + + $response = rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body ) ); + + $this->assertSame( 202, $response->get_status() ); + $this->assertSame( 'not_production_deploy', $response->get_data()['reason'] ); + $this->assertSame( array(), $this->origin->urls() ); + } + + /** + * @return iterable}> + */ + public static function provide_non_production_payloads() { + yield 'another repository' => array( array( 'repository' => array( 'full_name' => 'LibreSign/libresign' ) ) ); + yield 'the run is starting' => array( array( 'action' => 'requested' ) ); + yield 'the run failed' => array( array( 'workflow_run' => array( 'conclusion' => 'failure' ) ) ); + yield 'another branch' => array( array( 'workflow_run' => array( 'head_branch' => 'main' ) ) ); + yield 'another workflow' => array( array( 'workflow_run' => array( 'name' => 'tests' ) ) ); + } + + public function test_a_production_deploy_synchronizes_the_fragments() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $body = wp_json_encode( self::production_payload() ); + $response = rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body ) ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'synced', $data['status'] ); + $this->assertSame( array( 'header', 'footer' ), $data['synced'] ); + $this->assertSame( 'https://libresign.coop', $data['origin'] ); + $this->assertSame( 'delivery-1', $data['delivery_id'] ); + $this->assertSame( + array( + 'https://libresign.coop/fragments/header/', + 'https://libresign.coop/fragments/footer/', + ), + $this->origin->urls() + ); + $this->assertSame( '
site
', libresign_get_site_fragment_html( 'header' ) ); + } + + public function test_a_production_deploy_records_the_last_synchronization() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $body = wp_json_encode( self::production_payload() ); + rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body ) ); + + $last_sync = get_option( 'libresign_site_fragment_last_sync' ); + + $this->assertSame( 'synced', $last_sync['status'] ); + $this->assertSame( 'LibreSign/site', $last_sync['details']['repository'] ); + $this->assertSame( '0f1e2d3', $last_sync['details']['source_sha'] ); + $this->assertSame( array( 'header', 'footer' ), $last_sync['details']['synced'] ); + } + + public function test_the_same_delivery_is_only_processed_once() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $body = wp_json_encode( self::production_payload() ); + rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body, 'delivery-42' ) ); + $this->origin->forget(); + + $response = rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body, 'delivery-42' ) ); + + $this->assertSame( 202, $response->get_status() ); + $this->assertSame( 'duplicate_delivery', $response->get_data()['reason'] ); + $this->assertSame( array(), $this->origin->urls() ); + } + + public function test_a_failing_origin_is_reported_and_recorded() { + $this->origin->answer_with( new WP_Error( 'http_request_failed', 'Connection refused' ) ); + + $body = wp_json_encode( self::production_payload() ); + $response = rest_get_server()->dispatch( $this->signed_request( 'workflow_run', $body ) ); + + $this->assertTrue( $response->is_error() ); + $this->assertSame( 'http_request_failed', $response->get_data()['code'] ); + + $last_sync = get_option( 'libresign_site_fragment_last_sync' ); + + $this->assertSame( 'error', $last_sync['status'] ); + $this->assertSame( 'http_request_failed', $last_sync['details']['code'] ); + } + + public function test_the_delivery_guard_lets_the_first_delivery_through() { + $this->assertTrue( libresign_mark_github_delivery_once( 'abc' ) ); + $this->assertFalse( libresign_mark_github_delivery_once( 'abc' ) ); + $this->assertTrue( libresign_mark_github_delivery_once( 'def' ) ); + } + + public function test_a_delivery_without_an_identifier_is_never_blocked() { + $this->assertTrue( libresign_mark_github_delivery_once( '' ) ); + $this->assertTrue( libresign_mark_github_delivery_once( ' ' ) ); + } + + /** + * @dataProvider provide_stored_options + * + * @param string $option Option name. + * @param mixed $stored Value stored in the database. + * @param callable $resolver Function resolving the option. + * @param string $expected Expected resolved value. + */ + public function test_option_resolvers_fall_back_to_the_production_defaults( $option, $stored, $resolver, $expected ) { + if ( null === $stored ) { + delete_option( $option ); + } else { + update_option( $option, $stored ); + } + + $this->assertSame( $expected, call_user_func( $resolver ) ); + } + + /** + * @return iterable + */ + public static function provide_stored_options() { + yield 'the workflow name is stored' => array( + 'libresign_site_deploy_workflow_name', + 'deploy', + 'libresign_site_deploy_workflow_name', + 'deploy', + ); + yield 'the workflow name is trimmed' => array( + 'libresign_site_deploy_workflow_name', + ' deploy ', + 'libresign_site_deploy_workflow_name', + 'deploy', + ); + yield 'no workflow name falls back' => array( + 'libresign_site_deploy_workflow_name', + null, + 'libresign_site_deploy_workflow_name', + 'pages build and deployment', + ); + yield 'a blank workflow name falls back' => array( + 'libresign_site_deploy_workflow_name', + ' ', + 'libresign_site_deploy_workflow_name', + 'pages build and deployment', + ); + yield 'no branch falls back' => array( + 'libresign_site_deploy_branch_name', + null, + 'libresign_site_deploy_branch_name', + 'gh-pages', + ); + yield 'no repository falls back' => array( + 'libresign_github_deploy_organization_repository', + null, + 'libresign_site_deploy_repository_name', + 'LibreSign/site', + ); + yield 'no origin falls back' => array( + 'libresign_site_origin', + null, + 'libresign_site_origin', + 'https://libresign.coop', + ); + yield 'the origin loses its trailing slash' => array( + 'libresign_site_origin', + 'https://staging.libresign.coop/', + 'libresign_site_origin', + 'https://staging.libresign.coop', + ); + } + + public function test_a_secret_stored_in_plain_text_is_used_as_is() { + update_option( 'libresign_github_webhook_secret', 'plain-secret' ); + + $this->assertSame( 'plain-secret', libresign_github_webhook_secret() ); + } + + public function test_a_secret_saved_over_an_existing_one_is_stored_encrypted() { + $this->register_plugin_settings(); + + update_option( 'libresign_github_webhook_secret', 'saved-from-the-form' ); + + $this->assertNotSame( 'saved-from-the-form', get_option( 'libresign_github_webhook_secret' ) ); + $this->assertSame( 'saved-from-the-form', libresign_github_webhook_secret() ); + } + + /** + * A secret saved on a site that never had one goes through add_option(), + * which sanitizes the already sanitized value a second time, so it is only + * stored encrypted once because the encryption is idempotent. + */ + public function test_a_secret_saved_for_the_first_time_is_stored_encrypted() { + delete_option( 'libresign_github_webhook_secret' ); + $this->register_plugin_settings(); + + update_option( 'libresign_github_webhook_secret', 'saved-from-the-form' ); + + $this->assertNotSame( 'saved-from-the-form', get_option( 'libresign_github_webhook_secret' ) ); + $this->assertSame( 'saved-from-the-form', libresign_github_webhook_secret() ); + } + + public function test_saving_an_empty_secret_keeps_the_previous_one() { + $this->register_plugin_settings(); + + update_option( 'libresign_github_webhook_secret', 'saved-from-the-form' ); + update_option( 'libresign_github_webhook_secret', '' ); + + $this->assertSame( 'saved-from-the-form', libresign_github_webhook_secret() ); + } +} diff --git a/tests/Integration/Includes/SiteFragmentSyncTest.php b/tests/Integration/Includes/SiteFragmentSyncTest.php new file mode 100644 index 0000000..4df421d --- /dev/null +++ b/tests/Integration/Includes/SiteFragmentSyncTest.php @@ -0,0 +1,196 @@ +origin = new FakeHttp(); + } + + /** + * @dataProvider provide_origins + * + * @param string $origin Raw origin value. + * @param string $expected Normalized origin. + */ + public function test_normalize_site_origin( $origin, $expected ) { + $this->assertSame( $expected, libresign_site_fragment_normalize_site_origin( $origin ) ); + } + + /** + * @return iterable + */ + public static function provide_origins() { + yield 'an origin already normalized' => array( 'https://libresign.coop', 'https://libresign.coop' ); + yield 'the trailing slash goes away' => array( 'https://libresign.coop/', 'https://libresign.coop' ); + yield 'whitespace goes away' => array( " https://libresign.coop/ \n", 'https://libresign.coop' ); + yield 'a path is preserved' => array( 'https://libresign.coop/site/', 'https://libresign.coop/site' ); + yield 'a port is preserved' => array( 'http://localhost:8081/', 'http://localhost:8081' ); + yield 'a javascript url is dropped' => array( 'javascript:alert(1)', '' ); + yield 'an empty origin stays empty' => array( '', '' ); + } + + public function test_a_fragment_is_fetched_from_the_origin() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $this->assertSame( '
site
', libresign_fetch_site_fragment( self::ORIGIN, 'header' ) ); + $this->assertSame( array( self::ORIGIN . '/fragments/header/' ), $this->origin->urls() ); + } + + public function test_a_failing_status_becomes_an_error() { + $this->origin->answer_with( FakeHttp::response( 404, 'Not Found' ) ); + + $result = libresign_fetch_site_fragment( self::ORIGIN, 'header' ); + + $this->assertWPError( $result ); + $this->assertSame( 'libresign_fragment_fetch_failed', $result->get_error_code() ); + } + + public function test_an_empty_body_becomes_an_error() { + $this->origin->answer_with( FakeHttp::response( 200, " \n" ) ); + + $result = libresign_fetch_site_fragment( self::ORIGIN, 'header' ); + + $this->assertWPError( $result ); + $this->assertSame( 'libresign_fragment_empty', $result->get_error_code() ); + } + + public function test_a_transport_error_is_passed_through() { + $this->origin->answer_with( new WP_Error( 'http_request_failed', 'Connection refused' ) ); + + $result = libresign_fetch_site_fragment( self::ORIGIN, 'header' ); + + $this->assertWPError( $result ); + $this->assertSame( 'http_request_failed', $result->get_error_code() ); + } + + public function test_synchronizing_stores_every_fragment_with_its_metadata() { + $this->origin->answer_each( + array( + self::ORIGIN . '/fragments/header/' => FakeHttp::response( 200, '
site
' ), + self::ORIGIN . '/fragments/footer/' => FakeHttp::response( 200, '
site
' ), + ) + ); + + $result = libresign_sync_site_fragments_from_origin( + self::ORIGIN . '/', + array( 'header', 'footer' ), + array( + 'generated_at' => '2026-09-14T12:00:00Z', + 'source_sha' => '0f1e2d3', + 'source_url' => 'https://github.com/LibreSign/site/actions/runs/1', + ) + ); + + $this->assertSame( + array( + 'synced' => array( 'header', 'footer' ), + 'origin' => self::ORIGIN, + ), + $result + ); + + $header = get_option( 'libresign_site_fragment_header' ); + + $this->assertSame( '
site
', $header['html'] ); + $this->assertSame( self::ORIGIN, $header['origin'] ); + $this->assertSame( '2026-09-14T12:00:00Z', $header['generated_at'] ); + $this->assertSame( '0f1e2d3', $header['source_sha'] ); + $this->assertSame( 'https://github.com/LibreSign/site/actions/runs/1', $header['source_url'] ); + $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $header['synced_at'] ); + $this->assertSame( '
site
', libresign_get_site_fragment_html( 'footer' ) ); + } + + public function test_missing_metadata_is_stored_as_empty_strings() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + libresign_sync_site_fragments_from_origin( self::ORIGIN, array( 'header' ) ); + + $header = get_option( 'libresign_site_fragment_header' ); + + $this->assertSame( '', $header['generated_at'] ); + $this->assertSame( '', $header['source_sha'] ); + $this->assertSame( '', $header['source_url'] ); + } + + public function test_one_broken_fragment_does_not_stop_the_other() { + $this->origin->answer_each( + array( + self::ORIGIN . '/fragments/header/' => FakeHttp::response( 500, 'Server Error' ), + self::ORIGIN . '/fragments/footer/' => FakeHttp::response( 200, '
site
' ), + ) + ); + + $result = libresign_sync_site_fragments_from_origin( self::ORIGIN, array( 'header', 'footer' ) ); + + $this->assertSame( array( 'footer' ), $result['synced'] ); + $this->assertSame( '', libresign_get_site_fragment_html( 'header' ) ); + $this->assertSame( '
site
', libresign_get_site_fragment_html( 'footer' ) ); + } + + public function test_an_origin_that_is_entirely_down_returns_the_last_error() { + $this->origin->answer_with( FakeHttp::response( 500, 'Server Error' ) ); + + $result = libresign_sync_site_fragments_from_origin( self::ORIGIN, array( 'header', 'footer' ) ); + + $this->assertWPError( $result ); + $this->assertSame( 'libresign_fragment_fetch_failed', $result->get_error_code() ); + } + + public function test_blank_fragment_names_are_skipped() { + $this->origin->answer_with( FakeHttp::response( 200, '
site
' ) ); + + $result = libresign_sync_site_fragments_from_origin( self::ORIGIN, array( '', ' ', 'header' ) ); + + $this->assertSame( array( 'header' ), $result['synced'] ); + $this->assertSame( array( self::ORIGIN . '/fragments/header/' ), $this->origin->urls() ); + } + + public function test_syncing_nothing_reports_no_fragments() { + $result = libresign_sync_site_fragments_from_origin( self::ORIGIN, array() ); + + $this->assertSame( + array( + 'synced' => array(), + 'origin' => self::ORIGIN, + ), + $result + ); + } + + public function test_a_fragment_that_was_never_synced_has_no_html() { + $this->assertSame( '', libresign_get_site_fragment_html( 'header' ) ); + $this->assertSame( '', libresign_get_site_fragment_html( 'does-not-exist' ) ); + } + + public function test_a_fragment_stored_without_html_has_no_html() { + update_option( 'libresign_site_fragment_header', array( 'origin' => self::ORIGIN ) ); + + $this->assertSame( '', libresign_get_site_fragment_html( 'header' ) ); + } +} diff --git a/tests/Integration/LibresignWpCustomizationsTest.php b/tests/Integration/LibresignWpCustomizationsTest.php new file mode 100644 index 0000000..8e50250 --- /dev/null +++ b/tests/Integration/LibresignWpCustomizationsTest.php @@ -0,0 +1,389 @@ +github = new FakeHttp(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + + update_option( 'libresign_github_deploy_organization_repository', 'LibreSign/site' ); + update_option( 'libresign_github_deploy_token', PluginSecret::encrypt( 'deploy-token' ) ); + } + + public function tear_down() { + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * The deploy notices queued for the current user. + * + * @return array> + */ + private function queued_deploy_notices() { + $notices = get_transient( 'libresign_github_action_status_' . get_current_user_id() ); + + return is_array( $notices ) ? $notices : array(); + } + + public function test_the_version_route_answers_with_the_wordpress_version() { + global $wp_version; + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/libresign/v1/version' ) ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( array( 'version' => $wp_version ), $response->get_data() ); + } + + public function test_publishing_a_post_dispatches_the_site_deploy() { + update_option( 'libresign_github_deploy_token', PluginSecret::encrypt( 'deploy-token' ) ); + $this->github->answer_with( FakeHttp::response( 204 ) ); + + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $this->assertSame( array( 'https://api.github.com/repos/LibreSign/site/dispatches' ), $this->github->urls() ); + $this->assertSame( 'Bearer deploy-token', $this->github->args()['headers']['Authorization'] ); + $this->assertSame( array( 'event_type' => 'deploy-site' ), json_decode( $this->github->args()['body'], true ) ); + + $notices = $this->queued_deploy_notices(); + + $this->assertCount( 1, $notices ); + $this->assertSame( 'success', $notices[0]['type'] ); + $this->assertSame( $post_id, $notices[0]['post_id'] ); + $this->assertSame( 'indefinido', $notices[0]['language'] ); + } + + public function test_a_refused_dispatch_is_reported_as_an_error() { + $this->github->answer_with( FakeHttp::response( 401, (string) wp_json_encode( array( 'message' => 'Bad credentials' ) ) ) ); + + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $notices = $this->queued_deploy_notices(); + + $this->assertSame( 'error', $notices[0]['type'] ); + $this->assertStringContainsString( '401', $notices[0]['message'] ); + $this->assertStringContainsString( 'Bad credentials', $notices[0]['message'] ); + } + + public function test_an_unreachable_github_is_reported_as_an_error() { + $this->github->answer_with( new WP_Error( 'http_request_failed', 'Connection refused' ) ); + + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $notices = $this->queued_deploy_notices(); + + $this->assertSame( 'error', $notices[0]['type'] ); + $this->assertSame( 'Connection refused', $notices[0]['message'] ); + } + + public function test_a_draft_does_not_dispatch_anything() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + + self::factory()->post->create( array( 'post_status' => 'draft' ) ); + + $this->assertSame( array(), $this->github->urls() ); + $this->assertSame( array(), $this->queued_deploy_notices() ); + } + + public function test_publishing_a_page_does_not_dispatch_anything() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + ) + ); + + $this->assertSame( array(), $this->github->urls() ); + $this->assertSame( array(), $this->queued_deploy_notices() ); + } + + public function test_trashing_a_published_page_does_not_dispatch_anything() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + ) + ); + $this->github->forget(); + + wp_trash_post( $page_id ); + + $this->assertSame( array(), $this->github->urls() ); + } + + public function test_trashing_a_published_post_dispatches_the_site_deploy() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $this->github->forget(); + + wp_trash_post( $post_id ); + + $this->assertCount( 1, $this->github->urls() ); + } + + public function test_a_token_saved_over_an_existing_one_is_stored_encrypted() { + update_option( 'libresign_github_deploy_token', 'placeholder' ); + $this->register_plugin_settings(); + + update_option( 'libresign_github_deploy_token', 'a-personal-access-token' ); + + $this->assertNotSame( 'a-personal-access-token', get_option( 'libresign_github_deploy_token' ) ); + + $this->github->answer_with( FakeHttp::response( 204 ) ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $this->assertSame( 'Bearer a-personal-access-token', $this->github->args()['headers']['Authorization'] ); + } + + /** + * A token saved on a site that never had one goes through add_option(), + * which sanitizes the already sanitized value a second time, so it is only + * stored encrypted once because the encryption is idempotent. + */ + public function test_a_token_saved_for_the_first_time_is_stored_encrypted() { + delete_option( 'libresign_github_deploy_token' ); + $this->register_plugin_settings(); + + update_option( 'libresign_github_deploy_token', 'a-personal-access-token' ); + + $this->assertNotSame( 'a-personal-access-token', get_option( 'libresign_github_deploy_token' ) ); + + $this->github->answer_with( FakeHttp::response( 204 ) ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $this->assertSame( 'Bearer a-personal-access-token', $this->github->args()['headers']['Authorization'] ); + } + + public function test_a_deploy_token_that_cannot_be_decrypted_is_not_sent_to_github() { + update_option( 'libresign_github_deploy_token', PluginSecret::encrypt_with_other_salts( 'a-personal-access-token' ) ); + $this->github->answer_with( FakeHttp::response( 204 ) ); + + self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + $this->assertSame( array(), $this->github->urls() ); + } + + public function test_a_setting_of_zero_is_stored_instead_of_being_taken_for_a_blank_field() { + $this->register_plugin_settings(); + + update_option( 'libresign_github_webhook_secret', 'the-previous-secret' ); + update_option( 'libresign_github_webhook_secret', '0' ); + + $this->assertSame( '0', libresign_github_webhook_secret() ); + } + + public function test_saving_an_empty_deploy_token_keeps_the_previous_one() { + update_option( 'libresign_github_deploy_token', 'placeholder' ); + $this->register_plugin_settings(); + + update_option( 'libresign_github_deploy_token', 'a-personal-access-token' ); + $stored = get_option( 'libresign_github_deploy_token' ); + + update_option( 'libresign_github_deploy_token', '' ); + + $this->assertSame( $stored, get_option( 'libresign_github_deploy_token' ) ); + } + + /** + * @dataProvider provide_settings + * + * @param string $option Option name. + * @param string $saved Value submitted on the settings screen. + * @param string $expected Value actually stored. + */ + public function test_the_settings_fall_back_to_the_production_defaults( $option, $saved, $expected ) { + $this->register_plugin_settings(); + + update_option( $option, $saved ); + + $this->assertSame( $expected, get_option( $option ) ); + } + + /** + * @return iterable + */ + public static function provide_settings() { + yield 'an empty origin falls back' => array( 'libresign_site_origin', ' ', 'https://libresign.coop' ); + yield 'the origin loses its slash' => array( 'libresign_site_origin', 'https://staging.libresign.coop/', 'https://staging.libresign.coop' ); + yield 'an empty workflow falls back' => array( 'libresign_site_deploy_workflow_name', '', 'pages build and deployment' ); + yield 'the workflow is trimmed' => array( 'libresign_site_deploy_workflow_name', ' deploy ', 'deploy' ); + yield 'an empty branch falls back' => array( 'libresign_site_deploy_branch_name', '', 'gh-pages' ); + yield 'the branch is kept' => array( 'libresign_site_deploy_branch_name', 'main', 'main' ); + } + + public function test_an_internal_article_is_not_indexed() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + wp_set_post_categories( $post_id, array( self::factory()->category->create( array( 'slug' => 'article' ) ) ) ); + $this->go_to( get_permalink( $post_id ) ); + + ob_start(); + libresign_wp_add_noindex_meta_tag(); + $html = ob_get_clean(); + + $this->assertSame( '' . PHP_EOL, $html ); + } + + public function test_a_regular_post_is_indexed() { + $this->github->answer_with( FakeHttp::response( 204 ) ); + $post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $this->go_to( get_permalink( $post_id ) ); + + ob_start(); + libresign_wp_add_noindex_meta_tag(); + + $this->assertSame( '', ob_get_clean() ); + } + + public function test_the_home_page_is_indexed() { + $this->go_to( home_url( '/' ) ); + + ob_start(); + libresign_wp_add_noindex_meta_tag(); + + $this->assertSame( '', ob_get_clean() ); + } + + /** + * @dataProvider provide_nextcloud_hosts + * + * @param string $public_host Value of nextcloud_public_host. + * @param string $api_host Value of nextcloud_api_host. + * @param string $expected Host used for the customer facing link. + */ + public function test_the_customer_link_prefers_the_public_host( $public_host, $api_host, $expected ) { + update_option( 'nextcloud_public_host', $public_host ); + update_option( 'nextcloud_api_host', $api_host ); + + $this->assertSame( $expected, libresign_get_nextcloud_public_host() ); + } + + /** + * @return iterable + */ + public static function provide_nextcloud_hosts() { + yield 'the public host wins' => array( 'https://cloud.libresign.coop', 'http://host.docker.internal:8082', 'https://cloud.libresign.coop' ); + yield 'without it the api host is used' => array( '', 'https://cloud.libresign.coop', 'https://cloud.libresign.coop' ); + yield 'a blank public host is ignored' => array( ' ', 'https://cloud.libresign.coop', 'https://cloud.libresign.coop' ); + yield 'both are trimmed' => array( '', ' https://cloud.libresign.coop ', 'https://cloud.libresign.coop' ); + yield 'nothing configured, nothing shown' => array( '', '', '' ); + } + + public function test_a_page_without_polylang_is_its_own_translation() { + $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); + + $this->assertSame( $page_id, libresign_get_translated_page_id( $page_id, 'pt' ) ); + $this->assertSame( $page_id, libresign_get_translated_page_id( $page_id ) ); + } + + /** + * @dataProvider provide_missing_pages + * + * @param mixed $page_id Page identifier. + */ + public function test_a_missing_page_has_no_translation( $page_id ) { + $this->assertSame( 0, libresign_get_translated_page_id( $page_id, 'pt' ) ); + } + + /** + * @return iterable + */ + public static function provide_missing_pages() { + yield 'no page' => array( 0 ); + yield 'a negative id' => array( -1 ); + yield 'an empty id' => array( '' ); + } + + public function test_the_rewrite_rules_are_flushed_once_per_version() { + update_option( 'libresign_root_my_account_rewrite_version', 'stale' ); + + libresign_maybe_flush_root_my_account_endpoints(); + + $this->assertSame( LIBRESIGN_WP_REWRITE_VERSION, get_option( 'libresign_root_my_account_rewrite_version' ) ); + } + + public function test_the_settings_link_comes_first_on_the_plugins_screen() { + $links = libresign_add_settings_link( array( 'Deactivate' ) ); + + $this->assertCount( 2, $links ); + $this->assertStringContainsString( 'options-general.php?page=libresign-config', $links[0] ); + $this->assertSame( 'Deactivate', $links[1] ); + } + + public function test_the_settings_are_registered_on_admin_init() { + $this->assertSame( 10, has_action( 'admin_init', 'libresign_register_settings' ) ); + } + + public function test_the_post_author_is_exposed_with_a_gravatar_hash() { + $user_id = (int) self::factory()->user->create( + array( + 'user_email' => ' Ana@Example.ORG ', + 'display_name' => 'Ana Lima', + ) + ); + $post = get_post( self::factory()->post->create( array( 'post_author' => $user_id ) ) ); + + $response = new WP_REST_Response( array( 'author' => $user_id ) ); + $filtered = apply_filters( 'rest_prepare_post', $response, $post, new WP_REST_Request( 'GET', '/wp/v2/posts' ) ); + + $this->assertInstanceOf( WP_REST_Response::class, $filtered ); + $this->assertSame( + array( + // The author identifier is whatever post_author holds, a string. + 'id' => (string) $user_id, + 'name' => 'Ana Lima', + 'gravatar_hash' => md5( 'ana@example.org' ), + ), + $filtered->get_data()['author'] + ); + } + + public function test_the_plugin_requires_the_wordpress_version_the_suite_runs_against() { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + $data = get_plugin_data( dirname( __DIR__, 2 ) . '/libresign-wp-customizations.php', false, false ); + + $this->assertSame( '7.0', $data['RequiresWP'] ); + } +} diff --git a/tests/Support/FakeHttp.php b/tests/Support/FakeHttp.php new file mode 100644 index 0000000..0e77685 --- /dev/null +++ b/tests/Support/FakeHttp.php @@ -0,0 +1,130 @@ +}> + */ + private $requests = array(); + + /** + * Answer every request with the same response. + * + * @param array|WP_Error $response Canned response. + * @return void + */ + public function answer_with( $response ) { + $this->answer_each( array( '*' => $response ) ); + } + + /** + * Answer each URL with its own response, falling back to the '*' entry. + * + * A URL with neither an entry of its own nor a '*' fallback is answered with + * an error naming it, so a request that drifts fails as an assertion instead + * of as an undefined array key. + * + * @param array|WP_Error> $responses Response per URL. + * @return void + */ + public function answer_each( $responses ) { + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) use ( $responses ) { + $this->requests[] = array( + 'url' => (string) $url, + 'args' => (array) $args, + ); + + if ( isset( $responses[ $url ] ) ) { + return $responses[ $url ]; + } + + if ( isset( $responses['*'] ) ) { + return $responses['*']; + } + + return new WP_Error( + 'libresign_tests_http_unstubbed', + sprintf( 'No response was stubbed for %s.', $url ) + ); + }, + 10, + 3 + ); + } + + /** + * Requests recorded so far. + * + * @return array}> + */ + public function requests() { + return $this->requests; + } + + /** + * URLs requested so far. + * + * @return string[] + */ + public function urls() { + return array_column( $this->requests, 'url' ); + } + + /** + * Arguments of a recorded request. + * + * @param int $index Position of the request. + * @return array + */ + public function args( $index = 0 ) { + return isset( $this->requests[ $index ] ) ? $this->requests[ $index ]['args'] : array(); + } + + /** + * Drop the recorded requests, keeping the responses in place. + * + * @return void + */ + public function forget() { + $this->requests = array(); + } + + /** + * Build a response in the shape the HTTP API returns. + * + * @param int $code HTTP status code. + * @param string $body Response body. + * @return array + */ + public static function response( $code, $body = '' ) { + return array( + 'headers' => array(), + 'body' => $body, + 'response' => array( + 'code' => $code, + 'message' => get_status_header_desc( $code ), + ), + 'cookies' => array(), + 'filename' => null, + ); + } +} diff --git a/tests/Support/PluginSecret.php b/tests/Support/PluginSecret.php new file mode 100644 index 0000000..0a8d228 --- /dev/null +++ b/tests/Support/PluginSecret.php @@ -0,0 +1,50 @@ + + */ + private static function woocommerce_menu() { + return array( + 'dashboard' => 'Dashboard', + 'orders' => 'Orders', + 'subscriptions' => 'Subscriptions', + 'downloads' => 'Downloads', + 'edit-address' => 'Addresses', + 'payment-methods' => 'Payment methods', + 'edit-account' => 'Account details', + 'customer-logout' => 'Log out', + ); + } + + public function test_keeps_five_entries_in_the_configured_order() { + $this->assertSame( + array( 'subscriptions', 'orders', 'payment-methods', 'edit-account', 'customer-logout' ), + array_keys( Navigation::filter_items( self::woocommerce_menu() ) ) + ); + } + + public function test_renames_only_the_three_entries_whose_meaning_changed() { + $menu = Navigation::filter_items( self::woocommerce_menu() ); + + $this->assertSame( 'My subscription', $menu['subscriptions'] ); + $this->assertSame( 'Invoices', $menu['orders'] ); + $this->assertSame( 'Billing', $menu['payment-methods'] ); + $this->assertSame( 'Account details', $menu['edit-account'] ); + $this->assertSame( 'Log out', $menu['customer-logout'] ); + } + + /** + * @dataProvider provide_menus + * + * @param array $items Navigation handed over by WooCommerce. + * @param string[] $expected Expected resulting keys. + */ + public function test_filter_items( $items, $expected ) { + $this->assertSame( $expected, array_keys( Navigation::filter_items( $items ) ) ); + } + + /** + * @return iterable, 1: string[]}> + */ + public static function provide_menus() { + yield 'entries outside the list are dropped' => array( + self::woocommerce_menu(), + array( 'subscriptions', 'orders', 'payment-methods', 'edit-account', 'customer-logout' ), + ); + yield 'unregistered endpoints are skipped' => array( + array( + 'dashboard' => 'Dashboard', + 'orders' => 'Orders', + 'customer-logout' => 'Log out', + ), + array( 'orders', 'customer-logout' ), + ); + yield 'the input order is irrelevant' => array( + array( + 'customer-logout' => 'Log out', + 'orders' => 'Orders', + 'subscriptions' => 'Subscriptions', + ), + array( 'subscriptions', 'orders', 'customer-logout' ), + ); + yield 'an empty navigation stays empty' => array( array(), array() ); + yield 'a navigation with nothing we keep' => array( + array( + 'dashboard' => 'Dashboard', + 'downloads' => 'Downloads', + ), + array(), + ); + } + + public function test_renamed_entries_are_the_ones_with_a_matching_endpoint_title() { + $this->assertSame( + array( 'subscriptions', 'orders', 'payment-methods' ), + array_keys( Navigation::labels() ) + ); + } + + public function test_detail_endpoints_are_mapped_to_the_entry_they_belong_to() { + $aliases = Navigation::aliases(); + + $this->assertSame( array( 'subscription-payment-method' ), $aliases['subscriptions'] ); + $this->assertSame( array( 'edit-address' ), $aliases['payment-methods'] ); + $this->assertSame( array( 'subscriptions', 'payment-methods' ), array_keys( $aliases ) ); + } + + /** + * @dataProvider provide_endpoint_titles + * + * @param string $title Title WooCommerce would use. + * @param string $endpoint Endpoint being rendered. + * @param array $query_vars Query vars of the request being rendered. + * @param string $expected Expected title. + */ + public function test_endpoint_title( $title, $endpoint, $query_vars, $expected ) { + $this->assertSame( $expected, Navigation::endpoint_title( $title, $endpoint, $query_vars ) ); + } + + /** + * @return iterable, 3: string}> + */ + public static function provide_endpoint_titles() { + yield 'the title follows the navigation label' => array( 'Orders', 'orders', array( 'orders' => '' ), 'Invoices' ); + yield 'a paginated screen carries the page number' => array( 'Orders', 'orders', array( 'orders' => '3' ), 'Invoices (page 3)' ); + yield 'the first page is not numbered' => array( 'Orders', 'orders', array( 'orders' => '1' ), 'Invoices' ); + yield 'an entry that was not renamed keeps its title' => array( 'Account details', 'edit-account', array( 'edit-account' => '' ), 'Account details' ); + yield 'an endpoint absent from the request is not numbered' => array( 'Orders', 'orders', array(), 'Invoices' ); + } + + /** + * @dataProvider provide_detail_screens + * + * @param string[] $classes Classes WooCommerce computed. + * @param string $endpoint Navigation entry being rendered. + * @param array $query_vars Query vars of the request being rendered. + * @param bool $active Whether the entry ends up highlighted. + */ + public function test_filter_item_classes( $classes, $endpoint, $query_vars, $active ) { + $filtered = Navigation::filter_item_classes( $classes, $endpoint, $query_vars ); + + $this->assertSame( $active, in_array( 'is-active', $filtered, true ) ); + } + + /** + * @return iterable, 3: bool}> + */ + public static function provide_detail_screens() { + yield 'changing the payment method of a subscription' => array( + array( 'subscriptions' ), + 'subscriptions', + array( 'subscription-payment-method' => '12' ), + true, + ); + yield 'editing an address' => array( + array( 'payment-methods' ), + 'payment-methods', + array( 'edit-address' => 'billing' ), + true, + ); + yield 'an entry without detail screens' => array( + array( 'orders' ), + 'orders', + array( 'edit-address' => 'billing' ), + false, + ); + yield 'an entry whose detail screen is not being rendered' => array( + array( 'payment-methods' ), + 'payment-methods', + array( 'orders' => '' ), + false, + ); + yield 'an entry WooCommerce already highlighted' => array( + array( 'orders', 'is-active' ), + 'orders', + array(), + true, + ); + } + + public function test_the_classes_of_an_unrelated_entry_are_untouched() { + $this->assertSame( + array( 'orders' ), + Navigation::filter_item_classes( array( 'orders' ), 'orders', array( 'edit-address' => 'billing' ) ) + ); + } +} diff --git a/tests/Unit/Account/RootEndpointTest.php b/tests/Unit/Account/RootEndpointTest.php new file mode 100644 index 0000000..3a8af46 --- /dev/null +++ b/tests/Unit/Account/RootEndpointTest.php @@ -0,0 +1,69 @@ + + */ + private static function query_vars() { + return array( + 'order-pay' => 'order-pay', + 'order-received' => 'order-received', + 'orders' => 'orders', + 'edit-account' => 'edit-account', + 'edit-address' => 'edit-address', + 'payment-methods' => 'payment-methods', + 'lost-password' => 'lost-password', + 'customer-logout' => 'customer-logout', + 'subscriptions' => 'subscriptions', + 'add-payment-method' => '', + ); + } + + /** + * @dataProvider provide_paths + * + * @param string $request_path Path of the request. + * @param bool $expected Whether it is an account endpoint. + */ + public function test_matches( $request_path, $expected ) { + $this->assertSame( $expected, RootEndpoint::matches( $request_path, self::query_vars() ) ); + } + + /** + * @return iterable + */ + public static function provide_paths() { + yield 'an endpoint at the root' => array( '/payment-methods/', true ); + yield 'an endpoint with a value' => array( '/edit-address/billing/', true ); + yield 'the my-account alias' => array( '/my-account/', true ); + yield 'an endpoint under the alias' => array( '/my-account/orders/', true ); + yield 'no slashes around the path' => array( 'lost-password', true ); + yield 'the front page itself' => array( '/', false ); + yield 'an empty path' => array( '', false ); + yield 'a page' => array( '/planos/', false ); + yield 'a post' => array( '/2026/09/hello-world/', false ); + yield 'an endpoint deeper in the path' => array( '/planos/orders/', false ); + yield 'an endpoint that is not registered' => array( '/add-payment-method/', false ); + } + + public function test_a_store_without_endpoints_only_knows_the_alias() { + $this->assertTrue( RootEndpoint::matches( '/my-account/', array() ) ); + $this->assertFalse( RootEndpoint::matches( '/orders/', array() ) ); + } +} diff --git a/tests/Unit/Github/DeployDispatchTest.php b/tests/Unit/Github/DeployDispatchTest.php new file mode 100644 index 0000000..c72f564 --- /dev/null +++ b/tests/Unit/Github/DeployDispatchTest.php @@ -0,0 +1,63 @@ +assertSame( $expected, DeployDispatch::triggers_deploy( $new_status, $old_status, $post_type ) ); + } + + /** + * @return iterable + */ + public static function provide_transitions() { + yield 'publishing a post' => array( 'publish', 'draft', 'post', true ); + yield 'publishing a page' => array( 'publish', 'draft', 'page', false ); + yield 'publishing a product' => array( 'publish', 'draft', 'product', false ); + yield 'updating a published post' => array( 'publish', 'publish', 'post', true ); + yield 'trashing a published post' => array( 'trash', 'publish', 'post', true ); + yield 'trashing a published page' => array( 'trash', 'publish', 'page', false ); + yield 'trashing a published product' => array( 'trash', 'publish', 'product', false ); + yield 'saving a draft' => array( 'draft', 'draft', 'post', false ); + yield 'scheduling a post' => array( 'future', 'draft', 'post', false ); + yield 'restoring a post from trash' => array( 'draft', 'trash', 'post', false ); + } + + public function test_the_success_notice_links_to_the_actions_of_the_repository() { + $this->assertStringContainsString( + 'https://github.com/LibreSign/site/actions', + DeployDispatch::success_message( 'LibreSign/site' ) + ); + } + + public function test_the_failure_notice_carries_the_status_and_the_message() { + $message = DeployDispatch::failure_message( 401, 'Bad credentials' ); + + $this->assertStringContainsString( '401', $message ); + $this->assertStringContainsString( 'Bad credentials', $message ); + } + + public function test_the_failure_notice_survives_a_response_without_a_message() { + $this->assertStringContainsString( '500', DeployDispatch::failure_message( 500, '' ) ); + } +} diff --git a/tests/Unit/Github/SiteDeployTest.php b/tests/Unit/Github/SiteDeployTest.php new file mode 100644 index 0000000..733cf5d --- /dev/null +++ b/tests/Unit/Github/SiteDeployTest.php @@ -0,0 +1,111 @@ + $overrides Values replacing the defaults. + * @return WorkflowRun + */ + private static function production_run( array $overrides = array() ) { + return WorkflowRun::from_payload( + array_replace_recursive( + array( + 'action' => 'completed', + 'repository' => array( 'full_name' => 'LibreSign/site' ), + 'workflow_run' => array( + 'name' => 'pages build and deployment', + 'conclusion' => 'success', + 'head_branch' => 'gh-pages', + ), + ), + $overrides + ) + ); + } + + public function test_the_production_deploy_is_recognized() { + $this->assertTrue( self::site_deploy()->is_production_run( self::production_run() ) ); + } + + public function test_the_workflow_name_may_come_from_the_workflow_instead_of_the_run() { + $workflow_run = WorkflowRun::from_payload( + array( + 'action' => 'completed', + 'repository' => array( 'full_name' => 'LibreSign/site' ), + 'workflow' => array( 'name' => 'pages build and deployment' ), + 'workflow_run' => array( + 'conclusion' => 'success', + 'head_branch' => 'gh-pages', + ), + ) + ); + + $this->assertTrue( self::site_deploy()->is_production_run( $workflow_run ) ); + } + + /** + * @dataProvider provide_other_runs + * + * @param array $overrides Values replacing the production defaults. + */ + public function test_any_other_run_is_not_the_production_deploy( $overrides ) { + $this->assertFalse( self::site_deploy()->is_production_run( self::production_run( $overrides ) ) ); + } + + /** + * @return iterable}> + */ + public static function provide_other_runs() { + yield 'another repository' => array( array( 'repository' => array( 'full_name' => 'LibreSign/libresign' ) ) ); + yield 'the run is starting' => array( array( 'action' => 'requested' ) ); + yield 'the run is in progress' => array( array( 'action' => 'in_progress' ) ); + yield 'the run failed' => array( array( 'workflow_run' => array( 'conclusion' => 'failure' ) ) ); + yield 'the run was cancelled' => array( array( 'workflow_run' => array( 'conclusion' => 'cancelled' ) ) ); + yield 'another branch' => array( array( 'workflow_run' => array( 'head_branch' => 'main' ) ) ); + yield 'another workflow' => array( array( 'workflow_run' => array( 'name' => 'tests' ) ) ); + } + + public function test_the_expected_run_is_the_configured_one() { + $site_deploy = new SiteDeploy( 'LibreSign/staging', 'main', 'deploy' ); + + $this->assertFalse( $site_deploy->is_production_run( self::production_run() ) ); + $this->assertTrue( + $site_deploy->is_production_run( + self::production_run( + array( + 'repository' => array( 'full_name' => 'LibreSign/staging' ), + 'workflow_run' => array( + 'name' => 'deploy', + 'head_branch' => 'main', + ), + ) + ) + ) + ); + } +} diff --git a/tests/Unit/Github/WebhookGateTest.php b/tests/Unit/Github/WebhookGateTest.php new file mode 100644 index 0000000..d056d5e --- /dev/null +++ b/tests/Unit/Github/WebhookGateTest.php @@ -0,0 +1,226 @@ + $overrides Values replacing the defaults. + * @return string + */ + private static function production_payload( array $overrides = array() ) { + return (string) wp_json_encode( + array_replace_recursive( + array( + 'action' => 'completed', + 'repository' => array( 'full_name' => 'LibreSign/site' ), + 'workflow_run' => array( + 'name' => 'pages build and deployment', + 'conclusion' => 'success', + 'head_branch' => 'gh-pages', + 'head_sha' => '0f1e2d3', + ), + ), + $overrides + ) + ); + } + + /** + * The gate as configured in production. + * + * @param string $secret Secret shared with the repository webhook. + * @return WebhookGate + */ + private static function gate( $secret = self::SECRET ) { + return new WebhookGate( + $secret, + new SiteDeploy( 'LibreSign/site', 'gh-pages', 'pages build and deployment' ) + ); + } + + /** + * A delivery signed with the secret. + * + * @param string $event X-GitHub-Event header. + * @param string $body Raw request body. + * @return WebhookRequest + */ + private static function signed_delivery( $event, $body ) { + return new WebhookRequest( + 'GitHub-Hookshot/044aadd', + $event, + 'sha256=' . hash_hmac( 'sha256', $body, self::SECRET ), + 'delivery-1', + $body + ); + } + + public function test_an_unconfigured_secret_makes_the_endpoint_unavailable() { + $decision = self::gate( '' )->decide( self::signed_delivery( 'ping', '{}' ) ); + + $this->assertTrue( $decision->is_rejected() ); + $this->assertSame( 'libresign_github_webhook_secret_missing', $decision->code() ); + $this->assertSame( 503, $decision->status() ); + } + + public function test_a_blank_secret_makes_the_endpoint_unavailable() { + $decision = self::gate( ' ' )->decide( self::signed_delivery( 'ping', '{}' ) ); + + $this->assertSame( 'libresign_github_webhook_secret_missing', $decision->code() ); + } + + public function test_a_request_from_another_client_is_rejected() { + $decision = self::gate()->decide( new WebhookRequest( 'curl/8.7.1', 'ping', '', 'delivery-1', '{}' ) ); + + $this->assertTrue( $decision->is_rejected() ); + $this->assertSame( 'libresign_github_webhook_invalid_agent', $decision->code() ); + $this->assertSame( 403, $decision->status() ); + } + + public function test_a_wrongly_signed_request_is_rejected() { + $decision = self::gate()->decide( + new WebhookRequest( + 'GitHub-Hookshot/044aadd', + 'ping', + 'sha256=' . hash_hmac( 'sha256', '{}', 'another-secret' ), + 'delivery-1', + '{}' + ) + ); + + $this->assertTrue( $decision->is_rejected() ); + $this->assertSame( 'libresign_github_webhook_invalid_signature', $decision->code() ); + $this->assertSame( 403, $decision->status() ); + } + + public function test_the_client_is_checked_before_the_signature() { + $decision = self::gate()->decide( new WebhookRequest( 'curl/8.7.1', 'ping', 'sha256=deadbeef', 'delivery-1', '{}' ) ); + + $this->assertSame( 'libresign_github_webhook_invalid_agent', $decision->code() ); + } + + public function test_a_ping_is_answered() { + $decision = self::gate()->decide( self::signed_delivery( 'ping', '{"zen":"Design for failure."}' ) ); + + $this->assertTrue( $decision->is_pong() ); + } + + public function test_another_event_is_ignored() { + $decision = self::gate()->decide( self::signed_delivery( 'push', '{"ref":"refs/heads/main"}' ) ); + + $this->assertTrue( $decision->is_ignored() ); + $this->assertSame( + array( + 'reason' => 'unsupported_event', + 'event' => 'push', + ), + $decision->data() + ); + } + + public function test_a_body_that_is_not_json_is_rejected() { + $decision = self::gate()->decide( self::signed_delivery( 'workflow_run', 'not json' ) ); + + $this->assertTrue( $decision->is_rejected() ); + $this->assertSame( 'libresign_github_webhook_invalid_payload', $decision->code() ); + $this->assertSame( 400, $decision->status() ); + } + + /** + * @dataProvider provide_non_production_payloads + * + * @param array $overrides Values replacing the production defaults. + * @param array $reported_details Details expected in the answer. + */ + public function test_a_run_that_is_not_the_production_deploy_is_ignored( $overrides, $reported_details ) { + $decision = self::gate()->decide( self::signed_delivery( 'workflow_run', self::production_payload( $overrides ) ) ); + + $this->assertTrue( $decision->is_ignored() ); + $this->assertSame( array_merge( array( 'reason' => 'not_production_deploy' ), $reported_details ), $decision->data() ); + } + + /** + * @return iterable, 1: array}> + */ + public static function provide_non_production_payloads() { + yield 'another repository' => array( + array( 'repository' => array( 'full_name' => 'LibreSign/libresign' ) ), + array( + 'repository' => 'LibreSign/libresign', + 'workflow_name' => 'pages build and deployment', + 'head_branch' => 'gh-pages', + 'conclusion' => 'success', + ), + ); + yield 'the run is starting' => array( + array( 'action' => 'requested' ), + array( + 'repository' => 'LibreSign/site', + 'workflow_name' => 'pages build and deployment', + 'head_branch' => 'gh-pages', + 'conclusion' => 'success', + ), + ); + yield 'the run failed' => array( + array( 'workflow_run' => array( 'conclusion' => 'failure' ) ), + array( + 'repository' => 'LibreSign/site', + 'workflow_name' => 'pages build and deployment', + 'head_branch' => 'gh-pages', + 'conclusion' => 'failure', + ), + ); + yield 'another branch' => array( + array( 'workflow_run' => array( 'head_branch' => 'main' ) ), + array( + 'repository' => 'LibreSign/site', + 'workflow_name' => 'pages build and deployment', + 'head_branch' => 'main', + 'conclusion' => 'success', + ), + ); + yield 'another workflow' => array( + array( 'workflow_run' => array( 'name' => 'tests' ) ), + array( + 'repository' => 'LibreSign/site', + 'workflow_name' => 'tests', + 'head_branch' => 'gh-pages', + 'conclusion' => 'success', + ), + ); + } + + public function test_a_decision_that_is_not_a_deploy_has_no_workflow_run() { + $decision = self::gate()->decide( self::signed_delivery( 'push', '{"ref":"refs/heads/main"}' ) ); + + $this->expectException( LogicException::class ); + + $decision->workflow_run(); + } + + public function test_a_production_deploy_is_handed_over_with_the_run() { + $decision = self::gate()->decide( self::signed_delivery( 'workflow_run', self::production_payload() ) ); + + $this->assertTrue( $decision->is_deploy() ); + $this->assertSame( '0f1e2d3', $decision->workflow_run()->head_sha() ); + } +} diff --git a/tests/Unit/Github/WebhookRequestTest.php b/tests/Unit/Github/WebhookRequestTest.php new file mode 100644 index 0000000..d2ecc8c --- /dev/null +++ b/tests/Unit/Github/WebhookRequestTest.php @@ -0,0 +1,124 @@ + $headers Values replacing the empty defaults. + * @return WebhookRequest + */ + private static function delivery( array $headers = array() ) { + $headers = array_merge( + array( + 'user_agent' => 'GitHub-Hookshot/044aadd', + 'event' => 'workflow_run', + 'signature' => '', + 'delivery_id' => 'delivery-1', + 'body' => '{}', + ), + $headers + ); + + return new WebhookRequest( + $headers['user_agent'], + $headers['event'], + $headers['signature'], + $headers['delivery_id'], + $headers['body'] + ); + } + + /** + * @dataProvider provide_user_agents + * + * @param string $user_agent User agent header. + * @param bool $expected Expected result. + */ + public function test_is_from_github( $user_agent, $expected ) { + $this->assertSame( $expected, self::delivery( array( 'user_agent' => $user_agent ) )->is_from_github() ); + } + + /** + * @return iterable + */ + public static function provide_user_agents() { + yield 'the agent GitHub sends' => array( 'GitHub-Hookshot/044aadd', true ); + yield 'surrounding whitespace' => array( " GitHub-Hookshot/044aadd\n", true ); + yield 'the match is case sensitive' => array( 'github-hookshot/044aadd', false ); + yield 'the prefix has to come first' => array( 'curl/8.7.1 GitHub-Hookshot/044aadd', false ); + yield 'another client' => array( 'curl/8.7.1', false ); + yield 'no user agent at all' => array( '', false ); + } + + /** + * @dataProvider provide_events + * + * @param string $event X-GitHub-Event header. + * @param string $expected Normalized event name. + */ + public function test_event( $event, $expected ) { + $this->assertSame( $expected, self::delivery( array( 'event' => $event ) )->event() ); + } + + /** + * @return iterable + */ + public static function provide_events() { + yield 'as GitHub sends it' => array( 'workflow_run', 'workflow_run' ); + yield 'uppercase' => array( 'Workflow_Run', 'workflow_run' ); + yield 'padded' => array( " ping \n", 'ping' ); + yield 'absent' => array( '', '' ); + } + + public function test_the_delivery_identifier_is_kept_as_received() { + $this->assertSame( ' delivery-1 ', self::delivery( array( 'delivery_id' => ' delivery-1 ' ) )->delivery_id() ); + } + + public function test_the_signature_is_checked_against_the_body() { + $delivery = self::delivery( + array( + 'body' => '{"action":"completed"}', + 'signature' => 'sha256=' . hash_hmac( 'sha256', '{"action":"completed"}', 'the-secret' ), + ) + ); + + $this->assertTrue( $delivery->has_valid_signature( 'the-secret' ) ); + $this->assertFalse( $delivery->has_valid_signature( 'another-secret' ) ); + } + + /** + * @dataProvider provide_bodies + * + * @param string $body Raw request body. + * @param array|null $expected Expected payload. + */ + public function test_payload( $body, $expected ) { + $this->assertSame( $expected, self::delivery( array( 'body' => $body ) )->payload() ); + } + + /** + * @return iterable|null}> + */ + public static function provide_bodies() { + yield 'a json object' => array( '{"action":"completed"}', array( 'action' => 'completed' ) ); + yield 'an empty object' => array( '{}', array() ); + yield 'a json array' => array( '[1,2]', array( 1, 2 ) ); + yield 'not json at all' => array( 'not json', null ); + yield 'a json scalar' => array( '"completed"', null ); + yield 'an empty body' => array( '', null ); + } +} diff --git a/tests/Unit/Github/WebhookSignatureTest.php b/tests/Unit/Github/WebhookSignatureTest.php new file mode 100644 index 0000000..012d581 --- /dev/null +++ b/tests/Unit/Github/WebhookSignatureTest.php @@ -0,0 +1,57 @@ +assertSame( $expected, WebhookSignature::matches( $body, $signature, $secret ) ); + } + + /** + * @return iterable + */ + public static function provide_signatures() { + yield 'signature sent the way GitHub sends it' => array( self::BODY, 'sha256=' . self::digest(), self::SECRET, true ); + yield 'prefix is optional' => array( self::BODY, self::digest(), self::SECRET, true ); + yield 'prefix is case insensitive' => array( self::BODY, 'SHA256=' . self::digest(), self::SECRET, true ); + yield 'hexadecimal is case insensitive' => array( self::BODY, strtoupper( self::digest() ), self::SECRET, true ); + yield 'surrounding whitespace is trimmed' => array( self::BODY, ' sha256=' . self::digest() . "\n", ' ' . self::SECRET . ' ', true ); + yield 'another secret does not match' => array( self::BODY, 'sha256=' . self::digest(), 'another-secret', false ); + yield 'a tampered body does not match' => array( self::BODY . ' ', 'sha256=' . self::digest(), self::SECRET, false ); + yield 'an empty body is never valid' => array( '', 'sha256=' . self::digest(), self::SECRET, false ); + yield 'an empty secret is never valid' => array( self::BODY, 'sha256=' . self::digest(), '', false ); + yield 'an empty signature is never valid' => array( self::BODY, '', self::SECRET, false ); + yield 'a non hexadecimal signature is refused' => array( self::BODY, 'sha256=not-a-digest', self::SECRET, false ); + yield 'a truncated signature is refused' => array( self::BODY, 'sha256=' . substr( self::digest(), 0, 32 ), self::SECRET, false ); + } +} diff --git a/tests/Unit/Github/WorkflowRunTest.php b/tests/Unit/Github/WorkflowRunTest.php new file mode 100644 index 0000000..7e8c320 --- /dev/null +++ b/tests/Unit/Github/WorkflowRunTest.php @@ -0,0 +1,115 @@ + 'completed', + 'repository' => array( 'full_name' => 'LibreSign/site' ), + 'workflow_run' => array( + 'name' => 'pages build and deployment', + 'conclusion' => 'success', + 'head_branch' => 'gh-pages', + 'head_sha' => '0f1e2d3', + 'html_url' => 'https://github.com/LibreSign/site/actions/runs/1', + 'updated_at' => '2026-09-14T12:00:00Z', + ), + ) + ); + + $this->assertSame( 'LibreSign/site', $workflow_run->repository() ); + $this->assertSame( 'completed', $workflow_run->action() ); + $this->assertSame( 'success', $workflow_run->conclusion() ); + $this->assertSame( 'gh-pages', $workflow_run->head_branch() ); + $this->assertSame( '0f1e2d3', $workflow_run->head_sha() ); + $this->assertSame( 'https://github.com/LibreSign/site/actions/runs/1', $workflow_run->html_url() ); + $this->assertSame( '2026-09-14T12:00:00Z', $workflow_run->updated_at() ); + $this->assertSame( 'pages build and deployment', $workflow_run->workflow_name() ); + } + + public function test_an_empty_payload_answers_every_field_with_an_empty_string() { + $workflow_run = WorkflowRun::from_payload( array() ); + + $this->assertSame( '', $workflow_run->repository() ); + $this->assertSame( '', $workflow_run->action() ); + $this->assertSame( '', $workflow_run->conclusion() ); + $this->assertSame( '', $workflow_run->head_branch() ); + $this->assertSame( '', $workflow_run->head_sha() ); + $this->assertSame( '', $workflow_run->html_url() ); + $this->assertSame( '', $workflow_run->updated_at() ); + $this->assertSame( '', $workflow_run->workflow_name() ); + } + + /** + * @dataProvider provide_payloads + * + * @param array $payload Parsed webhook payload. + * @param string $expected Expected workflow name. + */ + public function test_workflow_name( $payload, $expected ) { + $this->assertSame( $expected, WorkflowRun::from_payload( $payload )->workflow_name() ); + } + + /** + * @return iterable, 1: string}> + */ + public static function provide_payloads() { + yield 'the run name wins' => array( + array( + 'workflow_run' => array( 'name' => 'pages build and deployment' ), + 'workflow' => array( 'name' => 'deploy' ), + ), + 'pages build and deployment', + ); + yield 'falls back to the workflow name' => array( + array( 'workflow' => array( 'name' => 'deploy' ) ), + 'deploy', + ); + yield 'a blank run name falls back too' => array( + array( + 'workflow_run' => array( 'name' => ' ' ), + 'workflow' => array( 'name' => 'deploy' ), + ), + 'deploy', + ); + yield 'names are trimmed' => array( + array( 'workflow_run' => array( 'name' => " deploy \n" ) ), + 'deploy', + ); + yield 'neither is present' => array( array( 'action' => 'completed' ), '' ); + yield 'an empty payload' => array( array(), '' ); + } + + /** + * @dataProvider provide_unexpected_shapes + * + * @param array $payload Payload in a shape GitHub does not send. + */ + public function test_a_field_that_is_not_a_string_is_read_as_empty( $payload ) { + $this->assertSame( '', WorkflowRun::from_payload( $payload )->head_branch() ); + } + + /** + * @return iterable}> + */ + public static function provide_unexpected_shapes() { + yield 'the run is not an array' => array( array( 'workflow_run' => 'gh-pages' ) ); + yield 'the branch is an array' => array( array( 'workflow_run' => array( 'head_branch' => array( 'gh-pages' ) ) ) ); + yield 'the branch is null' => array( array( 'workflow_run' => array( 'head_branch' => null ) ) ); + yield 'the run key is missing' => array( array( 'repository' => array( 'full_name' => 'LibreSign/site' ) ) ); + } +} diff --git a/tests/Unit/Settings/SecretTest.php b/tests/Unit/Settings/SecretTest.php new file mode 100644 index 0000000..cdae5f6 --- /dev/null +++ b/tests/Unit/Settings/SecretTest.php @@ -0,0 +1,117 @@ +assertSame( $value, $secret->decrypt( $secret->encrypt( $value ) ) ); + } + + /** + * @return iterable + */ + public static function provide_values() { + yield 'a personal access token' => array( 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' ); + yield 'a webhook secret' => array( 'a-shared-secret' ); + yield 'punctuation' => array( '{"not":"json"} + / = %' ); + yield 'accents' => array( 'segredo com acentuação' ); + yield 'a single character' => array( 'x' ); + } + + /** + * @dataProvider provide_values + * + * @param string $value Value to store. + */ + public function test_a_stored_value_is_read_back_for_sending( $value ) { + $secret = self::secret(); + + $this->assertSame( $value, $secret->decrypt_or_discard( $secret->encrypt( $value ) ) ); + } + + public function test_a_value_encrypted_elsewhere_is_discarded_instead_of_sent() { + $stored_value = Secret::from_salts( 'another-auth-key', 'another-secure-auth-salt', 'another-nonce-salt' ) + ->encrypt( 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' ); + + $this->assertSame( $stored_value, self::secret()->decrypt( $stored_value ) ); + $this->assertSame( '', self::secret()->decrypt_or_discard( $stored_value ) ); + } + + /** + * @dataProvider provide_plain_tokens + * + * @param string $stored_value Value found in the database. + */ + public function test_a_token_saved_by_hand_is_still_sent( $stored_value ) { + $this->assertSame( $stored_value, self::secret()->decrypt_or_discard( $stored_value ) ); + } + + /** + * A GitHub token carries an underscore, which base64 does not. + * + * @return iterable + */ + public static function provide_plain_tokens() { + yield 'a classic personal access token' => array( 'ghp_0123456789abcdefghijklmnopqrstuvwxyz' ); + yield 'a fine grained token' => array( 'github_pat_11ABCDEFG0abcdefghijklmn' ); + yield 'nothing saved yet' => array( '' ); + } + + public function test_the_stored_value_is_not_the_value() { + $this->assertNotSame( 'a-shared-secret', self::secret()->encrypt( 'a-shared-secret' ) ); + } + + public function test_another_installation_cannot_read_the_value() { + $stored_value = self::secret()->encrypt( 'a-shared-secret' ); + $another_installation = Secret::from_salts( 'another-auth-key', 'another-secure-auth-salt', 'another-nonce-salt' ); + + $this->assertNotSame( 'a-shared-secret', $another_installation->decrypt( $stored_value ) ); + } + + /** + * @dataProvider provide_stored_values + * + * @param string $stored_value Value found in the database. + * @param string $expected Value the plugin uses. + */ + public function test_a_value_that_is_not_encrypted_is_used_as_it_is( $stored_value, $expected ) { + $this->assertSame( $expected, self::secret()->decrypt( $stored_value ) ); + } + + /** + * @return iterable + */ + public static function provide_stored_values() { + yield 'a secret saved by hand' => array( 'plain-secret', 'plain-secret' ); + yield 'surrounding whitespace' => array( " plain-secret \n", 'plain-secret' ); + yield 'nothing saved yet' => array( '', '' ); + yield 'blanks only' => array( ' ', '' ); + yield 'base64 that is not a secret' => array( 'cGxhaW4tc2VjcmV0', 'cGxhaW4tc2VjcmV0' ); + } +} diff --git a/tests/Unit/Subscription/StatusChangeTest.php b/tests/Unit/Subscription/StatusChangeTest.php new file mode 100644 index 0000000..50530e0 --- /dev/null +++ b/tests/Unit/Subscription/StatusChangeTest.php @@ -0,0 +1,58 @@ +assertNull( $strings ); + + return; + } + + $this->assertSame( array( 'question', 'confirm', 'dismiss' ), array_keys( $strings ) ); + $this->assertNotSame( '', trim( $strings['question'] ) ); + $this->assertNotSame( '', trim( $strings['confirm'] ) ); + $this->assertNotSame( '', trim( $strings['dismiss'] ) ); + } + + /** + * @return iterable + */ + public static function provide_statuses() { + yield 'cancelling asks for confirmation' => array( 'cancelled', true ); + yield 'reactivating asks for confirmation' => array( 'active', true ); + yield 'on hold goes straight through' => array( 'on-hold', false ); + yield 'pending cancel goes straight through' => array( 'pending-cancel', false ); + yield 'an unknown status goes through' => array( 'whatever', false ); + yield 'no status at all goes through' => array( '', false ); + } + + public function test_cancelling_and_reactivating_do_not_share_strings() { + $cancel = StatusChange::confirmation_strings( 'cancelled' ); + $reactivate = StatusChange::confirmation_strings( 'active' ); + + $this->assertNotSame( $cancel['question'], $reactivate['question'] ); + $this->assertNotSame( $cancel['confirm'], $reactivate['confirm'] ); + } +} diff --git a/tests/bootstrap-integration.php b/tests/bootstrap-integration.php new file mode 100644 index 0000000..e513e07 --- /dev/null +++ b/tests/bootstrap-integration.php @@ -0,0 +1,63 @@ +