From 3b6d19912be978a1ee1054598a80f0a2ce61345a Mon Sep 17 00:00:00 2001 From: "Xunnamius (Romulus)" Date: Fri, 14 Jun 2024 18:08:42 -0700 Subject: [PATCH] refactor: begin with elections_irv baseline --- .codecov.yml | 2 + .editorconfig | 9 + .env.default | 139 + .eslintrc.js | 260 + .gitattributes | 1 + .github/CODEOWNERS | 3 + .github/CODE_OF_CONDUCT.md | 82 + .github/ISSUE_TEMPLATE/BUG_REPORT.md | 77 + .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 26 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/PULL_REQUEST_TEMPLATE.md | 15 + .github/SUPPORT.md | 61 + .github/dependabot.yml | 21 + .github/pipeline.config.js | 60 + .github/workflows/README.md | 6 + .husky/.gitignore | 1 + .husky/commit-msg | 7 + .husky/pre-commit | 5 + .ncurc.js | 26 + .prettierignore | 13 + .remarkrc.mjs | 93 + .spellcheckignore | 0 .vercelignore | 22 + CHANGELOG.md | 10 + CONTRIBUTING.md | 99 + LICENSE | 21 + README.md | 3 + SECURITY.md | 17 + babel.config.js | 107 + commitlint.config.js | 28 + conventional.config.js | 4 + expect-env.js | 276 + external-scripts/README.md | 8 + external-scripts/ban-hammer.ts | 290 + external-scripts/initialize-data/crypto.ts | 58 + external-scripts/initialize-data/index.ts | 214 + .../initialize-data/question-generator.ts | 184 + external-scripts/log-stats.ts | 696 + external-scripts/prune-data.ts | 266 + jest.config.js | 51 + lib/README.md | 5 + lib/debug-extended/index.ts | 47 + lib/debug-extended/package.json | 3 + lib/debug-extended/unit.test.ts | 38 + lib/find-project-root/index.ts | 33 + lib/find-project-root/package.json | 4 + lib/find-project-root/unit.test.ts | 59 + lib/is-plain-object/index.ts | 5 + lib/is-plain-object/package.json | 3 + lib/jest-expect-matching-errors/index.ts | 50 + lib/jest-expect-matching-errors/package.json | 3 + lib/jest-mock-date/index.ts | 21 + lib/jest-mock-date/package.json | 3 + lib/jest-mongo-object-id-pseudo-sort/index.ts | 16 + .../package.json | 3 + lib/json-node-fetch/index.ts | 368 + lib/json-node-fetch/package.json | 3 + lib/json-node-fetch/test/integration.test.ts | 180 + lib/json-node-fetch/test/unit.test.ts | 317 + lib/json-unfetch/index.ts | 456 + lib/json-unfetch/package.json | 3 + lib/json-unfetch/test/integration.test.ts | 188 + lib/json-unfetch/test/unit.test.ts | 383 + lib/mongo-common/index.ts | 182 + lib/mongo-common/package.json | 3 + lib/mongo-common/unit.test.ts | 59 + lib/mongo-item/index.ts | 248 + lib/mongo-item/package.json | 3 + lib/mongo-item/unit.test.ts | 249 + lib/mongo-schema/index.ts | 319 + lib/mongo-schema/package.json | 3 + lib/mongo-schema/unit.test.ts | 347 + lib/mongo-test/index.ts | 262 + lib/mongo-test/package.json | 3 + lib/mongo-test/test/integration.test.ts | 115 + lib/mongo-test/test/unit.test.ts | 707 + lib/next-adhesive/add-raw-body.ts | 169 + lib/next-adhesive/auth-request.ts | 118 + lib/next-adhesive/check-content-type.ts | 179 + lib/next-adhesive/check-method.ts | 49 + lib/next-adhesive/check-version.ts | 37 + lib/next-adhesive/contrive-error.ts | 39 + lib/next-adhesive/handle-error.ts | 126 + lib/next-adhesive/limit-request.ts | 42 + lib/next-adhesive/log-request.ts | 45 + lib/next-adhesive/package.json | 3 + .../test/unit-add-raw-body.test.ts | 305 + .../test/unit-auth-request.test.ts | 228 + .../test/unit-check-content-type.test.ts | 719 + .../test/unit-check-method.test.ts | 173 + .../test/unit-check-version.test.ts | 174 + .../test/unit-contrive-error.test.ts | 53 + .../test/unit-handle-error.test.ts | 183 + .../test/unit-limit-request.test.ts | 158 + .../test/unit-log-request.test.ts | 104 + lib/next-adhesive/test/unit-use-cors.test.ts | 79 + lib/next-adhesive/use-cors.ts | 36 + lib/next-api-glue/index.ts | 385 + lib/next-api-glue/package.json | 3 + lib/next-api-glue/unit.test.ts | 1155 + lib/next-api-respond/index.ts | 206 + lib/next-api-respond/package.json | 3 + lib/next-api-respond/unit.test.ts | 533 + lib/next-api-util/index.ts | 13 + lib/next-api-util/package.json | 3 + lib/next-auth/authenticate.ts | 55 + lib/next-auth/authorize.ts | 100 + lib/next-auth/constants.ts | 43 + lib/next-auth/db.ts | 103 + lib/next-auth/index.ts | 5 + lib/next-auth/package.json | 3 + lib/next-auth/test/unit-authenticate.test.ts | 105 + lib/next-auth/test/unit-authorize.test.ts | 156 + lib/next-auth/test/unit-db.test.ts | 37 + lib/next-auth/test/unit-index.test.ts | 187 + lib/next-auth/test/unit-token.test.ts | 1720 + lib/next-auth/token.ts | 665 + lib/next-contrived/index.ts | 33 + lib/next-contrived/package.json | 3 + lib/next-contrived/unit.test.ts | 139 + lib/next-env/index.ts | 184 + lib/next-env/package.json | 3 + lib/next-env/unit.test.ts | 178 + lib/next-limit/index.ts | 115 + lib/next-limit/package.json | 3 + lib/next-limit/unit.test.ts | 334 + lib/next-log/index.ts | 77 + lib/next-log/package.json | 3 + lib/next-log/unit.test.ts | 238 + lib/suppress-experimental-warnings/index.ts | 39 + .../package.json | 3 + lib/throttled-fetch/index.ts | 916 + lib/throttled-fetch/package.json | 3 + lib/throttled-fetch/unit.test.ts | 1564 + lint-staged.config.js | 13 + next.config.js | 49 + package-lock.json | 31417 ++++++++++++++++ package.json | 226 + prettier.config.js | 20 + public/.gitkeep | 0 release.config.js | 99 + spellcheck-commit.js | 116 + src/backend/api.ts | 44 + src/backend/db.ts | 234 + src/backend/env.ts | 78 + src/backend/index.ts | 475 + src/backend/middleware.ts | 115 + src/backend/validators.ts | 262 + src/constants.ts | 8 + src/error.ts | 118 + src/pages/_app.tsx | 19 + src/pages/api/[[...catchAllForNotFound]].ts | 36 + src/pages/api/sys/auth/[auth_id].ts | 46 + src/pages/api/sys/auth/index.ts | 31 + src/pages/api/sys/auth/search.ts | 50 + src/pages/api/sys/ping.ts | 22 + .../[election_id]/ballots/[voter_id].ts | 58 + .../elections/[election_id]/ballots/index.ts | 29 + .../api/v1/elections/[election_id]/index.ts | 45 + src/pages/api/v1/elections/index.ts | 45 + src/pages/api/v1/info/index.ts | 25 + src/pages/index.tsx | 45 + test/api/integration.test.ts | 248 + test/api/unit-app-elections.test.ts | 205 + test/api/unit-app-info.test.ts | 49 + test/api/unit-catchall.test.ts | 58 + test/api/unit-sys-auth.test.ts | 179 + test/api/unit-sys-limits.test.ts | 1 + test/api/unit-sys-logs.test.ts | 1 + test/api/unit-sys-ping.test.ts | 124 + test/backend/unit-backend.test.ts | 1549 + test/db.ts | 254 + test/externals/unit-ban.test.ts | 329 + test/externals/unit-initialize.test.ts | 68 + test/externals/unit-prune.test.ts | 245 + test/externals/unit-stats.test.ts | 64 + test/integration.ts | 1480 + test/pages/unit-app.test.tsx | 20 + test/pages/unit-index.test.tsx | 18 + test/setup.ts | 1111 + test/util.ts | 152 + tsconfig.docs.json | 4 + tsconfig.eslint.json | 4 + tsconfig.json | 58 + tsconfig.lint.json | 11 + tsconfig.types.json | 14 + types/global.ts | 20 + types/next__bundle-analyzer.d.ts | 1 + types/random-case.d.ts | 3 + types/unique-filename.d.ts | 7 + vercel.json | 3 + webpack.config.js | 198 + 192 files changed, 60494 insertions(+) create mode 100644 .codecov.yml create mode 100644 .editorconfig create mode 100644 .env.default create mode 100644 .eslintrc.js create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SUPPORT.md create mode 100644 .github/dependabot.yml create mode 100644 .github/pipeline.config.js create mode 100644 .github/workflows/README.md create mode 100644 .husky/.gitignore create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100644 .ncurc.js create mode 100644 .prettierignore create mode 100644 .remarkrc.mjs create mode 100644 .spellcheckignore create mode 100644 .vercelignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 babel.config.js create mode 100644 commitlint.config.js create mode 100644 conventional.config.js create mode 100644 expect-env.js create mode 100644 external-scripts/README.md create mode 100644 external-scripts/ban-hammer.ts create mode 100644 external-scripts/initialize-data/crypto.ts create mode 100644 external-scripts/initialize-data/index.ts create mode 100644 external-scripts/initialize-data/question-generator.ts create mode 100644 external-scripts/log-stats.ts create mode 100644 external-scripts/prune-data.ts create mode 100644 jest.config.js create mode 100644 lib/README.md create mode 100644 lib/debug-extended/index.ts create mode 100644 lib/debug-extended/package.json create mode 100644 lib/debug-extended/unit.test.ts create mode 100644 lib/find-project-root/index.ts create mode 100644 lib/find-project-root/package.json create mode 100644 lib/find-project-root/unit.test.ts create mode 100644 lib/is-plain-object/index.ts create mode 100644 lib/is-plain-object/package.json create mode 100644 lib/jest-expect-matching-errors/index.ts create mode 100644 lib/jest-expect-matching-errors/package.json create mode 100644 lib/jest-mock-date/index.ts create mode 100644 lib/jest-mock-date/package.json create mode 100644 lib/jest-mongo-object-id-pseudo-sort/index.ts create mode 100644 lib/jest-mongo-object-id-pseudo-sort/package.json create mode 100644 lib/json-node-fetch/index.ts create mode 100644 lib/json-node-fetch/package.json create mode 100644 lib/json-node-fetch/test/integration.test.ts create mode 100644 lib/json-node-fetch/test/unit.test.ts create mode 100644 lib/json-unfetch/index.ts create mode 100644 lib/json-unfetch/package.json create mode 100644 lib/json-unfetch/test/integration.test.ts create mode 100644 lib/json-unfetch/test/unit.test.ts create mode 100644 lib/mongo-common/index.ts create mode 100644 lib/mongo-common/package.json create mode 100644 lib/mongo-common/unit.test.ts create mode 100644 lib/mongo-item/index.ts create mode 100644 lib/mongo-item/package.json create mode 100644 lib/mongo-item/unit.test.ts create mode 100644 lib/mongo-schema/index.ts create mode 100644 lib/mongo-schema/package.json create mode 100644 lib/mongo-schema/unit.test.ts create mode 100644 lib/mongo-test/index.ts create mode 100644 lib/mongo-test/package.json create mode 100644 lib/mongo-test/test/integration.test.ts create mode 100644 lib/mongo-test/test/unit.test.ts create mode 100644 lib/next-adhesive/add-raw-body.ts create mode 100644 lib/next-adhesive/auth-request.ts create mode 100644 lib/next-adhesive/check-content-type.ts create mode 100644 lib/next-adhesive/check-method.ts create mode 100644 lib/next-adhesive/check-version.ts create mode 100644 lib/next-adhesive/contrive-error.ts create mode 100644 lib/next-adhesive/handle-error.ts create mode 100644 lib/next-adhesive/limit-request.ts create mode 100644 lib/next-adhesive/log-request.ts create mode 100644 lib/next-adhesive/package.json create mode 100644 lib/next-adhesive/test/unit-add-raw-body.test.ts create mode 100644 lib/next-adhesive/test/unit-auth-request.test.ts create mode 100644 lib/next-adhesive/test/unit-check-content-type.test.ts create mode 100644 lib/next-adhesive/test/unit-check-method.test.ts create mode 100644 lib/next-adhesive/test/unit-check-version.test.ts create mode 100644 lib/next-adhesive/test/unit-contrive-error.test.ts create mode 100644 lib/next-adhesive/test/unit-handle-error.test.ts create mode 100644 lib/next-adhesive/test/unit-limit-request.test.ts create mode 100644 lib/next-adhesive/test/unit-log-request.test.ts create mode 100644 lib/next-adhesive/test/unit-use-cors.test.ts create mode 100644 lib/next-adhesive/use-cors.ts create mode 100644 lib/next-api-glue/index.ts create mode 100644 lib/next-api-glue/package.json create mode 100644 lib/next-api-glue/unit.test.ts create mode 100644 lib/next-api-respond/index.ts create mode 100644 lib/next-api-respond/package.json create mode 100644 lib/next-api-respond/unit.test.ts create mode 100644 lib/next-api-util/index.ts create mode 100644 lib/next-api-util/package.json create mode 100644 lib/next-auth/authenticate.ts create mode 100644 lib/next-auth/authorize.ts create mode 100644 lib/next-auth/constants.ts create mode 100644 lib/next-auth/db.ts create mode 100644 lib/next-auth/index.ts create mode 100644 lib/next-auth/package.json create mode 100644 lib/next-auth/test/unit-authenticate.test.ts create mode 100644 lib/next-auth/test/unit-authorize.test.ts create mode 100644 lib/next-auth/test/unit-db.test.ts create mode 100644 lib/next-auth/test/unit-index.test.ts create mode 100644 lib/next-auth/test/unit-token.test.ts create mode 100644 lib/next-auth/token.ts create mode 100644 lib/next-contrived/index.ts create mode 100644 lib/next-contrived/package.json create mode 100644 lib/next-contrived/unit.test.ts create mode 100644 lib/next-env/index.ts create mode 100644 lib/next-env/package.json create mode 100644 lib/next-env/unit.test.ts create mode 100644 lib/next-limit/index.ts create mode 100644 lib/next-limit/package.json create mode 100644 lib/next-limit/unit.test.ts create mode 100644 lib/next-log/index.ts create mode 100644 lib/next-log/package.json create mode 100644 lib/next-log/unit.test.ts create mode 100644 lib/suppress-experimental-warnings/index.ts create mode 100644 lib/suppress-experimental-warnings/package.json create mode 100644 lib/throttled-fetch/index.ts create mode 100644 lib/throttled-fetch/package.json create mode 100644 lib/throttled-fetch/unit.test.ts create mode 100644 lint-staged.config.js create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prettier.config.js create mode 100644 public/.gitkeep create mode 100644 release.config.js create mode 100644 spellcheck-commit.js create mode 100644 src/backend/api.ts create mode 100644 src/backend/db.ts create mode 100644 src/backend/env.ts create mode 100644 src/backend/index.ts create mode 100644 src/backend/middleware.ts create mode 100644 src/backend/validators.ts create mode 100644 src/constants.ts create mode 100644 src/error.ts create mode 100644 src/pages/_app.tsx create mode 100644 src/pages/api/[[...catchAllForNotFound]].ts create mode 100644 src/pages/api/sys/auth/[auth_id].ts create mode 100644 src/pages/api/sys/auth/index.ts create mode 100644 src/pages/api/sys/auth/search.ts create mode 100644 src/pages/api/sys/ping.ts create mode 100644 src/pages/api/v1/elections/[election_id]/ballots/[voter_id].ts create mode 100644 src/pages/api/v1/elections/[election_id]/ballots/index.ts create mode 100644 src/pages/api/v1/elections/[election_id]/index.ts create mode 100644 src/pages/api/v1/elections/index.ts create mode 100644 src/pages/api/v1/info/index.ts create mode 100644 src/pages/index.tsx create mode 100644 test/api/integration.test.ts create mode 100644 test/api/unit-app-elections.test.ts create mode 100644 test/api/unit-app-info.test.ts create mode 100644 test/api/unit-catchall.test.ts create mode 100644 test/api/unit-sys-auth.test.ts create mode 100644 test/api/unit-sys-limits.test.ts create mode 100644 test/api/unit-sys-logs.test.ts create mode 100644 test/api/unit-sys-ping.test.ts create mode 100644 test/backend/unit-backend.test.ts create mode 100644 test/db.ts create mode 100644 test/externals/unit-ban.test.ts create mode 100644 test/externals/unit-initialize.test.ts create mode 100644 test/externals/unit-prune.test.ts create mode 100644 test/externals/unit-stats.test.ts create mode 100644 test/integration.ts create mode 100644 test/pages/unit-app.test.tsx create mode 100644 test/pages/unit-index.test.tsx create mode 100644 test/setup.ts create mode 100644 test/util.ts create mode 100644 tsconfig.docs.json create mode 100644 tsconfig.eslint.json create mode 100644 tsconfig.json create mode 100644 tsconfig.lint.json create mode 100644 tsconfig.types.json create mode 100644 types/global.ts create mode 100644 types/next__bundle-analyzer.d.ts create mode 100644 types/random-case.d.ts create mode 100644 types/unique-filename.d.ts create mode 100644 vercel.json create mode 100644 webpack.config.js diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..16b82b1 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +coverage: + range: '75...100' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4960585 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.env.default b/.env.default new file mode 100644 index 0000000..be86136 --- /dev/null +++ b/.env.default @@ -0,0 +1,139 @@ +### GLOBAL VARIABLES ### + +# If !false, Next's bundle(s) will be analyzed and report files generated. +ANALYZE=false + +# This will overwrite the NODE_ENV setting during runtime and for the compiled +# applications. +# +# Recognized values: test development production +# Default value: empty +NODE_ENV= + +# MongoDB connect URI. Specify auth credentials if necessary. YOU MUST *NOT* +# SPECIFY A DATABASE AT THE END! +MONGODB_URI=mongodb://127.0.0.1:27017 + +# Dedicated port to be used by the MongoDB Memory Server during unit tests. +# Especially useful when stepping through code, since you can always access the +# db at `mongodb://127.0.0.1:MONGODB_MS_PORT` when the debugger is paused. +# Tip: call `jest.setTimeout()` with a large number (i.e. 10**6) to ensure the +# MongoClient isn't closed randomly leading to strange errors. +# +# Leave this blank to choose any random port (not recommended). Note: this +# option is also used when Node is started in debug mode, e.g. `node +# --inspect-brk` or `node --debug`, or if the debugger is attached before the +# database connection is memoized. +MONGODB_MS_PORT=6666 + +# Determines the maximum allowed character length of an *entire* HTTP +# Authorization header. The default is 500. +AUTH_HEADER_MAX_LENGTH=500 + +# Controls which versions of the API will respond to requests. Examples (disable +# v1; disable v1 and v2; disable v3, v5, and v7): +# DISABLED_API_VERSIONS=1 +# DISABLED_API_VERSIONS=1,2 +# DISABLED_API_VERSIONS=3,5,7 +# +# Note that `DISABLED_API_VERSIONS=` (i.e. empty) means no +# versions are disabled! +DISABLED_API_VERSIONS= + +# Determines the number of items returned by paginated endpoints. +RESULTS_PER_PAGE=100 + +# If !false, all rate limits and exponential soft banning will be ignored. +IGNORE_RATE_LIMITS=false + +# If !false, no one will be able to use the API. +LOCKOUT_ALL_CLIENTS=false + +# Controls what request methods are allowed. Empty means all are allowed +# (default). +# +# Example, to make API read-only: +# DISALLOWED_METHODS=POST,PUT +DISALLOWED_METHODS= + +# Every Nth request will be be cancelled and an HTTP 555 response returned. Note +# that, in addition to every Nth request, the very first request sent to the API +# will also return a contrived error. Set to 0 to disable all contrived errors. +REQUESTS_PER_CONTRIVED_ERROR=10 + +# Maximum allowed size of a request body (and content-length header value) in +# bytes. Should be a string like 1kb, 1mb, 500b. +MAX_CONTENT_LENGTH_BYTES=10kb + +# Maximum number of parameters that can be passed to endpoints that accept +# multiple slash parameters. +MAX_PARAMS_PER_REQUEST=100 + +# Minimum allowed ballots per election. +MAX_BALLOTS_PER_ELECTION=100 + +# Minimum allowed string length of an election title. +MIN_ELECTION_TITLE_LENGTH=4 + +# Maximum allowed string length of an election title. +MAX_ELECTION_TITLE_LENGTH=72 + +# Maximum allowed string length of an election description. +MAX_ELECTION_DESC_LENGTH=200 + +# Maximum allowed length of an election's options array. +MAX_ELECTION_OPTIONS_ITEMS=50 + +# Maximum allowed string length of a single election option / ballot ranking key. +MAX_ELECTION_OPTION_LENGTH=30 + +# Maximum allowed string length of a ballot voter_id. +MAX_VOTERID_LENGTH=24 + + +### EXTERNAL SCRIPT VARIABLES ### +# (optional unless using the relevant external script) + +# How often this script is going to be invoked. This doesn't determine anything +# automatically on its own, this is useful to ensure the script works no matter +# how often you decide to call it. +BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS=60 + +# The maximum number of requests per BAN_HAMMER_RESOLUTION_WINDOW_SECONDS +# allowed by a single client. +BAN_HAMMER_MAX_REQUESTS_PER_WINDOW=10 + +# How far back into the past this script looks when checking a key or ip against +# BAN_HAMMER_MAX_REQUESTS_PER_WINDOW. +BAN_HAMMER_RESOLUTION_WINDOW_SECONDS=1 + +# The initial amount of time an offender is banned. +BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES=1 + +# When an offender is banned twice in the same "period," they're banned for +# BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES * BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER +# minutes instead of the default. This is also the length of the "period". +BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER=2 + +# The size (in bytes) of the root request-log collection will not be allowed to +# exceed this amount. Oldest entries are deleted first. Should be a string like +# 1kb, 1mb, 500b. +PRUNE_DATA_MAX_LOGS_BYTES=100mb + +# The size (in bytes) of the root limited-log collection will not be allowed to +# exceed this amount. Oldest entries are deleted first. Should be a string like +# 1kb, 1mb, 500b. +PRUNE_DATA_MAX_BANNED_BYTES=10mb + +# The size (in bytes) of the articles collection will not be allowed to exceed +# this amount. Oldest entries are deleted first. Should be a string like 1kb, +# 1mb, 500b. +PRUNE_DATA_MAX_ELECTIONS_BYTES=200mb + +# The size (in bytes) of the opportunities collection will not be allowed to +# exceed this amount. Oldest entries are deleted first. Should be a string like +# 1kb, 1mb, 500b. +PRUNE_DATA_MAX_BALLOTS_BYTES=120mb + +### TOOLS FRONTEND VARIABLES ### +# (optional unless using tools) diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..52c23c9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,260 @@ +'use strict'; + +const debug = require('debug')(`${require('./package.json').name}:eslint-config`); +const restrictedGlobals = require('confusing-browser-globals'); + +const plugins = ['unicorn', '@typescript-eslint', 'import']; + +const xtends = [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + 'plugin:unicorn/recommended', + 'plugin:@next/next/recommended' +]; + +const environment = { + es2022: true, + node: true + // * Instead of including more options here, enable them on a per-file basis +}; + +const rules = { + 'no-console': 'warn', + 'no-return-await': 'warn', + 'no-await-in-loop': 'warn', + 'import/no-unresolved': ['error', { commonjs: true }], + 'no-restricted-globals': ['warn'].concat(restrictedGlobals), + 'no-extra-boolean-cast': 'off', + 'no-empty': 'off', + '@typescript-eslint/camelcase': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/prefer-ts-expect-error': 'warn', + '@typescript-eslint/no-floating-promises': [ + 'error', + { ignoreVoid: true, ignoreIIFE: true } + ], + '@typescript-eslint/ban-ts-comment': [ + 'warn', + { + 'ts-expect-error': 'allow-with-description', + minimumDescriptionLength: 6 + } + ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_+', + varsIgnorePattern: '^_+', + caughtErrorsIgnorePattern: String.raw`^ignored?\d*$`, + caughtErrors: 'all' + } + ], + // ? Ever since v4, we will rely on TypeScript to catch these + 'no-undef': 'off', + '@typescript-eslint/no-var-requires': 'off', + // ? I'll be good, I promise + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false, fixStyle: 'inline-type-imports' } + ], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: true } + ], + 'no-unused-vars': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/no-keyword-prefix': 'off', + 'unicorn/prefer-string-replace-all': 'warn', + // ? Handled by integration tests + 'unicorn/prefer-module': 'off', + // ? I am of the opinion that there is a difference between something being + // ? defined as nothing and something being undefined, such as in JSON. + // ? Usually, though, I try to avoid null, but I'd rather do that on a case by + // ? case basis. + 'unicorn/no-null': 'off', + // ? Ensure files have the correct case... except for a few + 'unicorn/filename-case': [ + 'error', + { + case: 'kebabCase', + ignore: ['next__bundle-analyzer.d.ts'] + } + ], + // ? If MongoDB can get away with "DB" in its name, so can we. Also, + // ? unnecessary underscores are a big no-no. + 'unicorn/prevent-abbreviations': [ + 'warn', + { + checkFilenames: false, + replacements: { + args: false, + str: false, + fn: false, + db: false, + dir: false, + dist: false, + tmp: false, + pkg: false, + src: false, + dest: false, + obj: false, + val: false, + env: false, + temp: false, + req: false, + res: false, + prop: false, + props: false, + params: false, + lib: false, + param: false, + num: false + }, + ignore: [/stderr/i] + } + ], + // ? Actually, I rather like this curt syntax + 'unicorn/no-await-expression-member': 'off', + // ? Between disabling this and disabling no-empty-function, I choose this + 'unicorn/no-useless-undefined': 'off', + // ? Not sure why this isn't the default + 'unicorn/prefer-export-from': ['warn', { ignoreUsedVariables: true }], + // ? Yeah, I read The Good Parts too, I know what I'm doing + 'unicorn/consistent-function-scoping': 'off', + // ? It's 2022. Use Prettier + 'unicorn/no-nested-ternary': 'off', + // ? `Array.from` communicates intent much better than `[...]` + 'unicorn/prefer-spread': 'off', + // ? Not realistic when using TypeScript + 'unicorn/prefer-native-coercion-functions': 'off', + // ? Premature optimization is evil + 'unicorn/no-array-for-each': 'off', + // ? Lol, no + 'unicorn/explicit-length-check': 'off', + // ? I don't think so + 'unicorn/no-negated-condition': 'off', + // ? This is not it, chief (Prettier prevails) + 'unicorn/number-literal-case': 'off', + // ? I'll decide when I want switch cases for fallthrough or not, thanks + 'unicorn/prefer-switch': 'off', + // ? Unicorn isn't smart enough to make this work in a way that's useful + 'unicorn/import-style': 'off' +}; + +module.exports = { + parser: '@typescript-eslint/parser', + plugins, + extends: xtends, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + impliedStrict: true, + jsx: true + }, + project: 'tsconfig.eslint.json' + }, + env: environment, + rules, + overrides: [ + { + files: ['*.test.*'], + plugins: [...plugins, 'jest'], + env: { ...environment, jest: true }, + extends: [ + ...xtends, + 'plugin:jest/all', + 'plugin:jest/style', + 'plugin:jest-dom/recommended' + ], + rules: { + ...rules, + 'jest/lowercase': 'off', + 'jest/consistent-test-it': 'off', + 'jest/require-top-level-describe': 'off', + 'jest/valid-describe': 'off', + 'jest/no-hooks': 'off', + 'jest/require-to-throw-message': 'off', + 'jest/prefer-called-with': 'off', + 'jest/prefer-spy-on': 'off', + 'jest/no-if': 'off', + 'jest/no-disabled-tests': 'warn', + 'jest/no-commented-out-tests': 'warn', + 'jest/require-hook': 'off', + 'jest/no-alias-methods': 'off', + 'jest/max-expects': 'off', + 'jest/prefer-mock-promise-shorthand': 'off', + 'jest/no-conditional-in-test': 'off', + 'jest/no-conditional-expect': 'off', + 'jest/prefer-each': 'off', + 'jest/prefer-snapshot-hint': 'off', + 'jest/no-untyped-mock-factory': 'warn', + 'jest/prefer-importing-jest-globals': 'off' + } + } + ], + settings: { + react: { + version: 'detect' + }, + 'import/extensions': ['.ts', '.tsx', '.js', '.jsx'], + // ? Switch parsers depending on which type of file we're looking at + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx', '.cts', '.mts'], + '@babel/eslint-parser': ['.js', '.jsx', '.cjs', '.mjs'] + }, + 'import/resolver': { + node: {}, + alias: { + map: [ + // ! If changed, also update these aliases in tsconfig.json, + // ! webpack.config.js, next.config.ts, and jest.config.js + ['universe', './src'], + ['multiverse', './lib'], + ['testverse', './test'], + ['externals', './external-scripts'], + ['types', './types'], + ['package', './package.json'], + // ? These are used at various points (including at compile time by + // ? Next.js) to get mongo schema configuration and/or test dummy data. + // ! Must be defined if using @xunnamius/mongo-schema + ['configverse/get-schema-config', './src/backend/db.ts'], + // ! Must be defined if using @xunnamius/mongo-test + ['configverse/get-dummy-data', './test/db.ts'] + ], + extensions: ['.js', '.jsx', '.ts', '.tsx'] + }, + typescript: {} + }, + 'import/ignore': [ + // ? Don't go complaining about anything that we don't own + '.*/node_modules/.*', + '.*/bin/.*' + ] + }, + ignorePatterns: [ + 'coverage', + 'dist', + 'test/fixtures', + '__fixtures__', + '__snapshots__', + 'test/integration/assets', + 'bin', + 'build', + '/next.config.js' + ], + globals: { + page: true, + browser: true, + context: true, + jestPuppeteer: true + } +}; + +debug('exports: %O', module.exports); diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7557ad6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# See https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners + +* @Xunnamius diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..34c123e --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,82 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socioeconomic status, nationality, +personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language. +- Being respectful of differing viewpoints and experiences. +- Gracefully accepting constructive criticism. +- Focusing on what is best for the community. +- Showing empathy towards other community members. + +Examples of unacceptable behavior by participants include: + +- Racism or sexism in any shape or form. +- The use of sexualized language or imagery and unwelcome sexual attention or + advances. +- Trolling, insulting/derogatory comments, and personal or political attacks. +- Public or private harassment. +- Publishing others' private information, such as a physical or electronic + address, without explicit permission. +- Other conduct which could reasonably be considered inappropriate in a + professional setting. + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at +[Xunnamius@users.noreply.github.com][1]. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +[https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][2] + +For answers to common questions about this code of conduct, see +[https://www.contributor-covenant.org/faq][3] + +[homepage]: https://www.contributor-covenant.org +[1]: mailto:Xunnamius@users.noreply.github.com +[2]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +[3]: https://www.contributor-covenant.org/faq diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..a868650 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,77 @@ +--- +name: 🀯 Bug report +about: Alert us about an issue +labels: bug +--- + + + +
The problem + + + +
+ +
Reproduction steps + + + +
+ + + + + + diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..84fa226 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,26 @@ +--- +name: 🀩 Feature request +about: Tell us about your awesome idea +labels: enhancement +--- + + + + + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1ffe6a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security Channel + url: 'mailto:security@ergodark.com?subject=SECURITY%20INCIDENT%3A%20%28five%20word%20summary%29' + about: Report security-related issues here! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5a2370b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + + + + + +--- + + + +- [ ] I have read **[CONTRIBUTING.md][1]**. +- [ ] This PR is not security related (see [SECURITY.md][2]). + +[1]: /CONTRIBUTING.md +[2]: /SECURITY.md diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..8de758a --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,61 @@ +# Support [![Average issue resolution time][badge-issue-resolution]][link-issue-resolution] [![Issue open percentage][badge-issue-percentage]][open-issues] + +Need help? Want to help this project? See below! + +## Reporting an Issue + +Before you begin, please [search open and past issues][open-issues] to see if +your issue or feature request is already being talked about. + +If you find your issue already exists, make sure to [bump the issue by adding a +reaction][github-blog]. **If you're serious about wanting an issue to get +attention, use a reaction in place of a "+1" or other similar comment.** + +If you cannot find an existing issue that describes your bug or feature, then +you're clear to create your issue using the guidelines below. + +### Standard Reactions + +Analysis of issue popularity (by tooling or otherwise) relies on the following +reaction emojis being present on issues, PRs, and replies: + +`:+1:` πŸ‘πŸΏπŸ‘πŸΎπŸ‘πŸ½πŸ‘πŸΌπŸ‘πŸ»πŸ‘ β€” up vote (AKA: _approve_, _like_, _+1_) + +`:-1:` πŸ‘ŽπŸΏπŸ‘ŽπŸΎπŸ‘ŽπŸ½πŸ‘ŽπŸΌπŸ‘ŽπŸ»πŸ‘Ž β€” down vote (AKA: _disapprove_, _dislike_, _-1_) + +### Bug Reports and Feature Requests + +For the timeliest resolution to bug reports specifically, please make a good +faith effort to [follow the template][choose-new-issue]. The more quality +information you can provide, the quicker a fix will be found. Additionally: + +- Ensure you file one issue per problem or feature request. +- Do not enumerate multiple bugs or feature requests in the same issue. +- Do not add your issue as a comment to an existing issue unless you're + experiencing identical behavior or behavior that stems from the exact same + problem. + +That's it! You are now ready to [submit a new issue][choose-new-issue]! Thank +you for contribution πŸŽ‰, your efforts are greatly appreciated! πŸ™ŒπŸΏ + +#### Closure Policy + +- Issues that do not follow the appropriate template or contain necessary + information may be closed immediately and the poster directed to these + guidelines. +- Issues that go for an extended period without activity are subject to closure. + +[badge-issue-percentage]: + https://isitmaintained.com/badge/open/nhscc/elections_irv.api.hscc.bdpa.org.svg + 'Open issues as a percentage of all issues' +[badge-issue-resolution]: + https://isitmaintained.com/badge/resolution/nhscc/elections_irv.api.hscc.bdpa.org.svg + 'Average time to resolve an issue' +[choose-new-issue]: + https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/issues/new/choose +[github-blog]: + https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments +[link-issue-resolution]: + https://isitmaintained.com/project/nhscc/elections_irv.api.hscc.bdpa.org +[open-issues]: + https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/issues?q= diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..26d18ee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + commit-message: + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + target-branch: 'canary' + + - package-ecosystem: npm + directory: / + schedule: + interval: daily + commit-message: + prefix: 'build' + prefix-development: 'chore' + include: 'scope' + target-branch: 'canary' diff --git a/.github/pipeline.config.js b/.github/pipeline.config.js new file mode 100644 index 0000000..330ac3b --- /dev/null +++ b/.github/pipeline.config.js @@ -0,0 +1,60 @@ +/** + * This object is used to configure the GitHub Actions that comprise the + * build-test-deploy pipeline. Each property is optional. + */ +module.exports = { + // * The name and email used to author commits and interact with GitHub. + // ! This should correspond to the identity of the GH_TOKEN secret. + // committer: { + // name: 'xunn-bot', + // email: 'bot@xunn.io' + // }, + // + // * Selectively enable debugger verbose output in the pipeline. + // ? To enable debugging across all source in this repo (excluding + // ? node_modules) without having to type the package name, use a boolean: + // debugString: true, // false is treated the same as null/commented out + // ? Or you can type out a custom debug namespace string instead, e.g.: + // debugString: '@your-namespace/some-package:*', + // ? This key can only appear in local pipeline config and not global. + // ? See also: https://www.npmjs.com/package/debug#wildcards + // ? For even more debugging tools, see: https://bit.ly/2R6NAdZ + // + // * The semver version of node to install and setup before each job. + // nodeCurrentVersion: '18.x', + // + // * Node semver versions to run unit and integration tests against. + // nodeTestVersions: ['14.x', '16.x', '18.x'], + // + // * Webpack semver versions to run unit and integration tests against. + // webpackTestVersions: ['5.x'], + // + // * Regular expressions for skipping CI/CD. To skip CL, use git with the + // * `--no-verify` option. + // ciSkipRegex: /\[skip ci\]|\[ci skip\]/i, + // cdSkipRegex: /\[skip cd\]|\[cd skip\]/i, + // + // * Should auto-merge be retried on failure even when the PR appears + // * unmergeable? If `true`, uses exponential back-off internally. + // ! WARNING: leaving this as `true` might waste Actions minutes and $$$ in + // ! private repositories. Watch your usage numbers! + // canRetryAutomerge: true, + // + // * Npm audit will fail upon encountering problems of at least the specified + // * severity. + // npmAuditFailLevel: 'high', + // + // * Attempt to upload project coverage data to codecov if `true`. + // canUploadCoverage: true, + // + // * The maximum amount of time in seconds any "retry"-type operation can + // * continue retrying. This includes all exponential backoff steps. + // ! A 5 minute limit is hardcoded into pipeline workflows, so values above + // ! ~250 might lead to undesirable VM hard stops. + // retryCeilingSeconds: 180, + // + // * How many days GitHub should keep uploaded artifacts around. + // ! 90 days is GitHub's default, but this should be lowered to 1 day for + // ! private repos where artifact storage costs $$$. + // artifactRetentionDays: 90 +}; diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..2b6db42 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,6 @@ +Note: the `build-test`, `deploy`, `cleanup`, and `post-release-check` workflows +are part of the [Projector][1] pipeline. Details on pipeline design and +operation can be found [here][2]. + +[1]: https://github.com/Xunnamius/projector +[2]: https://github.com/Xunnamius/projector-pipeline diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..62eaff5 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,7 @@ +#!/bin/sh +. "$(dirname $0)/_/husky.sh" + +npx commitlint -e +if [ -z $GAC_VERIFY_SIMPLE ]; then npm run test; fi +echo +node spellcheck-commit.js diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..86c234d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname $0)/_/husky.sh" + +if [ -z $GAC_VERIFY_SIMPLE ]; then npm run lint; fi +NODE_ENV=format npx lint-staged --concurrent false diff --git a/.ncurc.js b/.ncurc.js new file mode 100644 index 0000000..23fad5c --- /dev/null +++ b/.ncurc.js @@ -0,0 +1,26 @@ +// * https://www.npmjs.com/package/npm-check-updates#configuration-files + +// TODO: remove outdated deps and exceptions (below) when externalizing libs to +// TODO: packages + +module.exports = { + reject: [ + // ? Pin the CJS version of execa + 'execa', + // ? Pin the CJS version of node-fetch (and its types) + 'node-fetch', + '@types/node-fetch', + // ? Pin the CJS version of find-up + 'find-up', + // ? Pin the CJS version of chalk + 'chalk', + // ? Pin the CJS version of auto-bind + 'auto-bind', + // ? Pin the CJS version of inquirer + 'inquirer', + // ? Pin the non-busted version of unfetch + 'unfetch', + // ? Pin the CJS version of text-extensions + 'text-extensions' + ] +}; diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0603a70 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,13 @@ +# Paths below are ignored by prettier as well as remark and doctoc when called +# with npm run format) +build +external-scripts/bin +node_modules +dist +coverage +package-lock.json +test/fixtures +docs +CHANGELOG.md +bundle-stats.ignore.json +external-scripts/*.json diff --git a/.remarkrc.mjs b/.remarkrc.mjs new file mode 100644 index 0000000..04a6c33 --- /dev/null +++ b/.remarkrc.mjs @@ -0,0 +1,93 @@ +/** + * @typedef {{settings?: import('mdast-util-to-markdown').Options, plugins?: + * import('unified-engine/lib/configuration').PluggableList | + * import('unified-engine/lib/configuration').PluginIdList}} Config + */ + +/** + * Remark configuration loaded when `NODE_ENV === 'lint'`. The goal here is to + * check for things that will not be corrected by prettier or remark during a + * formatting pass (see below). + * + * @type {Config} + */ +const lintConfig = { + plugins: [ + 'ignore', + 'frontmatter', + 'gfm', + 'lint', + 'lint-definition-case', + 'lint-fenced-code-flag', + 'lint-fenced-code-flag-case', + 'lint-file-extension', + 'lint-first-heading-level', + 'lint-heading-increment', + 'lint-heading-whitespace', + 'lint-list-item-style', + 'lint-no-duplicate-defined-urls', + 'lint-no-duplicate-headings-in-section', + 'lint-no-empty-sections', + 'lint-no-empty-url', + 'lint-heading-word-length', + 'lint-no-heading-like-paragraph', + 'lint-no-heading-punctuation', + 'lint-no-inline-padding', + 'lint-no-literal-urls', + 'lint-no-multiple-toplevel-headings', + 'lint-no-reference-like-url', + 'lint-no-shell-dollars', + 'lint-no-shortcut-reference-image', + 'lint-no-shortcut-reference-link', + 'lint-no-tabs', + 'lint-no-undefined-references', + 'lint-ordered-list-marker-value', + ['lint-strikethrough-marker', '~~'], + // ? Prettier will reformat list markers UNLESS they precede checkboxes + ['lint-unordered-list-marker-style', '-'], + 'validate-links' + ] +}; + +/** + * Remark configuration loaded when `NODE_ENV === 'format'`. The goal here is to + * correct things that will not be taken care of by prettier. + * + * @type {Config} + */ +const formatConfig = { + plugins: [ + 'ignore', + 'frontmatter', + 'gfm', + 'tight-comments', + ['capitalize-headings', { excludeHeadingLevel: { h1: true } }], + 'remove-unused-definitions', + 'remove-url-trailing-slash', + 'renumber-references', + 'sort-definitions' + ] +}; + +if (!['lint', 'format'].includes(process.env.NODE_ENV)) { + throw new Error('remark expects NODE_ENV to be one of either: lint, format'); +} + +/** + * @type {Config} + */ +export default { + settings: { + bullet: '-', + emphasis: '_', + fences: true, + listItemIndent: 'one', + rule: '-', + strong: '*', + tightDefinitions: true, + ...(process.env.NODE_ENV === 'lint' ? lintConfig.settings : formatConfig.settings) + }, + plugins: [ + ...(process.env.NODE_ENV === 'lint' ? lintConfig.plugins : formatConfig.plugins) + ] +}; diff --git a/.spellcheckignore b/.spellcheckignore new file mode 100644 index 0000000..e69de29 diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..2cecbe8 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,22 @@ +# This file's syntax is only KINDA similar like .gitignore... + +# Ignore all root-level files +/* + +# Re-add root-level dirs +!/data +!/lib +!/public +!/src +!/types + +# Re-add root-level files +!/.gitignore +!/babel.config.js +!/next.config.js +!/package-lock.json +!/package.json +!/tsconfig.json + +# Custom adds +!/expect-env.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4696bfa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this auto-generated +file. The format is based on [Conventional Commits][1]; this project adheres to +[Semantic Versioning][2]. + +## 1.2.0 (2023-05-27) + +[1]: https://conventionalcommits.org +[2]: https://semver.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a071dc0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing + +Hi there! First off, we're thrilled 🀩 you want contribute to this project! + +First time contributor to a GitHub project? If you could use some help getting +started, [take a look at this quick and easy guide][how-to-contribute]. πŸ’œ + +## Briefly: Submitting a Pull Request (Pr) + +> See also: [CODE_OF_CONDUCT.md][code-of-conduct] + +This repository uses a [fully automated][github-actions] [continuous +linting][husky-cl] (CL), integration testing (CI), and deployment (CD) +[projector][projector]-based pipeline for integrating PRs and publishing +releases. The neat thing about a fully automated pipeline is that anyone +anywhere can make a contribution quickly and with minimal tedium. + +This repository makes extensive use of [debug][pkg-debug]. Should you wish to +view all possible debugging output, [export +`DEBUG='*,*:*'`][pkg-debug-wildcards]. + +The ideal contributor flow is as follows: + +1. [Fork][fork] this repository and [clone it locally][how-to-clone]. + - If there is a custom Docker image available for this project and you're + comfortable with Docker, consider using it instead. +2. Configure and install dependencies with `npm ci`. + - You use `npm ci` here instead of `npm install` to [prevent unnecessary + updates to `package.json` and `package-lock.json`][npm-ci], but if it makes + more sense to use `npm install` feel free to use that instead. + - If `.env.example` exists, consider copying it to `.env` and configuring + sensible defaults. +3. Before making any changes, ensure all unit tests are passing with + `npm run test`. +4. _(optional but recommended)_ Create a new branch, usually off `main`. + - Example: `git checkout -b contrib-feature-1`. +5. Make your changes and commit. Your work will be checked as you commit; any + problems will abort the commit/push attempt. + - Ensure any new tests still pass even when the `DEBUG` environment variable + is defined. +6. Push your commits to your fork and, when you're ready, [_fearlessly_ submit + your PR][pr-compare]! Your changes will be tested in our CI pipeline. +7. Pat yourself on the back! Your hard work is well on its way to being reviewed + and, if everything looks good, merged and released πŸš€ + +Additionally, there are a few things you can do to greatly increase the +likelihood your PR passes review: + +- **Do** [open an issue][choose-new-issue] and discuss your proposed changes (to + prevent wasting your valuable time, e.g. _maybe we're already working on a + fix!_), and [search][open-issues] to see if there are any existing issues + related to your concerns. +- **Do** practice [atomic committing][atomic-commits]. +- **Do not** reduce code coverage ([codecov][codecov] checks are performed + during CI). +- **Do** [follow convention][conventional-commits] when coming up with your + commit messages. +- **Do not** circumvent CL, i.e. automated pre-commit linting, formatting, and + unit testing. +- **Do** ensure `README.md` and other documentation that isn't autogenerated is + kept consistent with your changes. +- **Do not** create a PR to introduce [_purely_ cosmetic + commits][cosmetic-commits]. + - Code de-duplication and other potential optimizations we **do not** consider + _purely_ cosmetic πŸ™‚ +- **Do** keep your PR as narrow and focused as possible. + - If you ran `npm install` instead of `npm ci` and it updated `package.json` + or `package-lock.json` and those updates have nothing to do with your PR + (e.g. random nested deps were updated), **do not** stage changes to those + files. + - If there are multiple related changes to be made but (1) they do not + immediately depend on one another or (2) one implements extended/alternative + functionality based on the other, consider submitting them as separate PRs + instead. + +At this point, you're ready to create your PR and ✨ contribute ✨! + +[atomic-commits]: https://www.codewithjason.com/atomic-commits-testing +[choose-new-issue]: + https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/issues/new/choose +[code-of-conduct]: /.github/CODE_OF_CONDUCT.md +[codecov]: https://about.codecov.io +[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0#summary +[cosmetic-commits]: + https://github.com/rails/rails/pull/13771#issuecomment-32746700 +[fork]: https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/fork +[github-actions]: https://github.com/features/actions +[how-to-clone]: + https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository +[how-to-contribute]: https://www.dataschool.io/how-to-contribute-on-github +[husky-cl]: + https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/tree/main/.husky +[npm-ci]: https://docs.npmjs.com/cli/v6/commands/npm-ci +[open-issues]: + https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/issues?q= +[pkg-debug]: https://www.npmjs.com/package/debug +[pkg-debug-wildcards]: https://www.npmjs.com/package/debug#wildcards +[pr-compare]: https://github.com/nhscc/elections_irv.api.hscc.bdpa.org/compare +[projector]: https://github.com/nhscc/elections_irv.api.hscc.bdpa.org#readme diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..027e59a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Bernard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f9b816 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# elections-irv.api.hscc.bdpa.org + +Live API used by solutions to the Elections (IRV) NHSCC problem statement. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0c30ac7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security ☠️ [![Known Vulnerabilities][2]][1] + +If the issue is related to a public alert from OWASP/GitHub/Dependabot/CVE/etc +and [does not already have an open issue][3], feel free to [open a new +issue][4]. Otherwise, please report any security vulnerability, other +security-related incident, or otherwise sensitive subject to us [via email][5]. + +Thank you for your contribution! + +[1]: https://snyk.io/test/github/Xunnamius/elections_irv.api.hscc.bdpa.org +[2]: + https://snyk.io/test/github/Xunnamius/elections_irv.api.hscc.bdpa.org/badge.svg +[3]: https://github.com/Xunnamius/elections_irv.api.hscc.bdpa.org/issues?q= +[4]: + https://github.com/Xunnamius/elections_irv.api.hscc.bdpa.org/issues/new/choose +[5]: + mailto:security@ergodark.com?subject=ALERT%3A%20SECURITY%20INCIDENT%3A%20%28five%20word%20summary%29 diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..1ccc18c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,107 @@ +'use strict'; +// * Every now and then, we adopt best practices from CRA +// * https://tinyurl.com/yakv4ggx + +// TODO: replace with 'package' +const pkgName = require('./package.json').name; +const debug = require('debug')(`${pkgName}:babel-config`); + +debug('NODE_ENV: %O', process.env.NODE_ENV); + +// ! This is pretty aggressive. It targets modern browsers only. +// ? For some projects, less aggressive targets will make much more +// ? sense! +const browserTargets = + 'Chrome >= 60, Safari >= 10.1, iOS >= 10.3, Firefox >= 54, Edge >= 15'; +// ? Something like the following might be more appropriate: +//const targets = '>1% in US and not ie 11'; + +// ? Next.js-specific Babel settings +const nextBabelPresetAndConfig = [ + '@xunnamius/next-babel', + { + 'preset-env': { + targets: browserTargets, + + // ? If users import all core-js they're probably not concerned with + // ? bundle size. We shouldn't rely on magic to try and shrink it. + useBuiltIns: false, + + // ? Do not transform modules to CJS + // ! MUST BE FALSE (see: https://nextjs.org/docs/#customizing-babel-config) + modules: false, + + // ? Exclude transforms that make all code slower + exclude: ['transform-typeof-symbol'] + }, + 'preset-typescript': { + allowDeclareFields: true + } + } +]; + +/** + * @type {import('@babel/core').TransformOptions} + */ +module.exports = { + comments: false, + parserOpts: { strictMode: true }, + assumptions: { constantReexports: true }, + plugins: [ + '@babel/plugin-proposal-export-default-from', + '@babel/plugin-syntax-import-assertions' + + // ? Interoperable named CJS imports for free + // [ + // 'transform-default-named-imports', + // { + // exclude: [/^next([/?#].+)?/, /^mongodb([/?#].+)?/] + // } + // ] + ], + // ? Sub-keys under the "env" config key will augment the above + // ? configuration depending on the value of NODE_ENV and friends. Default + // ? is: development + env: { + // * Used by Jest and `npm test` + test: { + comments: true, + sourceMaps: 'both', + presets: [ + ['@babel/preset-env', { targets: { node: true } }], + '@babel/preset-react', + ['@babel/preset-typescript', { allowDeclareFields: true }] + // ? We don't care about minification + ], + plugins: [ + // ? Only active when testing, the plugin solves the following problem: + // ? https://stackoverflow.com/q/40771520/1367414 + 'explicit-exports-references' + ] + }, + // * Used by Vercel, `npm start`, and `npm run build` + production: { + // ? Source maps are handled by Next.js and Webpack + presets: [nextBabelPresetAndConfig] + // ? Minification is handled by Webpack + }, + // * Used by `npm run dev`; is also the default environment + development: { + // ? Source maps are handled by Next.js and Webpack + presets: [nextBabelPresetAndConfig], + // ? https://reactjs.org/docs/error-boundaries.html#how-about-event-handlers + plugins: ['@babel/plugin-transform-react-jsx-source'] + // ? We don't care about minification + }, + // * Used by `npm run build-externals` + external: { + presets: [ + ['@babel/preset-env', { targets: { node: true } }], + ['@babel/preset-typescript', { allowDeclareFields: true }] + // ? Minification is handled by Webpack + ] + } + } +}; + +debug('exports: %O', module.exports); diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..73f7aaf --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,28 @@ +'use strict'; + +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-leading-blank': [2, 'always'], + 'footer-leading-blank': [2, 'always'], + 'type-enum': [ + 2, + 'always', + [ + 'feat', + 'feature', + 'fix', + 'perf', + 'revert', + 'build', + 'docs', + 'style', + 'refactor', + 'test', + 'ci', + 'cd', + 'chore' + ] + ] + } +}; diff --git a/conventional.config.js b/conventional.config.js new file mode 100644 index 0000000..eb01d6a --- /dev/null +++ b/conventional.config.js @@ -0,0 +1,4 @@ +'use strict'; +module.exports = require('@xunnamius/conventional-changelog-projector')({ + // * Your customizations here +}); diff --git a/expect-env.js b/expect-env.js new file mode 100644 index 0000000..9903a00 --- /dev/null +++ b/expect-env.js @@ -0,0 +1,276 @@ +'use strict'; +/* eslint-disable no-console */ + +const debug = require('debug')(`${require('./package.json').name}:expect-env`); + +const DEFAULT_VALUE_REGEX = [/^.*$/, /.*/]; + +class IllegalEnvironmentError extends Error {} + +module.exports = { + /** + * This function accepts a single object parameter with a list of `rules` used + * to verify `env`. If `env` is not defined, `process.env` is used instead. If + * `rules` is not defined, the "rules" defined under the "expect-env" key in + * ./package.json are used instead. + * + * Below, "name" is the name of an environment variable and "value" is its + * expected value. + * + * Each rule can be one of: + * + * (1) A simple STRING variable "name" interpreted as RegExp(`^${STRING}$`) + * + * (2) An OBJECT where "name" and "value" are both regex STRINGs; "required" + * is optional and defaults to `true`; and "errorMessage" is optional: + * + * { + * "name": "^(MY_)?VARIABLE_NAME$", + * "value": "^(true|false)$", + * "required": true, // β—„ optional + * "errorMessage": "some custom error message" // β—„ optional + * } + * + * (3) An OBJECT where "operation" is either "xor", "or", "and", or "not"; + * "variables" is an array where each element is (1) or (2) (without + * "required" or "errorMessage"); "required" is optional and defaults to + * `true`; and "errorMessage" is optional: + * + * { + * "operation": "xor", + * "variables": ["^MY_V_1$", { name: "^MY_V_2$", value: "^.*$" }], + * "required": false, // β—„ optional + * "errorMessage": "some custom error message" // β—„ optional + * } + * + * When "operation" is "xor", exactly one of the elements in "variables" must + * match the environment. When it's "or", at least one of the elements must + * match (this is the default). When it's "and", all of the elements must + * match. When it's "not", exactly zero elements must match. When "required" + * is `false` and "operation" is something other than "not", rules matching + * non-existent variable names will be skipped (others will still be + * verified). + * + * With both (2) and (3), "errorMessage" is output to the console if the rule + * fails verification or was not found in `env` but "required" is `true`. + * + * When `isCli=false`, this function will return an array of objects each with + * the shape of (3) representing all the rules that failed to match against + * the current environment. An empty array is returned if all rules matched + * and verification succeeded. When `isCli=true` (the default), this function + * will output errors to the console and non-zero exit if verification fails. + */ + verifyEnvironment(options) { + let { rules } = options || {}; + + const { env: outsideEnv, isCli } = { + ...options, + env: undefined, + isCli: true + }; + + let fromPkg = false; + let errorMessage = null; + + const normalize = (rule) => { + let normalizedRule; + const makeErrorMessage = (reason) => `missing dep: ${reason}`; + + debug('::normalize (not normalized): %O', rule); + + if (typeof rule === 'string' || rule instanceof RegExp) { + if (typeof rule === 'string') { + rule = rule.startsWith('^') ? rule.slice(1) : rule; + rule = rule.startsWith('$') ? rule.slice(0, -1) : rule; + rule = new RegExp(`^${rule}$`); + } + + normalizedRule = { + operation: 'or', + variables: [{ name: rule, value: DEFAULT_VALUE_REGEX[0] }], + required: true, + errorMessage: String( + makeErrorMessage( + `must define environment variable with name matching regex ${rule}` + ) + ) + }; + } else if (typeof rule.name === 'string' || rule.name instanceof RegExp) { + rule.name = rule.name instanceof RegExp ? rule.name : new RegExp(rule.name); + rule.value = + typeof rule.value === 'string' + ? new RegExp(rule.value) + : rule.value instanceof RegExp + ? rule.value + : DEFAULT_VALUE_REGEX[0]; + + normalizedRule = { + operation: 'or', + variables: [{ name: rule.name, value: rule.value }], + required: rule.required === undefined || !!rule.required, + errorMessage: String( + rule.errorMessage || + makeErrorMessage( + `must define environment variable with name matching regex ` + + `${rule.name}${ + DEFAULT_VALUE_REGEX.map(String).includes(rule.value.toString()) + ? '' + : ' and value matching regex ' + rule.value + }` + ) + ) + }; + } else if ( + ['and', 'or', 'xor', 'not'].includes(rule.operation) && + Array.isArray(rule.variables) + ) { + const variables = rule.variables.map((r) => normalize(r).variables[0]); + const not = rule.operation === 'not'; + const opString = not ? ' ' : `${rule.operation} `; + + const makeSubstr = (name, value) => + `name ${not ? 'NOT ' : ''}matching regex ${name}` + + (DEFAULT_VALUE_REGEX.map(String).includes((value || '').toString()) + ? '' + : ` and value ${not ? 'NOT ' : ''}matching regex ${value}`); + + normalizedRule = { + operation: rule.operation, + variables, + required: rule.required === undefined || !!rule.required, + errorMessage: String( + rule.errorMessage || + makeErrorMessage( + `must define ${ + not ? 'ALL environment variables' : 'environment variable' + } with` + + (variables.length > 1 ? ':\n' : ' ') + + variables + .slice(1) + // eslint-disable-next-line unicorn/no-array-reduce + .reduce( + (str, { name, value }) => + `${str}\n${opString}${makeSubstr(name, value)}`, + `${' '.repeat(opString.length)}${makeSubstr( + variables[0].name, + variables[0].value + )}` + ) + ) + ) + }; + } else { + debug('::normalize BAD RULE ENCOUNTERED: %O', rule); + throw new IllegalEnvironmentError( + `bad rule encountered${ + fromPkg ? ' in ./package.json "expect-env"' : '' + }: ${JSON.stringify(rule, undefined, 2)}` + ); + } + + debug('::normalize (normalized): %O', normalizedRule); + return normalizedRule; + }; + + if (!rules) { + try { + ({ + 'expect-env': { rules, errorMessage } + } = require('./package.json')); + fromPkg = true; + } catch { + /* ignored */ + } + } + + debug('rules: %O', rules); + debug('errorMessage: %O', errorMessage); + + if (rules === undefined) return []; + + if (!Array.isArray(rules)) { + throw new IllegalEnvironmentError( + fromPkg + ? '"expect-env" key must have an array value in package.json' + : '`rules` must be an array' + ); + } + + const env = outsideEnv || process.env; + const envVariables = Object.keys(env); + const violations = []; + let verificationSucceeded = true; + + rules + .map((rule) => normalize(rule)) + .forEach((rule) => { + let succeeded = null; + + for (const { name, value } of rule.variables) { + let matchedAtLeastOneVariable = false; + + // ? Attempt to match variable names and values vs name and value + const matched = envVariables.some((v) => { + const matchesName = name.test(v); + matchedAtLeastOneVariable = matchedAtLeastOneVariable || matchesName; + return matchesName && value.test(env[v]); + }); + + // ? If it's not required and not in env, skip evaluation + if (!rule.required && !matchedAtLeastOneVariable) { + succeeded = true; + continue; + } + + if (rule.operation === 'or') { + if (matched) { + succeeded = true; + break; + } + } else if (rule.operation === 'and') { + if (matched) succeeded = true; + else { + succeeded = false; + break; + } + } else if (rule.operation === 'not') { + if (!matched) succeeded = true; + else { + succeeded = false; + break; + } + } else if (rule.operation === 'xor') { + if (succeeded === null) succeeded = !matched ? null : true; + else if (succeeded && matched) { + succeeded = false; + break; + } + } else { + throw new IllegalEnvironmentError( + `unrecognized operation "${rule.operation}"` + ); + } + } + + if (!succeeded) + isCli ? console.error(rule.errorMessage) : violations.push(rule); + verificationSucceeded = verificationSucceeded && succeeded; + }); + + if (!verificationSucceeded && isCli) + throw new IllegalEnvironmentError( + errorMessage || 'environment verification failed' + ); + + debug('violated rules: %O', violations); + return violations; + } +}; + +try { + !module.parent && module.exports.verifyEnvironment(); +} catch (error) { + console.error(error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +} diff --git a/external-scripts/README.md b/external-scripts/README.md new file mode 100644 index 0000000..5dce5c0 --- /dev/null +++ b/external-scripts/README.md @@ -0,0 +1,8 @@ +# External Scripts + +External scripts or "externals" are auxiliary executables that are co-located +with application source for ease of compilation and management. See each +external's documentation for usage instructions. + +Note that externals must first be compiled before they can be run, i.e. +`npm run build-externals`. diff --git a/external-scripts/ban-hammer.ts b/external-scripts/ban-hammer.ts new file mode 100644 index 0000000..3567159 --- /dev/null +++ b/external-scripts/ban-hammer.ts @@ -0,0 +1,290 @@ +/* eslint-disable unicorn/no-process-exit */ +/* eslint-disable unicorn/no-thenable */ +import { AppError, InvalidAppEnvironmentError } from 'named-app-errors'; + +import { debugNamespace as namespace } from 'universe/constants'; +import { getEnv } from 'universe/backend/env'; + +import { getDb } from 'multiverse/mongo-schema'; +import { debugFactory } from 'multiverse/debug-extended'; + +const debugNamespace = `${namespace}:ban-hammer`; + +const oneSecondInMs = 1000; +const log = debugFactory(debugNamespace); +const debug = debugFactory(`${debugNamespace}:debug`); + +// eslint-disable-next-line no-console +log.log = console.info.bind(console); + +// ? Ensure this next line survives Webpack +if (!globalThis.process.env.DEBUG && getEnv().NODE_ENV !== 'test') { + debugFactory.enable( + `${debugNamespace},${debugNamespace}:*,-${debugNamespace}:debug` + ); +} + +/** + * Pores over request-log entries and drops the ban hammer on rule breaking + * clients. + */ +const invoked = async () => { + try { + const { + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: calledEverySeconds, + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: maxRequestsPerWindow, + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: resolutionWindowSeconds, + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: defaultBanTimeMinutes, + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: punishMultiplier + } = getEnv(); + + if (!calledEverySeconds || !(Number(calledEverySeconds) > 0)) { + throw new InvalidAppEnvironmentError( + 'BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS must be greater than zero' + ); + } + + if (!maxRequestsPerWindow || !(Number(maxRequestsPerWindow) > 0)) { + throw new InvalidAppEnvironmentError( + 'BAN_HAMMER_MAX_REQUESTS_PER_WINDOW must be greater than zero' + ); + } + + if (!resolutionWindowSeconds || !(Number(resolutionWindowSeconds) > 0)) { + throw new InvalidAppEnvironmentError( + 'BAN_HAMMER_RESOLUTION_WINDOW_SECONDS must be greater than zero' + ); + } + + if (!defaultBanTimeMinutes || !(Number(defaultBanTimeMinutes) > 0)) { + throw new InvalidAppEnvironmentError( + 'BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES must be greater than zero' + ); + } + + if (!punishMultiplier || !(Number(punishMultiplier) > 0)) { + throw new InvalidAppEnvironmentError( + 'BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER must be greater than zero' + ); + } + + const calledEveryMs = oneSecondInMs * calledEverySeconds; + const defaultBanTimeMs = oneSecondInMs * 60 * defaultBanTimeMinutes; + const resolutionWindowMs = oneSecondInMs * resolutionWindowSeconds; + const db = await getDb({ name: 'root' }); + + const pipeline = [ + { + $limit: 1 + }, + { + $project: { _id: 1 } + }, + { + $project: { _id: 0 } + }, + { + $lookup: { + from: 'request-log', + as: 'headerBased', + pipeline: [ + { + $match: { + header: { $ne: null }, + $expr: { + $gte: [ + '$createdAt', + { $subtract: [{ $toLong: '$$NOW' }, calledEveryMs] } + ] + } + } + }, + { + $group: { + _id: { + header: '$header', + interval: { + $subtract: [ + '$createdAt', + { $mod: ['$createdAt', resolutionWindowMs] } + ] + } + }, + count: { $sum: 1 } + } + }, + { + $match: { + count: { $gt: maxRequestsPerWindow } + } + }, + { + $project: { + header: '$_id.header', + until: { $add: [{ $toLong: '$$NOW' }, defaultBanTimeMs] } + } + }, + { + $project: { + _id: 0, + count: 0 + } + } + ] + } + }, + { + $lookup: { + from: 'request-log', + as: 'ipBased', + pipeline: [ + { + $match: { + $expr: { + $gte: [ + '$createdAt', + { $subtract: [{ $toLong: '$$NOW' }, calledEveryMs] } + ] + } + } + }, + { + $group: { + _id: { + ip: '$ip', + interval: { + $subtract: [ + '$createdAt', + { $mod: ['$createdAt', resolutionWindowMs] } + ] + } + }, + count: { $sum: 1 } + } + }, + { + $match: { + count: { $gt: maxRequestsPerWindow } + } + }, + { + $project: { + ip: '$_id.ip', + until: { $add: [{ $toLong: '$$NOW' }, defaultBanTimeMs] } + } + }, + { + $project: { + _id: 0, + count: 0 + } + } + ] + } + }, + { + $lookup: { + from: 'limited-log', + as: 'previous', + pipeline: [ + { + $match: { + $expr: { + $gte: [ + '$until', + { + $subtract: [ + { $toLong: '$$NOW' }, + defaultBanTimeMs * punishMultiplier + ] + } + ] + } + } + }, + { + $project: { + _id: 0 + } + } + ] + } + }, + { + $project: { + union: { $concatArrays: ['$headerBased', '$ipBased', '$previous'] } + } + }, + { + $unwind: { + path: '$union' + } + }, + { + $replaceRoot: { + newRoot: '$union' + } + }, + { + $group: { + _id: { + ip: '$ip', + header: '$header' + }, + count: { + $sum: 1 + }, + until: { + $max: '$until' + } + } + }, + { + $set: { + until: { + $cond: { + if: { $ne: ['$count', 1] }, + then: { + $max: [ + { + $add: [{ $toLong: '$$NOW' }, defaultBanTimeMs * punishMultiplier] + }, + '$until' + ] + }, + else: '$until' + } + }, + ip: '$_id.ip', + header: '$_id.header' + } + }, + { + $project: { + count: 0, + _id: 0 + } + }, + { + $out: 'limited-log' + } + ]; + + debug('aggregation pipeline: %O', pipeline); + + const cursor = db.collection('request-log').aggregate(pipeline); + + await cursor.next(); + await cursor.close(); + + log('execution complete'); + process.exit(0); + } catch (error) { + throw new AppError(`${error}`); + } +}; + +export default invoked().catch((error: Error) => { + log.error(error.message); + process.exit(2); +}); diff --git a/external-scripts/initialize-data/crypto.ts b/external-scripts/initialize-data/crypto.ts new file mode 100644 index 0000000..3e7a76e --- /dev/null +++ b/external-scripts/initialize-data/crypto.ts @@ -0,0 +1,58 @@ +import { webcrypto as crypto } from 'node:crypto'; + +/** + * We use 64 byte keys (128 hex digits long) + */ +const KEY_SIZE_BYTES = 64; + +/** + * We use 16 byte salts (32 hex digits long) + */ +const SALT_SIZE_BYTES = 16; + +/** + * A function that converts a ByteArray or any other array of bytes into a + * string of hexadecimal digits + */ +export const convertBufferToHex = (buffer: ArrayBufferLike) => { + return [...new Uint8Array(buffer)] + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +}; + +/** + * Turns a password (string) and salt (buffer) into a key and salt (hex strings) + */ +export const deriveKeyFromPassword = async ( + passwordString: string, + saltBuffer?: Uint8Array +) => { + const textEncoder = new TextEncoder(); + const passwordBuffer = textEncoder.encode(passwordString); + + saltBuffer = saltBuffer || crypto.getRandomValues(new Uint8Array(SALT_SIZE_BYTES)); + + const plaintextKey = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + 'PBKDF2', + false, + ['deriveBits'] + ); + + const pbkdf2Buffer = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt: saltBuffer, + iterations: 100_000, + hash: 'SHA-256' + }, + plaintextKey, + KEY_SIZE_BYTES * 8 + ); + + const saltString = convertBufferToHex(saltBuffer); + const keyString = convertBufferToHex(pbkdf2Buffer); + + return { keyString, saltString }; +}; diff --git a/external-scripts/initialize-data/index.ts b/external-scripts/initialize-data/index.ts new file mode 100644 index 0000000..5ffafa4 --- /dev/null +++ b/external-scripts/initialize-data/index.ts @@ -0,0 +1,214 @@ +/* eslint-disable unicorn/no-process-exit */ +/* eslint-disable no-await-in-loop */ +import { faker } from '@faker-js/faker'; +import inquirer, { type PromptModule } from 'inquirer'; +import { ObjectId } from 'mongodb'; +import { AppError } from 'named-app-errors'; + +import { getEnv } from 'universe/backend/env'; +import { debugNamespace as namespace } from 'universe/constants'; + +import { debugFactory } from 'multiverse/debug-extended'; +import { getDb } from 'multiverse/mongo-schema'; + +import type { InternalBallot, InternalElection } from 'universe/backend/db'; +import { getRandomFakeElectionData } from './question-generator'; + +const debugNamespace = `${namespace}:initialize-data`; + +const log = debugFactory(debugNamespace); +const debug = debugFactory(`${debugNamespace}:debug`); +const logOrDebug = () => { + return log.enabled ? log : debug; +}; + +// eslint-disable-next-line no-console +log.log = console.info.bind(console); + +// ? Ensure this next line survives Webpack +if (!globalThis.process.env.DEBUG && getEnv().NODE_ENV !== 'test') { + debugFactory.enable( + `${debugNamespace},${debugNamespace}:*,-${debugNamespace}:debug` + ); +} + +/** + * Returns the `inquirer` instance unless a string `testPrompterParams` is given + * (passed to URLSearchParams, usually provided by a TEST_PROMPTER_X environment + * variable), in which case a passthrough promise that resolves to a simulated + * answer object based on `testPrompterParams` is returned as the resolved + * result of calling `prompt()` instead. + */ +const getPrompter = (testPrompterParams?: string): { prompt: PromptModule } => { + return testPrompterParams + ? { + prompt: (() => { + debug( + `using simulated inquirer prompt based on params: ${testPrompterParams}` + ); + + return Promise.resolve( + Object.fromEntries( + Array.from(new URLSearchParams(testPrompterParams).entries()) + ) + ); + }) as unknown as PromptModule + } + : inquirer; +}; + +/** + * Sets up a database from scratch by creating collections (only if they do + * not already exist) and populating them with a large amount of data. Suitable + * for initializing local machines or production instances alike. + * + * This function is data-preserving (all actions are non-destructive: data is + * never overwritten or deleted) + */ +const invoked = async () => { + try { + const answers = await getPrompter(process.env.TEST_PROMPTER_INITIALIZER).prompt<{ + action: string; + token: string; + }>([ + { + name: 'action', + message: 'select an initializer action', + type: 'list', + choices: [ + { + name: 'commit initial state to database', + value: 'commit' + }, + { + name: 'commit initial state to database (force)', + value: 'commit-force' + }, + { name: 'exit', value: 'exit' } + ] + } + ]); + + switch (answers.action) { + case 'exit': { + break; + } + + case 'commit': + case 'commit-force': { + const [, appDb] = await Promise.all([ + getDb({ name: 'root' }), + getDb({ name: 'app' }) + ]); + + const [electionsDb, ballotsDb] = await Promise.all([ + appDb.collection('elections'), + appDb.collection('ballots') + ]); + + const hasNoRecords = + (await electionsDb.countDocuments()) + + (await ballotsDb.countDocuments()) === + 0; + + if (answers.action === 'commit-force' || hasNoRecords) { + const { MAX_BALLOTS_PER_ELECTION, MAX_VOTERID_LENGTH } = getEnv(); + + const elections = Array.from({ + length: faker.helpers.rangeToNumber({ min: 900, max: 1000 }) + }).map((_, index) => { + const createdAt = faker.helpers.rangeToNumber({ + min: Date.now() - 10 ** 9, + max: Date.now() + (index % 5 === 0 ? 10 ** 12 : 0) + }); + + const opensAt = faker.helpers.rangeToNumber({ + min: createdAt - (index % 15 === 0 ? 10 ** 9 : 0), + max: createdAt + (index % 3 === 0 ? 10 ** 12 : 1) + }); + + const closesAt = faker.helpers.rangeToNumber({ + min: opensAt + 1, + max: opensAt + (index % 6 === 0 ? 10 ** 12 : 10 ** 10) + }); + + return { + __provenance: 'auto-generated', + _id: new ObjectId(), + ...getRandomFakeElectionData(), + createdAt, + opensAt, + closesAt, + deleted: index % 10 === 0 + }; + }); + + const ballots = elections.flatMap(({ _id, options }) => { + if (!options.length) { + return []; + } + + return Array.from({ + length: faker.helpers.rangeToNumber({ + min: 0, + max: MAX_BALLOTS_PER_ELECTION + }) + }).map((_, ballotIndex) => { + return { + __provenance: 'auto-generated', + _id: new ObjectId(), + election_id: _id, + voter_id: + faker.internet.userName().slice(0, MAX_VOTERID_LENGTH - 5) + + faker.string.numeric(5), + ranking: Object.fromEntries( + faker.helpers + .shuffle( + options.slice( + 0, + ballotIndex === 0 + ? // ? The first ballot will always be incomplete + faker.helpers.rangeToNumber({ + min: 1, + max: options.length + }) + : options.length + ) + ) + .map((option, optionIndex) => [option, optionIndex + 1]) + ) + }; + }); + }); + + await electionsDb.insertMany(elections); + await ballotsDb.insertMany(ballots); + } + + await getPrompter(process.env.TEST_PROMPTER_FINALIZER).prompt<{ + action: string; + token: string; + }>([ + { + name: 'action', + message: 'what now?', + type: 'list', + choices: [{ name: 'exit', value: 'exit' }] + } + ]); + + break; + } + } + + logOrDebug()('execution complete'); + process.exit(0); + } catch (error) { + throw new AppError(`${error}`); + } +}; + +export default invoked().catch((error: Error) => { + log.error(error.message); + process.exit(2); +}); diff --git a/external-scripts/initialize-data/question-generator.ts b/external-scripts/initialize-data/question-generator.ts new file mode 100644 index 0000000..2463c1e --- /dev/null +++ b/external-scripts/initialize-data/question-generator.ts @@ -0,0 +1,184 @@ +/* eslint-disable unicorn/no-array-reduce */ +import { faker } from '@faker-js/faker'; +import { type InternalElection } from 'universe/backend/db'; +import { getEnv } from 'universe/backend/env'; + +const questions = { + best1: 'What is your favorite {{noun}}?', + best2: 'Which {{noun}} sounds best?', + best3: 'Coolest {{noun}}?', + best4: 'Hey, what is the most useful {{noun}}?', + worst: 'What is your least favorite {{noun}}?', + adjective: 'Which {{noun}} is the {{superlative}} {{adjective}}?', + compare: 'Is a {{thing1}} {{compare}} a {{thing2}}?', + super: 'For Super Earth?' +}; + +const fakerNounMethods: [noun: string, objectNotation: string][] = [ + ['airline', 'airline.airline().name'], + ['airport', 'airline.airport().name'], + ['airplane', 'airline.airplane().name'], + + ['animal', 'animal.type()'], + ['bear', 'animal.bear()'], + ['bird', 'animal.bird()'], + ['cat', 'animal.cat()'], + ['cetacean', 'animal.cetacean()'], + ['cow', 'animal.cow()'], + ['crocodilia', 'animal.crocodilia()'], + ['dog', 'animal.dog()'], + ['fish', 'animal.fish()'], + ['horse', 'animal.horse()'], + ['insect', 'animal.insect()'], + ['lion', 'animal.lion()'], + ['rabbit', 'animal.rabbit()'], + ['rodent', 'animal.rodent()'], + ['snake', 'animal.snake()'], + + ['color', 'color.human()'], + + ['product', 'commerce.product()'], + ['department', 'commerce.department()'], + ['price', 'commerce.price()'], + + ['company', 'company.name()'], + ['buzzword', 'company.buzzNoun()'], + + ['database engine', 'database.engine()'], + + ['git branch name', 'git.branch()'], + + ['hacker abbreviation', 'hacker.adjective()'], + + ['username', 'internet.displayName()'], + ['domain name', 'internet.domainName()'], + ['domain name', 'internet.domainName()'], + + ['city', 'location.city()'], + ['state', 'location.state()'], + ['country', 'location.country()'], + + ['music genre', 'music.genre()'], + ['song name', 'music.songName()'], + + ['job', 'person.jobType()'], + ['job title', 'person.jobTitle()'], + + ['chemical', 'science.chemicalElement().name'], + + ['file type', 'system.commonFileType()'], + + ['vehicle', 'vehicle.vehicle()'], + ['vehicle type', 'vehicle.type()'], + ['bike', 'vehicle.bicycle()'], + ['manufacturer', 'vehicle.manufacturer()'], + ['car model', 'vehicle.model()'] +]; + +const superlatives = ['most', 'least']; + +const comparisonWords = [ + 'better than', + 'greater than', + 'worse than', + 'as good as', + 'nicer than', + 'more beautiful than' +]; + +const likertScale = [ + 'strong yes', + 'moderate yes', + 'unsure/ambivalent', + 'moderate no', + 'strong no' +]; + +function getStringFromFaker(methodString: string) { + const str = methodString.split('.').reduce((f, method) => { + // @ts-expect-error: TS is not smart enough to figure this out + return method.endsWith('()') ? f[method.slice(0, -2)]() : f[method]; + }, faker); + + if (typeof str !== 'string') { + throw new TypeError('str is not a string'); + } + + return str; +} + +export function getRandomFakeElectionData() { + const { + MAX_ELECTION_DESC_LENGTH, + MAX_ELECTION_OPTION_LENGTH, + MAX_ELECTION_TITLE_LENGTH, + MAX_ELECTION_OPTIONS_ITEMS + } = getEnv(); + + const chosenKey = faker.helpers.objectKey(questions); + + const data: Pick = { + title: '', + description: faker.lorem.words({ min: 0, max: 50 }), + options: Array.from({ + length: faker.helpers.rangeToNumber({ + min: 0, + max: MAX_ELECTION_OPTIONS_ITEMS + }) + }) + }; + + switch (chosenKey) { + case 'best1': + case 'best2': + case 'best3': + case 'best4': + case 'worst': { + const [noun, objNotation] = faker.helpers.arrayElement(fakerNounMethods); + data.title = faker.helpers.mustache(questions[chosenKey], { noun }); + data.options = data.options.map(() => getStringFromFaker(objNotation)); + break; + } + + case 'adjective': { + const [noun, objNotation] = faker.helpers.arrayElement(fakerNounMethods); + data.options = data.options.map(() => getStringFromFaker(objNotation)); + data.title = faker.helpers.mustache(questions[chosenKey], { + noun, + superlative: faker.helpers.arrayElement(superlatives), + adjective: faker.word.adjective() + }); + break; + } + + case 'compare': { + const [, objNotation] = faker.helpers.arrayElement(fakerNounMethods); + data.options = likertScale; + data.title = faker.helpers.mustache(questions[chosenKey], { + thing1: getStringFromFaker(objNotation), + thing2: getStringFromFaker(objNotation), + compare: faker.helpers.arrayElement(comparisonWords) + }); + break; + } + + case 'super': { + data.title = questions[chosenKey]; + data.options = likertScale; + break; + } + + default: { + throw new TypeError(`${chosenKey} is unhandled`); + } + } + + data.title = data.title.slice(0, MAX_ELECTION_TITLE_LENGTH); + data.description = data.description.slice(0, MAX_ELECTION_DESC_LENGTH); + data.options = Array.from( + // ? Ensure options are unique + new Set(data.options.map((option) => option.slice(0, MAX_ELECTION_OPTION_LENGTH))) + ); + + return data; +} diff --git a/external-scripts/log-stats.ts b/external-scripts/log-stats.ts new file mode 100644 index 0000000..ae0e51f --- /dev/null +++ b/external-scripts/log-stats.ts @@ -0,0 +1,696 @@ +/* eslint-disable unicorn/no-process-exit */ +/* eslint-disable unicorn/no-thenable */ +import jsonFile from 'jsonfile'; + +import { + AppError, + GuruMeditationError, + InvalidAppEnvironmentError +} from 'named-app-errors'; + +import { debugNamespace as namespace } from 'universe/constants'; +import { getEnv } from 'universe/backend/env'; + +import { getDb } from 'multiverse/mongo-schema'; +import { debugFactory } from 'multiverse/debug-extended'; + +const debugNamespace = `${namespace}:log-stats`; +const cachePath = `${__dirname}/log-stats-cache.json`; + +const log = debugFactory(debugNamespace); +const debug = debugFactory(`${debugNamespace}:debug`); + +// eslint-disable-next-line no-console +log.log = console.info.bind(console); + +// ? Ensure this next line survives Webpack +if (!globalThis.process.env.DEBUG && getEnv().NODE_ENV !== 'test') { + debugFactory.enable( + `${debugNamespace},${debugNamespace}:*,-${debugNamespace}:debug` + ); +} + +/** + * Pores over request-log entries and drops the ban hammer on rule breaking + * clients. + */ +const invoked = async () => { + try { + if (!getEnv().MONGODB_URI) { + throw new InvalidAppEnvironmentError( + 'MONGODB_URI must be a valid mongodb connection string' + ); + } + + let show404s = false; + + if (globalThis.process.argv.includes('--show-404s')) { + show404s = true; + } + + log(`compiling statistics (404s ${show404s ? 'shown' : 'hidden'})...`); + + const previousResults: Record = await (async () => { + try { + debug(`reading in results from cache at ${cachePath}`); + return await jsonFile.readFile(cachePath); + } catch { + return {}; + } + })(); + + debug('previous results: %O', previousResults); + + const db = await getDb({ name: 'root' }); + const requestLogDb = db.collection('request-log'); + const limitedLogDb = db.collection('limited-log'); + + const requestLogPipeline = [ + { + $facet: { + // ? Select the latest timestamp per (header) + group_header_x_latest: [ + { + $group: { + _id: '$header', + latestAt: { $max: '$createdAt' } + } + } + ], + // ? Count requests per (header) + group_header: [ + { + $group: { + _id: '$header', + totalRequests: { $sum: 1 }, + preflightRequests: { + $sum: { + $cond: { if: { $eq: ['$method', 'OPTIONS'] }, then: 1, else: 0 } + } + }, + normalRequests: { + $sum: { + $cond: { if: { $eq: ['$method', 'OPTIONS'] }, then: 0, else: 1 } + } + } + } + } + ], + // ? Count requests per (header, ip) + group_header_x_ip: [ + { + $group: { + _id: { header: '$header', ip: '$ip' }, + requests: { $sum: 1 } + } + }, + { + $group: { + _id: '$_id.header', + ips: { + $push: { + ip: '$_id.ip', + requests: '$requests' + } + } + } + } + ], + // ? Count requests per (header, method) + group_header_x_method: [ + { + $group: { + _id: { header: '$header', method: '$method' }, + requests: { $sum: 1 } + } + }, + { + $group: { + _id: '$_id.header', + methods: { + $push: { + method: '$_id.method', + requests: '$requests' + } + } + } + } + ], + // ? Count requests per (header, status) + group_header_x_status: [ + { + $group: { + _id: { header: '$header', status: '$resStatusCode' }, + requests: { $sum: 1 } + } + }, + { + $group: { + _id: '$_id.header', + statuses: { + $push: { + status: '$_id.status', + requests: '$requests' + } + } + } + } + ], + // ? Count requests per (header, endpoint) + group_header_x_endpoints: [ + { + $group: { + _id: { + header: '$header', + endpoint: { + $cond: { + if: { $not: ['$endpoint'] }, + then: '', + else: '$endpoint' + } + } + }, + requests: { $sum: 1 } + } + }, + { + $group: { + _id: '$_id.header', + endpoints: { + $push: { + endpoint: '$_id.endpoint', + requests: '$requests' + } + } + } + } + ] + } + }, + + // ? Merge stats into per-header documents + { + $project: { + headerStats: { + $concatArrays: [ + '$group_header_x_latest', + '$group_header', + '$group_header_x_ip', + '$group_header_x_method', + '$group_header_x_status', + '$group_header_x_endpoints' + ] + } + } + }, + { + $unwind: '$headerStats' + }, + { + $group: { + _id: '$headerStats._id', + stats: { $mergeObjects: '$$ROOT.headerStats' } + } + }, + { + $replaceRoot: { newRoot: '$stats' } + }, + + // ? Sort results by greatest number of requests + { + $sort: { normalRequests: -1 } + }, + + // ? Add relevant fields + { + $addFields: { + header: '$_id', + token: { $arrayElemAt: [{ $split: ['$_id', ' '] }, 1] } + } + }, + + // ? Cross-reference tokens with identity data + { + $lookup: { + from: 'auth', + localField: 'token', + foreignField: 'token.bearer', + as: 'auth' + } + }, + { + $replaceRoot: { + newRoot: { + $mergeObjects: [{ $arrayElemAt: ['$auth', 0] }, '$$ROOT'] + } + } + }, + + // ? Beautify output + { + $addFields: { + owner: { $ifNull: ['$attributes.owner', ''] }, + token: { + $cond: { + if: { $gt: ['$attributes.owner', null] }, + then: { $ifNull: ['$token', ''] }, + else: '' + } + } + } + }, + { + $project: { + _id: false, + attributes: false, + auth: false, + scheme: false + } + } + ]; + + // ? Calculates duration percentiles: fastest, 50%, 90%, 95%, 99%, 99.9%, + // ? and slowest + const requestPercentilePipeline = [ + { $match: { durationMs: { $exists: true } } }, + { $sort: { durationMs: 1 } }, + { + $addFields: { + endpoint: { $ifNull: ['$endpoint', ''] }, + resStatusCode: { $ifNull: ['$resStatusCode', ''] }, + method: { $ifNull: ['$method', ''] } + } + }, + { + $group: { + _id: null, + durations: { $push: '$durationMs' }, + endpoints: { $push: '$endpoint' }, + statuses: { $push: '$resStatusCode' }, + methods: { $push: '$method' } + } + }, + { + $addFields: { + index_50: { $floor: { $multiply: [0.5, { $size: '$durations' }] } }, + index_90: { $floor: { $multiply: [0.9, { $size: '$durations' }] } }, + index_95: { $floor: { $multiply: [0.95, { $size: '$durations' }] } }, + index_99: { $floor: { $multiply: [0.99, { $size: '$durations' }] } }, + index_999: { $floor: { $multiply: [0.999, { $size: '$durations' }] } }, + index_9999: { $floor: { $multiply: [0.9999, { $size: '$durations' }] } } + } + }, + { + $project: { + _id: false, + fastest: { + duration: { $arrayElemAt: ['$durations', 0] }, + endpoint: { $arrayElemAt: ['$endpoints', 0] }, + status: { $arrayElemAt: ['$statuses', 0] }, + method: { $arrayElemAt: ['$methods', 0] } + }, + percentile_50: { + duration: { $arrayElemAt: ['$durations', '$index_50'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_50'] }, + status: { $arrayElemAt: ['$statuses', '$index_50'] }, + method: { $arrayElemAt: ['$methods', '$index_50'] } + }, + percentile_90: { + duration: { $arrayElemAt: ['$durations', '$index_90'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_90'] }, + status: { $arrayElemAt: ['$statuses', '$index_90'] }, + method: { $arrayElemAt: ['$methods', '$index_90'] } + }, + percentile_95: { + duration: { $arrayElemAt: ['$durations', '$index_95'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_95'] }, + status: { $arrayElemAt: ['$statuses', '$index_95'] }, + method: { $arrayElemAt: ['$methods', '$index_95'] } + }, + percentile_99: { + duration: { $arrayElemAt: ['$durations', '$index_99'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_99'] }, + status: { $arrayElemAt: ['$statuses', '$index_99'] }, + method: { $arrayElemAt: ['$methods', '$index_99'] } + }, + percentile_999: { + duration: { $arrayElemAt: ['$durations', '$index_999'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_999'] }, + status: { $arrayElemAt: ['$statuses', '$index_999'] }, + method: { $arrayElemAt: ['$methods', '$index_999'] } + }, + percentile_9999: { + duration: { $arrayElemAt: ['$durations', '$index_9999'] }, + endpoint: { $arrayElemAt: ['$endpoints', '$index_9999'] }, + status: { $arrayElemAt: ['$statuses', '$index_9999'] }, + method: { $arrayElemAt: ['$methods', '$index_9999'] } + }, + slowest: { + duration: { $arrayElemAt: ['$durations', -1] }, + endpoint: { $arrayElemAt: ['$endpoints', -1] }, + status: { $arrayElemAt: ['$statuses', -1] }, + method: { $arrayElemAt: ['$methods', -1] } + } + } + } + ]; + + const limitedLogPipeline = [ + { + $project: { + _id: false, + header: true, + token: { $arrayElemAt: [{ $split: ['$header', ' '] }, 1] }, + ip: true, + until: true + } + }, + { + $lookup: { + from: 'auth', + localField: 'token', + foreignField: 'token.bearer', + as: 'auth' + } + }, + { + $replaceRoot: { + newRoot: { + $mergeObjects: [{ $arrayElemAt: ['$auth', 0] }, '$$ROOT'] + } + } + }, + { + $project: { + owner: { $ifNull: ['$attributes.owner', ''] }, + token: { + $cond: { + if: { $gt: ['$attributes.owner', null] }, + then: { $ifNull: ['$token', ''] }, + else: '' + } + }, + header: true, + ip: true, + until: true, + dummy: true + } + }, + { + $sort: { until: -1, _id: -1 } + }, + { + $project: { + _id: false, + dummy: false + } + } + ]; + + const byRequests = (a: { requests: number }, b: { requests: number }) => + b.requests - a.requests; + + debug('running request-log aggregation pipeline: %O', requestLogPipeline); + + const requestLogCursor = requestLogDb.aggregate<{ + owner: string; + token: string; + header: string | null; + normalRequests: number; + preflightRequests: number; + totalRequests: number; + ips: { ip: string; requests: number }[]; + statuses: { status: number; requests: number }[]; + methods: { method: string; requests: number }[]; + endpoints: { endpoint: string; requests: number }[]; + latestAt: number; + }>(requestLogPipeline); + + const requestLogStats = await requestLogCursor.toArray(); + + debug( + 'running request-log percentile aggregation pipeline: %O', + requestPercentilePipeline + ); + + const percentileCursor = requestLogDb.aggregate<{ + fastest: Percentile; + percentile_50: Percentile; + percentile_90: Percentile; + percentile_95: Percentile; + percentile_99: Percentile; + percentile_999: Percentile; + percentile_9999: Percentile; + slowest: Percentile; + }>(requestPercentilePipeline); + + const requestPercentiles = await percentileCursor.next(); + + debug('running limited-log aggregation pipeline: %O', limitedLogPipeline); + + const limitedLogCursor = limitedLogDb.aggregate<{ + owner?: string; + token?: string; + header?: string | null; + ip?: string; + until: number; + }>(limitedLogPipeline); + + const limitedLogStats = await limitedLogCursor.toArray(); + + debug('closing cursors'); + + await Promise.all([ + requestLogCursor.close(), + percentileCursor.close(), + limitedLogCursor.close() + ]); + + const chalk = (await import('chalk')).default; + const outputStrings: string[] = []; + + const addAuthInfo = ( + owner: string, + token: string, + header: string | null, + error = false + ) => { + outputStrings.push( + ` owner: ${ + owner === '' + ? chalk.gray(owner) + : chalk[error ? 'red' : 'green'].bold(owner) + }`, + ` token: ${token === '' ? chalk.gray(token) : token}`, + ` header: ${header ?? chalk.gray(header)}` + ); + }; + + debug('compiling output'); + debug(`requestLogStats.length=${requestLogStats.length}`); + debug(`limitedLogStats.length=${limitedLogStats.length}`); + debug('requestPercentiles=%O', requestPercentiles); + + outputStrings.push(`\n::REQUEST LOG::${requestLogStats.length ? '\n' : ''}`); + + if (!requestLogStats.length) { + outputStrings.push(' '); + Object.keys(previousResults).forEach((k) => { + delete previousResults[k]; + }); + } else { + requestLogStats.forEach( + ({ + owner, + token, + header, + normalRequests, + preflightRequests, + totalRequests, + ips, + methods, + endpoints, + statuses, + latestAt + }) => { + addAuthInfo(owner, token, header); + + const headerString = String(header); + const delta = previousResults[headerString] + ? normalRequests - previousResults[headerString] + : null; + + previousResults[headerString] = normalRequests; + + outputStrings.push( + ` total requests: ${ + preflightRequests + ? `${normalRequests} (+${preflightRequests} preflight, ${totalRequests} total)` + : totalRequests + }${ + delta !== null + ? chalk.yellow(` (Ξ”${delta >= 0 ? `+${delta}` : delta})`) + : '' + }`, + ` most recent request: ${new Date(latestAt).toLocaleString()}`, + ' requests by ip:' + ); + + ips.forEach(({ ip, requests: requestsFromIp }) => + outputStrings.push(` ${ip} - ${requestsFromIp} requests`) + ); + + outputStrings.push(' requests by HTTP status code:'); + + statuses + .sort(byRequests) + .forEach(({ status, requests: requestResponseStatus }) => { + const str = ` ${status} - ${requestResponseStatus} requests`; + outputStrings.push(status === 429 ? chalk.red(str) : str); + }); + + outputStrings.push(' requests by HTTP method:'); + + methods + .sort(byRequests) + .forEach(({ method, requests: requestsOfMethod }) => { + const str = ` ${method} - ${requestsOfMethod} requests`; + outputStrings.push(method === 'OPTIONS' ? chalk.gray(str) : str); + }); + + outputStrings.push(' requests by endpoint:'); + + const _404Array: string[] = []; + + endpoints + .sort(byRequests) + .forEach(({ endpoint, requests: requestsToEndpoint }) => { + const str = ` ${endpoint} - ${requestsToEndpoint} requests`; + + if (endpoint.startsWith('404:')) { + _404Array.push(str); + } else { + outputStrings.push( + endpoint === '' ? chalk.gray(str) : str + ); + } + }); + + if (_404Array.length) { + if (show404s) { + _404Array.forEach((str) => outputStrings.push(chalk.gray(str))); + } else { + outputStrings.push( + chalk.gray( + ` [${_404Array.length} 404s elided, use --show-404s to view]` + ) + ); + } + } + + outputStrings.push(''); + } + ); + + outputStrings.push( + ' :PERCENTILES:', + ` fastest: ${ + requestPercentiles?.fastest !== undefined + ? percentileToString(requestPercentiles.fastest) + : '' + }`, + ` 50%<=: ${ + requestPercentiles?.percentile_50 !== undefined + ? percentileToString(requestPercentiles.percentile_50) + : '' + }`, + ` 90%<=: ${ + requestPercentiles?.percentile_90 !== undefined + ? percentileToString(requestPercentiles.percentile_90) + : '' + }`, + ` 95%<=: ${ + requestPercentiles?.percentile_95 !== undefined + ? percentileToString(requestPercentiles.percentile_95) + : '' + }`, + ` 99%<=: ${ + requestPercentiles?.percentile_99 !== undefined + ? percentileToString(requestPercentiles.percentile_99) + : '' + }`, + ` 99.9%<=: ${ + requestPercentiles?.percentile_999 !== undefined + ? percentileToString(requestPercentiles.percentile_999) + : '' + }`, + ` 99.99%<=: ${ + requestPercentiles?.percentile_9999 !== undefined + ? percentileToString(requestPercentiles.percentile_9999) + : '' + }`, + ` slowest: ${ + requestPercentiles?.slowest !== undefined + ? percentileToString(requestPercentiles.slowest) + : '' + }` + ); + } + + outputStrings.push(`\n::LIMIT LOG::${limitedLogStats.length ? '\n' : ''}`); + + if (!limitedLogStats.length) { + outputStrings.push(' '); + } else { + limitedLogStats.forEach(({ owner, token, header, ip, until }) => { + const now = Date.now(); + const banned = until > now; + + if (owner && token && header !== undefined) { + addAuthInfo(owner, token, header, banned); + } else if (ip) { + outputStrings.push(` ip: ${ip}`); + } else { + throw new GuruMeditationError('encountered malformed limit log data'); + } + + outputStrings.push( + ` status: ${ + !banned + ? chalk.gray('expired') + : chalk.red.bold( + `banned until ${new Date(until - now).toLocaleString()}` + ) + }`, + '' + ); + }); + } + + log(outputStrings.join('\n')); + + debug(`writing out results to cache at ${cachePath}`); + await jsonFile.writeFile(cachePath, previousResults); + + log('execution complete'); + process.exit(0); + } catch (error) { + throw new AppError(`${error}`); + } +}; + +export default invoked().catch((error: Error) => { + log.error(error.message); + process.exit(2); +}); + +type Percentile = { + duration: number; + endpoint: number; + status: number; + method: number; +}; + +function percentileToString(percentile: Percentile): string { + return `${percentile.duration}ms\t${percentile.method}\t${percentile.status}\t${percentile.endpoint}`; +} diff --git a/external-scripts/prune-data.ts b/external-scripts/prune-data.ts new file mode 100644 index 0000000..929e773 --- /dev/null +++ b/external-scripts/prune-data.ts @@ -0,0 +1,266 @@ +/* eslint-disable unicorn/no-process-exit */ +import { toss } from 'toss-expression'; + +import { + AppError, + GuruMeditationError, + InvalidAppEnvironmentError +} from 'named-app-errors'; + +import { superDeleteElectionAndRelatedBallots } from 'universe/backend'; +import { getElectionsDb } from 'universe/backend/db'; +import { getEnv } from 'universe/backend/env'; +import { debugNamespace as namespace } from 'universe/constants'; + +import { debugFactory } from 'multiverse/debug-extended'; +import { getDb } from 'multiverse/mongo-schema'; + +import type { Document, ObjectId, WithId } from 'mongodb'; +import type { Promisable } from 'type-fest'; + +const debugNamespace = `${namespace}:prune-data`; + +const log = debugFactory(debugNamespace); +const debug = debugFactory(`${debugNamespace}:debug`); + +type DataLimit = { + limit: { maxBytes: number } | { maxDocuments: number }; + orderBy?: string; + deleteFn?: (thresholdEntry: WithId) => Promisable; +}; + +// eslint-disable-next-line no-console +log.log = console.info.bind(console); + +// ? Ensure this next line survives Webpack +if (!globalThis.process.env.DEBUG && getEnv().NODE_ENV !== 'test') { + debugFactory.enable( + `${debugNamespace},${debugNamespace}:*,-${debugNamespace}:debug` + ); +} + +// * Add new env var configurations here +const getDbCollectionLimits = (env: ReturnType) => { + const limits: Record> = { + root: { + 'request-log': { + limit: { + maxBytes: + env.PRUNE_DATA_MAX_LOGS_BYTES && env.PRUNE_DATA_MAX_LOGS_BYTES > 0 + ? env.PRUNE_DATA_MAX_LOGS_BYTES + : toss( + new InvalidAppEnvironmentError( + 'PRUNE_DATA_MAX_LOGS_BYTES must be greater than zero' + ) + ) + } + }, + 'limited-log': { + limit: { + maxBytes: + env.PRUNE_DATA_MAX_BANNED_BYTES && env.PRUNE_DATA_MAX_BANNED_BYTES > 0 + ? env.PRUNE_DATA_MAX_BANNED_BYTES + : toss( + new InvalidAppEnvironmentError( + 'PRUNE_DATA_MAX_BANNED_BYTES must be greater than zero' + ) + ) + } + } + }, + app: { + elections: { + limit: { + maxBytes: + env.PRUNE_DATA_MAX_ELECTIONS_BYTES && + env.PRUNE_DATA_MAX_ELECTIONS_BYTES > 0 + ? env.PRUNE_DATA_MAX_ELECTIONS_BYTES + : toss( + new InvalidAppEnvironmentError( + 'PRUNE_DATA_MAX_ELECTIONS_BYTES must be greater than zero' + ) + ) + }, + async deleteFn(thresholdEntry) { + const electionsDb = await getElectionsDb(); + + const ids = ( + await electionsDb + .find( + { _id: { $lte: thresholdEntry._id } }, + { projection: { _id: true } } + ) + .toArray() + ).map((entry) => entry._id); + + await Promise.all( + ids.map((id) => + superDeleteElectionAndRelatedBallots({ election_id: id.toString() }) + ) + ); + + return ids.length; + } + }, + ballots: { + limit: { + maxBytes: + env.PRUNE_DATA_MAX_BALLOTS_BYTES && env.PRUNE_DATA_MAX_BALLOTS_BYTES > 0 + ? env.PRUNE_DATA_MAX_BALLOTS_BYTES + : toss( + new InvalidAppEnvironmentError( + 'PRUNE_DATA_MAX_BALLOTS_BYTES must be greater than zero' + ) + ) + } + } + } + }; + + debug('limits: %O', limits); + return limits; +}; + +/** + * Runs maintenance on the database, ensuring collections do not grow too large. + */ +const invoked = async () => { + try { + const limits = getDbCollectionLimits(getEnv()); + + await Promise.all( + Object.entries(limits).map(async ([dbName, dbLimitsObj]) => { + debug(`using db "${dbName}"`); + const db = await getDb({ name: dbName }); + + await Promise.all( + Object.entries(dbLimitsObj).map(async ([collectionName, colLimitsObj]) => { + const name = `${dbName}.${collectionName}`; + debug(`collection "${name}" is a target for pruning`); + + const subLog = log.extend(name); + const collection = db.collection(collectionName); + const totalCount = await collection.countDocuments(); + + const { + limit: limitSpec, + orderBy = '_id', + deleteFn = undefined + } = colLimitsObj; + + const pruneCollectionAtThreshold = async ( + thresholdEntry: WithId | null, + deleteFn: DataLimit['deleteFn'], + pruneMessage: string, + noPruneMessage: string + ) => { + if (thresholdEntry) { + debug(`determined threshold entry: ${thresholdEntry._id}`); + let deletedCount: number; + + if (deleteFn) { + debug('using custom pruning strategy'); + deletedCount = await deleteFn(thresholdEntry); + } else { + debug('using default pruning strategy'); + deletedCount = ( + await collection.deleteMany({ + [orderBy]: { $lte: thresholdEntry[orderBy] } + }) + ).deletedCount; + } + + subLog(`${deletedCount} pruned (${pruneMessage})`); + } else { + subLog(`0 pruned (${noPruneMessage})`); + } + }; + + if ('maxBytes' in limitSpec) { + debug('limiting metric: document size'); + + const { maxBytes } = limitSpec; + + debug(`sorting ${name} by "${orderBy}"`); + debug( + `iteratively summing document size until limit is reached (${maxBytes} bytes)` + ); + + // ? Use $bsonSize operator to sort by most recent first, then sum + // ? them until either documents are exhausted or total size > + // ? limit, then delete the (old) documents that exist beyond the + // ? limit. + const cursor = collection.aggregate>([ + { $sort: { [orderBy]: -1 } }, + { $project: { _id: true, size: { $bsonSize: '$$ROOT' } } } + ]); + + let totalSizeBytes = 0; + let thresholdId: ObjectId | null = null; + let foundThresholdId = false; + + (await cursor.toArray()).forEach(({ _id, size }) => { + if (!thresholdId) { + thresholdId = _id; + } + + totalSizeBytes += size; + + if (!foundThresholdId) { + if (totalSizeBytes > maxBytes) { + foundThresholdId = true; + } else { + thresholdId = _id; + } + } + }); + + await pruneCollectionAtThreshold( + foundThresholdId && thresholdId ? { _id: thresholdId } : null, + deleteFn, + `${totalCount}, ${totalSizeBytes}b > ${maxBytes}b`, + `${totalCount}, ${totalSizeBytes}b <= ${maxBytes}b` + ).then(() => cursor.close()); + } else { + debug('limiting metric: document count'); + + const { maxDocuments } = limitSpec; + + if (!maxDocuments) { + throw new GuruMeditationError('invalid limit spec'); + } + + debug(`sorting ${name} by "${orderBy}"`); + debug(`skipping ${maxDocuments} entries"`); + + const cursor = collection + .find() + .sort({ [orderBy]: -1 }) + .skip(maxDocuments) + .limit(1); + + const thresholdEntry = await cursor.next(); + + await pruneCollectionAtThreshold( + thresholdEntry, + deleteFn, + `${totalCount} > ${maxDocuments}`, + `${totalCount} <= ${maxDocuments}` + ).then(() => cursor.close()); + } + }) + ); + }) + ); + + log('execution complete'); + process.exit(0); + } catch (error) { + throw new AppError(`${error}`); + } +}; + +export default invoked().catch((error: Error) => { + log.error(error.message); + process.exit(2); +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5154de7 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,51 @@ +'use strict'; + +/** + * @type {import('jest').Config} + */ +module.exports = { + restoreMocks: true, + resetMocks: true, + testEnvironment: 'node', + testRunner: 'jest-circus/runner', + // ? 24h if debugging so MMS and other tools don't choke, otherwise 1m + testTimeout: + 1000 * + 60 * + (process.env.VSCODE_INSPECTOR_OPTIONS + ? 60 * 24 + : process.platform === 'win32' + ? 5 + : 1), + // ? Minimum of 2 concurrent tests executed at once; maximum of cpu cores - 1 + maxConcurrency: Math.max(require('node:os').cpus().length - 1, 2), + verbose: false, + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + // ! If changed, also update these aliases in tsconfig.json, + // ! babel.config.js, webpack.config.js, next.config.ts, and .eslintrc.js + moduleNameMapper: { + '^universe/(.*)$': '/src/$1', + '^multiverse/(.*)$': '/lib/$1', + '^testverse/(.*)$': '/test/$1', + '^externals/(.*)$': '/external-scripts/$1', + '^types/(.*)$': '/types/$1', + '^package$': '/package.json', + // ? These are used at various points (including at compile time by + // ? Next.js) to get mongo schema configuration and/or test dummy data. + // ! Must be defined if using @xunnamius/mongo-schema + '^configverse/get-schema-config$': '/src/backend/db.ts', + // ! Must be defined if using @xunnamius/mongo-test + '^configverse/get-dummy-data$': '/test/db.ts' + }, + setupFilesAfterEnv: ['./test/setup.ts'], + collectCoverageFrom: [ + 'src/**/*.ts*', + 'lib/**/*.ts*', + 'external-scripts/**/*.ts*', + '!**/*.test.*' + ], + // ? Make sure jest-haste-map doesn't try to parse and cache fixtures + modulePathIgnorePatterns: ['/test/fixtures/'], + // ? Transform specific third-party ESM packages into CJS using babel + transformIgnorePatterns: ['node_modules/(?!(dot-prop|unfetch)/)'] +}; diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..08a7a6c --- /dev/null +++ b/lib/README.md @@ -0,0 +1,5 @@ +# Libraries + +This folder holds code frequently passed around between projects. If passed +around enough, it will be turned into an npm module. Until then, they get +dogfooded here as multiverse libs. diff --git a/lib/debug-extended/index.ts b/lib/debug-extended/index.ts new file mode 100644 index 0000000..119bfff --- /dev/null +++ b/lib/debug-extended/index.ts @@ -0,0 +1,47 @@ +import getDebugger from 'debug'; +import type { Debug, Debugger } from 'debug'; + +export type { Debug, Debugger }; + +/** + * A Debug factory interface that returns `ExtendedDebugger` instances. + */ +export interface ExtendedDebug extends Debug { + (...args: Parameters): ExtendedDebugger; +} + +/** + * A Debugger interface extended with convenience methods. + */ +export interface ExtendedDebugger extends Debugger { + error: Debugger; + warn: Debugger; + extend: (...args: Parameters) => ExtendedDebugger; +} + +/** + * An `ExtendedDebug` instance that returns an `ExtendedDebugger` instance via + * `extendDebugger`. + */ +const debugFactory = ((...args: Parameters) => { + return extendDebugger(getDebugger(...args)); +}) as ExtendedDebug; + +Object.assign(debugFactory, getDebugger); + +export { debugFactory }; + +/** + * Extends a `Debugger` instance with several convenience methods, returning + * what would more accurately be called an `ExtendedDebugger` instance. + */ +export function extendDebugger(instance: Debugger) { + const extend = instance.extend.bind(instance); + const finalInstance = instance as ExtendedDebugger; + + finalInstance.error = finalInstance.extend(''); + finalInstance.warn = finalInstance.extend(''); + finalInstance.extend = (...args) => extendDebugger(extend(...args)); + + return finalInstance; +} diff --git a/lib/debug-extended/package.json b/lib/debug-extended/package.json new file mode 100644 index 0000000..dd44c03 --- /dev/null +++ b/lib/debug-extended/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/debug-extended" +} diff --git a/lib/debug-extended/unit.test.ts b/lib/debug-extended/unit.test.ts new file mode 100644 index 0000000..7101a27 --- /dev/null +++ b/lib/debug-extended/unit.test.ts @@ -0,0 +1,38 @@ +import { debugFactory, extendDebugger } from 'multiverse/debug-extended'; +import { debug as oldDebug } from 'debug'; + +describe('::debugFactory', () => { + it('returns ExtendedDebugger instances', async () => { + expect.hasAssertions(); + + const debug = debugFactory('namespace'); + + expect(debug).toHaveProperty('error'); + expect(debug).toHaveProperty('warn'); + expect(debug).toHaveProperty('extend'); + }); + + it('returns an instance with error, warn, and extend', async () => { + expect.hasAssertions(); + + const debug = debugFactory('namespace'); + const extended = debug.extend('extended'); + + expect(extended).toHaveProperty('error'); + expect(extended).toHaveProperty('warn'); + expect(extended).toHaveProperty('extend'); + }); +}); + +describe('::extendDebugger', () => { + it('returns an instance with error, warn, and extend', async () => { + expect.hasAssertions(); + + const debug = oldDebug('namespace'); + const extended = extendDebugger(debug); + + expect(extended).toHaveProperty('error'); + expect(extended).toHaveProperty('warn'); + expect(extended).toHaveProperty('extend'); + }); +}); diff --git a/lib/find-project-root/index.ts b/lib/find-project-root/index.ts new file mode 100644 index 0000000..037f430 --- /dev/null +++ b/lib/find-project-root/index.ts @@ -0,0 +1,33 @@ +import { dirname } from 'node:path'; +import { sync as findUpSync } from 'find-up'; +import { GuruMeditationError } from 'named-app-errors'; +import { toss } from 'toss-expression'; + +const memory = { rootPath: null } as { rootPath: string | null }; + +/** + * Overwrite the memoized findProjectRoot result with an explicit value. Useful + * in testing environments and complex setups. + */ +export function setProjectRoot(rootPath: string | null) { + memory.rootPath = rootPath; +} + +/** + * Synchronously finds the root of a project by walking up parent + * directories beginning at `process.cwd()` and looking for certain files/dirs. + */ +export function findProjectRoot() { + return (memory.rootPath = + memory.rootPath ?? + dirname( + findUpSync('next.config.js') || + findUpSync('projector.config.js') || + findUpSync('.git') || + toss( + new GuruMeditationError( + 'could not find project root: none of "next.config.js", "projector.config.js", nor ".git" were found in any ancestor directory' + ) + ) + )); +} diff --git a/lib/find-project-root/package.json b/lib/find-project-root/package.json new file mode 100644 index 0000000..2dee606 --- /dev/null +++ b/lib/find-project-root/package.json @@ -0,0 +1,4 @@ +{ + "name": "@xunnamius/find-project-root", + "version": "0.0.0-development" +} diff --git a/lib/find-project-root/unit.test.ts b/lib/find-project-root/unit.test.ts new file mode 100644 index 0000000..816048b --- /dev/null +++ b/lib/find-project-root/unit.test.ts @@ -0,0 +1,59 @@ +import { findProjectRoot, setProjectRoot } from 'multiverse/find-project-root'; +import { sync as findUpSync } from 'find-up'; +import { asMockedFunction } from '@xunnamius/jest-types'; + +jest.mock('find-up'); + +const mockFindUpSync = asMockedFunction(findUpSync); + +describe('::findProjectRoot', () => { + it('find the project root unless all expected paths not encountered', async () => { + expect.hasAssertions(); + + mockFindUpSync.mockImplementationOnce(() => '/some/x/path/next.config.js'); + + expect(findProjectRoot()).toBe('/some/x/path'); + + setProjectRoot(null); + mockFindUpSync.mockImplementationOnce(() => undefined); + mockFindUpSync.mockImplementationOnce(() => '/some/y/path/projector.config.js'); + + expect(findProjectRoot()).toBe('/some/y/path'); + + setProjectRoot(null); + mockFindUpSync.mockImplementationOnce(() => undefined); + mockFindUpSync.mockImplementationOnce(() => undefined); + mockFindUpSync.mockImplementationOnce(() => '/some/z/path/.git'); + + expect(findProjectRoot()).toBe('/some/z/path'); + + setProjectRoot(null); + mockFindUpSync.mockImplementation(() => undefined); + + expect(() => findProjectRoot()).toThrow('could not find project root'); + }); + + it('memoizes the result', async () => { + expect.hasAssertions(); + + mockFindUpSync.mockImplementationOnce(() => '/some/x/path/next.config.js'); + expect(findProjectRoot()).toBe('/some/x/path'); + + mockFindUpSync.mockImplementation(() => undefined); + expect(findProjectRoot()).toBe('/some/x/path'); + }); +}); + +describe('::setProjectRoot', () => { + it('controls output of findProjectRoot', async () => { + expect.hasAssertions(); + + mockFindUpSync.mockImplementationOnce(() => '/some/w/path/next.config.js'); + + setProjectRoot('/some/x/path/next.config.js'); + expect(findProjectRoot()).toBe('/some/x/path/next.config.js'); + expect(findProjectRoot()).toBe('/some/x/path/next.config.js'); + setProjectRoot(null); + expect(findProjectRoot()).toBe('/some/w/path'); + }); +}); diff --git a/lib/is-plain-object/index.ts b/lib/is-plain-object/index.ts new file mode 100644 index 0000000..f01eea0 --- /dev/null +++ b/lib/is-plain-object/index.ts @@ -0,0 +1,5 @@ +import { isPlainObject as originalIsPlainObject } from 'is-plain-object'; + +export function isPlainObject(obj: unknown): obj is Record { + return originalIsPlainObject(obj); +} diff --git a/lib/is-plain-object/package.json b/lib/is-plain-object/package.json new file mode 100644 index 0000000..e91347e --- /dev/null +++ b/lib/is-plain-object/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/is-plain-object" +} diff --git a/lib/jest-expect-matching-errors/index.ts b/lib/jest-expect-matching-errors/index.ts new file mode 100644 index 0000000..32fe71c --- /dev/null +++ b/lib/jest-expect-matching-errors/index.ts @@ -0,0 +1,50 @@ +import { TrialError } from 'named-app-errors'; +import { isPromise } from 'node:util/types'; + +import type { Promisable } from 'type-fest'; + +// TODO: make this into a package alongside the other helpers like itemExists +// TODO: and some of the jest multiverse libs. Add a descriptor/error msg +// TODO: functionality too! + +/** + * Maps each element of the `spec` array into a Jest expectation asserting that + * `errorFn` either throws an error or rejects. If an assertion fails, a + * helpful error message is thrown. + */ +export async function expectExceptionsWithMatchingErrors< + T extends [params: unknown, errorMessage: string][] +>(spec: T, errorFn: (params: T[number][0], index: number) => Promisable) { + await Promise.all( + spec.map(async ([params, message], index) => { + let result = undefined; + let error = undefined; + let errored = false; + + try { + result = errorFn(params, index); + if (isPromise(result)) { + result = await result; + } + } catch (error_) { + errored = true; + error = error_; + } + + if (!errored) { + throw new TrialError( + `assertion failed: an exception did not occur for spec element at index #${index}: ${JSON.stringify( + params, + undefined, + 2 + )}\n\nExpected error message: ${message}` + ); + } + + return expect({ index, params, error }).toMatchObject({ + index, + error: { message } + }); + }) + ); +} diff --git a/lib/jest-expect-matching-errors/package.json b/lib/jest-expect-matching-errors/package.json new file mode 100644 index 0000000..cc3bcdc --- /dev/null +++ b/lib/jest-expect-matching-errors/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/jest-expect-matching-errors" +} diff --git a/lib/jest-mock-date/index.ts b/lib/jest-mock-date/index.ts new file mode 100644 index 0000000..2e1c47d --- /dev/null +++ b/lib/jest-mock-date/index.ts @@ -0,0 +1,21 @@ +/** + * The mock Date.now() value returned after calling `useMockDateNow`. + */ +export const mockDateNowMs = Date.now(); + +/** + * Sets up a Jest spy on the `Date` object's `now` method such that it returns + * `mockNow` or `mockDateNowMs` (default) rather than the actual date. If you + * want to restore the mock, you will have to do so manually (or use Jest + * configuration to do so automatically). + * + * This is useful when testing against/playing with dummy data containing values + * derived from the current time (i.e. unix epoch). + */ +export function useMockDateNow(options?: { mockNow?: number }) { + beforeEach(() => { + jest + .spyOn(Date, 'now') + .mockImplementation(() => options?.mockNow || mockDateNowMs); + }); +} diff --git a/lib/jest-mock-date/package.json b/lib/jest-mock-date/package.json new file mode 100644 index 0000000..0706309 --- /dev/null +++ b/lib/jest-mock-date/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/jest-mock-date" +} diff --git a/lib/jest-mongo-object-id-pseudo-sort/index.ts b/lib/jest-mongo-object-id-pseudo-sort/index.ts new file mode 100644 index 0000000..dfb2951 --- /dev/null +++ b/lib/jest-mongo-object-id-pseudo-sort/index.ts @@ -0,0 +1,16 @@ +import type { WithId } from 'mongodb'; + +/** + * A sort predicate meant to be used with {@link Array.prototype.sort} when + * ordering items by their `ObjectId`. In the majority of cases, the result + * should be the same as what MongoDb would return with `{ sort: { _id: 1 }}`). + */ +export function objectIdPseudoSortPredicate(order: 'ascending' | 'descending') { + return (a: WithId, b: WithId) => { + const [first, second] = order === 'ascending' ? [a, b] : [b, a]; + return ( + Number.parseInt(first._id.toString(), 16) - + Number.parseInt(second._id.toString(), 16) + ); + }; +} diff --git a/lib/jest-mongo-object-id-pseudo-sort/package.json b/lib/jest-mongo-object-id-pseudo-sort/package.json new file mode 100644 index 0000000..098266c --- /dev/null +++ b/lib/jest-mongo-object-id-pseudo-sort/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/jest-mongo-object-id-pseudo-sort" +} diff --git a/lib/json-node-fetch/index.ts b/lib/json-node-fetch/index.ts new file mode 100644 index 0000000..f045eb6 --- /dev/null +++ b/lib/json-node-fetch/index.ts @@ -0,0 +1,368 @@ +import { isNativeError } from 'node:util/types'; +import { makeNamedError } from 'named-app-errors'; + +import fetch, { + FetchError, + Headers, + type Response, + type BodyInit, + type RequestInit +} from 'node-fetch'; + +import type { JsonObject, JsonPrimitive } from 'type-fest'; + +export const JsonContentType = 'application/json' as const; + +/** + * Represents a JSON Fetch error. + */ +export class JsonFetchError< + T extends JsonObject | JsonPrimitive | undefined +> extends FetchError { + constructor( + public readonly res: Response | undefined, + public readonly json: T, + message: string + ) { + super(message, 'json-fetch-error'); + } +} +makeNamedError(JsonFetchError, 'JsonFetchError'); + +/** + * Options to configure how jsonFetch executes. + * + * @see https://github.com/node-fetch/node-fetch#options + */ +export type JsonRequestInit = Omit & { + /** + * If `true`, jsonFetch will reject when `response.ok` is not `true`; if + * `false`, `json` will be undefined and `error` will be an empty object. + * + * @default false + * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/ok + */ + rejectIfNotOk?: boolean; + /** + * If `true`, jsonFetch will reject when a response is missing the + * `application/json` content-type header; if `false`, `json` will be + * undefined and `error` will be an empty object. + * + * @default false + */ + rejectIfNonJsonContentType?: boolean; + /** + * The request body to send. Automatically stringified (via `JSON.stringify`) + * if request content-type is `application/json`. + * + * Note that this type is loose enough to accept JSON objects, but if you're + * not using the `application/json` content-type when passing a JSON object as + * the body then jsonFetch will reject with an error. + */ + body?: BodyInit | JsonObject | JsonPrimitive; +}; + +/** + * The mutable default options for all `jsonFetch` calls. Keys will be + * overridden by the optional `options` object passed into each call, e.g. + * `jsonFetch(url, options)`. + * + * Note: you must use `credentials: 'include'` to include cookies with your + * requests. This is not the default setting. + * + * **WARN: this setting MUST only be used in "end-developer" source, not in + * libraries or anything that is meant to be imported into higher-order code or + * you run the risk of terrible conflicts!** + * + * @see https://github.com/node-fetch/node-fetch#options + */ +export const globalJsonRequestOptions: JsonRequestInit = { + headers: { 'content-type': JsonContentType }, + rejectIfNotOk: false, + rejectIfNonJsonContentType: false +}; + +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and the response body parsed as JSON under either `error` + * (if the response has a non-2xx status) or `json`. + * + * If the response was not received with an `application/json` content-type + * header or has a non-2xx status _and_ unparseable response body, `json` will + * be undefined and `error` will be an empty object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header or 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { + * method: 'POST', + * headers: { authorization: `Bearer ${apiKey}` }, + * body: requestData + * } + * ); + * + * if (error) { + * console.error(error?.reason ?? (res.ok + * ? 'bad json' + * : res.statusText)); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init?: Omit & { + rejectIfNotOk?: false; + rejectIfNonJsonContentType?: false; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and the response body parsed as JSON under either `error` + * (if the response has a non-2xx status) or `json`. + * + * If the response was received with a non-2xx status _and_ unparseable response + * body, `json` will be undefined and `error` will be an empty object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, or 3) the response was received with a content-type + * header other than `application/json`. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * + * try { + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { rejectIfNonJsonContentType: true } + * ); + * + * if (error) { + * console.error(error?.reason ?? res.statusText); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-json response bodies + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit & { + rejectIfNotOk?: false; + rejectIfNonJsonContentType: true; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and either the response body parsed as JSON under `json` + * or, if the response was received with a content-type header other than + * `application/json`, an empty object under `error`. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, or 3) the response was received with a non-2xx status. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * + * try { + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { rejectIfNotOk: true } + * ); + * + * if (error) { + * console.error(error?.reason ?? 'bad json'); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-2xx responses + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit & { + rejectIfNotOk: true; + rejectIfNonJsonContentType?: false; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and and the response body parsed as JSON under `json`. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, 3) the response was received with a content-type header + * other than `application/json`, or 4) the response was received with a non-2xx + * status. + * + * Hence, when jsonFetch is called in this way, `json` will always be defined + * and `error` will always be undefined. + * + * @example + * ``` + * try { + * const url = 'https://some.resource.com/data.json'; + * const { json } = await jsonFetch(url, { + * rejectIfNotOk: true, + * rejectIfNonJsonContentType: true + * }); + * doSomethingWith(json); + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-2xx/non-json response bodies + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit & { + rejectIfNotOk: true; + rejectIfNonJsonContentType: true; + } +): Promise<{ + res: Response; + json: JsonType; + error: undefined; +}>; +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>(url: string, init?: JsonRequestInit): Promise { + const parsedOptions = { + ...globalJsonRequestOptions, + ...init + }; + + if (parsedOptions.headers) { + parsedOptions.headers = new Headers(parsedOptions.headers); + + if (parsedOptions.headers.get('content-type') === JsonContentType) { + try { + parsedOptions.body = JSON.stringify(parsedOptions.body); + } catch (error) { + throw new JsonFetchError( + undefined, + undefined, + `failed to stringify request body: ${ + isNativeError(error) ? error.message : error + }` + ); + } + } + } + + const res = await fetch(url, parsedOptions as RequestInit); + const responseContentType = res.headers.get('content-type'); + + let parseError = ''; + let json: JsonType | undefined = undefined; + let error: Partial | undefined = undefined; + + try { + json = await res.json(); + } catch (error) { + parseError = `${isNativeError(error) ? error.message : error}`; + } + + if (!res.ok && parsedOptions.rejectIfNotOk) { + throw new JsonFetchError( + res, + json, + `response status code ${res.status} was not in the range 200-299` + ); + } + + if ( + responseContentType !== JsonContentType && + parsedOptions.rejectIfNonJsonContentType + ) { + throw new JsonFetchError( + res, + json, + `received response ${ + responseContentType + ? `with unexpected content-type "${responseContentType}"` + : 'without a content-type' + } (expected "application/json")` + ); + } + + if (parseError && responseContentType === JsonContentType) { + throw new JsonFetchError( + res, + json, + `failed to parse response body: ${parseError}` + ); + } + + if (responseContentType !== JsonContentType || parseError) { + json = undefined; + error = {}; + } else if (!res.ok) { + error = json as unknown as Partial; + json = undefined; + } + + return { res, json, error }; +} diff --git a/lib/json-node-fetch/package.json b/lib/json-node-fetch/package.json new file mode 100644 index 0000000..32b7403 --- /dev/null +++ b/lib/json-node-fetch/package.json @@ -0,0 +1,3 @@ +{ + "name": "json-node-fetch" +} diff --git a/lib/json-node-fetch/test/integration.test.ts b/lib/json-node-fetch/test/integration.test.ts new file mode 100644 index 0000000..476f7df --- /dev/null +++ b/lib/json-node-fetch/test/integration.test.ts @@ -0,0 +1,180 @@ +import { createServer, type ServerResponse, type IncomingMessage } from 'node:http'; +import { globalJsonRequestOptions, jsonFetch } from 'multiverse/json-node-fetch'; + +let cleanupFn = () => undefined; + +afterAll(() => cleanupFn()); + +describe('::jsonFetch', () => { + it('works', async () => { + expect.hasAssertions(); + + let status = 200; + let header: Record = { 'content-type': 'application/json' }; + let data: unknown = { hello: 'world!' }; + + const server = createServer((_req: IncomingMessage, res: ServerResponse) => { + res.statusCode = status; + + const headers = Object.entries(header); + headers.length && res.setHeader(...headers[0]); + + let dat; + try { + if (data !== '{"broken') { + dat = JSON.stringify(data); + } + } catch { + dat = data; + } + + res.end(dat); + }); + + cleanupFn = () => void server.close(); + + const port = await new Promise((resolve, reject) => { + server.listen(() => { + const addr = server.address(); + !addr || typeof addr === 'string' + ? reject(new Error('assertion failed unexpectedly')) + : resolve(addr.port); + }); + }); + + const localUrl = `http://localhost:${port}/`; + let { res, json, error } = await jsonFetch(localUrl); + + expect(res.url).toBe(localUrl); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + status = 555; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual(data); + + status = 200; + header = {}; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + await expect( + jsonFetch(localUrl, { rejectIfNonJsonContentType: true }) + ).rejects.toThrow('without a content-type'); + + header = { 'content-type': 'text/unknown' }; + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + await expect( + jsonFetch(localUrl, { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toThrow('"text/unknown"'); + + data = { hello: 'world!' }; + + await expect( + jsonFetch(localUrl, { rejectIfNonJsonContentType: true }).catch( + (error) => error.json + ) + ).resolves.toStrictEqual(data); + + header = { 'content-type': 'application/json' }; + status = 666; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual(data); + + data = {}; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + globalJsonRequestOptions.rejectIfNotOk = true; + + await expect(jsonFetch(localUrl)).rejects.toThrow('666'); + + data = { hello: 'worlds!' }; + status = 201; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + delete globalJsonRequestOptions.rejectIfNotOk; + + header = { 'CONTENT-TYPE': 'application/json' }; + status = 200; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + await expect( + jsonFetch(localUrl, { + headers: { 'CONTENT-TYPE': 'application/json' }, + method: 'POST', + body: { hi: 'world' } + }) + ).resolves.toBeDefined(); + + header = {}; + status = 500; + data = '{"broken'; + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true + }) + ).rejects.toThrow(/without a content-type/); + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + header = { 'content-type': 'application/json' }; + + await expect(jsonFetch(localUrl)).rejects.toThrow( + /failed to parse response body:/ + ); + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true, + rejectIfNotOk: true + }) + ).rejects.toThrow(/500/); + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true + }) + ).rejects.toThrow(/failed to parse response body:/); + + const badObj = { badObj: {} }; + badObj.badObj = badObj; + + await expect( + jsonFetch(localUrl, { + body: badObj + }) + ).rejects.toThrow(/failed to stringify request body:/); + }); +}); diff --git a/lib/json-node-fetch/test/unit.test.ts b/lib/json-node-fetch/test/unit.test.ts new file mode 100644 index 0000000..a341d25 --- /dev/null +++ b/lib/json-node-fetch/test/unit.test.ts @@ -0,0 +1,317 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import fetch, { Headers } from 'node-fetch'; +import { globalJsonRequestOptions, jsonFetch } from 'multiverse/json-node-fetch'; +import { toss } from 'toss-expression'; + +import type { JsonObject } from 'type-fest'; +import type { Response } from 'node-fetch'; + +jest.mock('node-fetch', (): typeof import('node-fetch') => { + const fetch = jest.fn(); + // ? We need to mock Headers (earlier than when beforeEach runs) + // @ts-expect-error: defining Headers + fetch.Headers = jest.requireActual('node-fetch').Headers; + // ? We also need to mock FetchError + // @ts-expect-error: defining FetchError + fetch.FetchError = jest.requireActual('node-fetch').FetchError; + return fetch as unknown as typeof import('node-fetch'); +}); + +const mockFetch = asMockedFunction(fetch); + +const mockedFetchResult = {} as unknown as Response; +let mockedFetchResultJson = {} as JsonObject | Error | string; + +beforeEach(() => { + mockFetch.mockImplementation(async () => mockedFetchResult); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = true; + mockedFetchResult.status = 200; + mockedFetchResult.headers = new Headers(); + + mockedFetchResult.json = jest.fn(async () => { + return typeof mockedFetchResultJson === 'string' || + mockedFetchResultJson instanceof Error + ? toss(mockedFetchResultJson) + : mockedFetchResultJson; + }); +}); + +describe('::jsonFetch', () => { + it('fetches a resource and returns the response itself and the body as json', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + }); + + it('returns empty error if the response has a non-json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + + mockedFetchResult.headers.set('content-type', 'something/else'); + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + }); + + it('rejects if the response has a non-json content-type and rejectIfNonJsonContentType is true', async () => { + expect.hasAssertions(); + + mockedFetchResultJson = { hello: 'world' }; + + await expect( + jsonFetch('some-url', { rejectIfNonJsonContentType: true }) + ).rejects.toThrow( + 'received response without a content-type (expected "application/json")' + ); + + mockedFetchResult.headers.set('content-type', 'something/else'); + + await expect( + jsonFetch('some-url', { rejectIfNonJsonContentType: true }) + ).rejects.toThrow( + 'received response with unexpected content-type "something/else" (expected "application/json")' + ); + }); + + it('rejects if the response has a json content-type but non-json body regardless of status code', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ? in JSON at position ??' + ); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ?? in JSON at position ?' + ); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 413; + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ?? in JSON at position ?' + ); + }); + + it('returns error if the response has a non-2xx status code', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 567; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: mockedFetchResultJson + }); + }); + + it('returns empty error if the response has a non-2xx status code and non-json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/something-or-other'); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = false; + mockedFetchResult.status = 403; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + }); + + it('rejects if the response has a non-2xx status code and rejectIfNotOk is true', async () => { + expect.hasAssertions(); + + globalJsonRequestOptions.rejectIfNotOk = true; + + try { + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 567; + globalJsonRequestOptions.rejectIfNotOk = false; + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + + // ? Should also reject with an HttpError even if JSON is not parsable + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + } finally { + delete globalJsonRequestOptions.rejectIfNotOk; + } + }); + + it('rejects if the response has a non-2xx status code, non-json content-type, or non-json body when both rejectIfNonJsonContentType and rejectIfNotOk are true', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = false; + mockedFetchResult.status = 404; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: mockedFetchResultJson }); + + mockedFetchResult.headers.set('content-type', 'application/something'); + mockedFetchResult.ok = true; + mockedFetchResult.status = 200; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: mockedFetchResultJson }); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: undefined }); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: undefined }); + }); + + it('rejects on failure to stringify request body with json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + + const badObj = { badObj: {} }; + badObj.badObj = badObj; + + await expect( + jsonFetch('some-url', { + headers: { 'content-type': 'application/json' }, + body: 'hello' + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: { hello: 'world' } + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + headers: { 'content-type': 'something/else' }, + body: badObj + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: badObj + }) + ).rejects.toThrow('failed to stringify request body:'); + + const spy = jest + .spyOn(JSON, 'stringify') + .mockImplementation(() => + toss(new SyntaxError('unexpected token ? in JSON at position ??')) + ); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: 'whatever' + }) + ).rejects.toThrow( + 'failed to stringify request body: unexpected token ? in JSON at position ??' + ); + + spy.mockImplementation(() => toss('unexpected token ?? in JSON at position ?')); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: 'whatever' + }) + ).rejects.toThrow( + 'failed to stringify request body: unexpected token ?? in JSON at position ?' + ); + }); + + it('handles empty global options', async () => { + expect.hasAssertions(); + + const oldValue = globalJsonRequestOptions.headers; + delete globalJsonRequestOptions.headers; + + try { + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + } finally { + globalJsonRequestOptions.headers = oldValue; + } + }); +}); diff --git a/lib/json-unfetch/index.ts b/lib/json-unfetch/index.ts new file mode 100644 index 0000000..5432d9c --- /dev/null +++ b/lib/json-unfetch/index.ts @@ -0,0 +1,456 @@ +import { isNativeError } from 'node:util/types'; +import unfetch from 'unfetch'; +import { makeNamedError } from 'named-app-errors'; + +import type { JsonObject, JsonPrimitive } from 'type-fest'; + +const JsonContentType = 'application/json'; + +// ? Some types are being taken from TypeScript's global built-in DOM library + +/** + * Represents a JSON (un)Fetch error. + */ +export class JsonUnfetchError< + T extends JsonObject | JsonPrimitive | undefined +> extends Error { + constructor( + public readonly res: Response | undefined, + public readonly json: T, + message: string + ) { + super(message); + } +} +makeNamedError(JsonUnfetchError, 'JsonUnfetchError'); + +export type Response = Awaited>; +export type RequestInit = NonNullable[1]>; +export type BodyInit = RequestInit['body']; + +/** + * Options to configure how jsonFetch executes. + * + * @see https://github.com/developit/unfetch#api + */ +export type JsonRequestInit = Omit & { + /** + * Enables SWR compatibility mode when `true`. Favor importing `swrFetch`, a + * SWR syntactic sugar function, to use SWR compatibility rather than setting + * this manually. + * + * @default false + */ + swr?: boolean; + /** + * If `true`, jsonFetch will reject when `response.ok` is not `true`; if + * `false`, `json` will be undefined and `error` will be an empty object. + * + * @default false + * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/ok + */ + rejectIfNotOk?: boolean; + /** + * If `true`, jsonFetch will reject when a response is missing the + * `application/json` content-type header; if `false`, `json` will be + * undefined and `error` will be an empty object. + * + * @default false + */ + rejectIfNonJsonContentType?: boolean; + /** + * The request body to send. Automatically stringified (via `JSON.stringify`) + * if request content-type is `application/json`. + * + * Note that this type is loose enough to accept JSON objects, but if you're + * not using the `application/json` content-type when passing a JSON object as + * the body then jsonFetch will reject with an error. + */ + body?: BodyInit | JsonObject | JsonPrimitive; +}; + +/** + * The mutable default options for all `jsonFetch` calls. Keys will be + * overridden by the optional `options` object passed into each call, e.g. + * `jsonFetch(url, options)`. + * + * Note: you must use `credentials: 'include'` to include cookies with your + * requests. This is not the default setting. + * + * @see https://github.com/developit/unfetch#api + */ +export const globalJsonRequestOptions: JsonRequestInit = { + headers: { 'content-type': JsonContentType }, + rejectIfNotOk: false, + rejectIfNonJsonContentType: false +}; + +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and the response body parsed as JSON under either `error` + * (if the response has a non-2xx status) or `json`. + * + * If the response was not received with an `application/json` content-type + * header or has a non-2xx status _and_ unparseable response body, `json` will + * be undefined and `error` will be an empty object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header or 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { + * method: 'POST', + * headers: { authorization: `Bearer ${apiKey}` }, + * body: requestData + * } + * ); + * + * if (error) { + * console.error(error?.reason ?? (res.ok + * ? 'bad json' + * : res.statusText)); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init?: Omit< + JsonRequestInit, + 'swr' | 'rejectIfNotOk' | 'rejectIfNonJsonContentType' + > & { + swr?: false; + rejectIfNotOk?: false; + rejectIfNonJsonContentType?: false; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and the response body parsed as JSON under either `error` + * (if the response has a non-2xx status) or `json`. + * + * If the response was received with a non-2xx status _and_ unparseable response + * body, `json` will be undefined and `error` will be an empty object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, or 3) the response was received with a content-type + * header other than `application/json`. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * + * try { + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { rejectIfNonJsonContentType: true } + * ); + * + * if (error) { + * console.error(error?.reason ?? res.statusText); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-json response bodies + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit< + JsonRequestInit, + 'swr' | 'rejectIfNotOk' | 'rejectIfNonJsonContentType' + > & { + swr?: false; + rejectIfNotOk?: false; + rejectIfNonJsonContentType: true; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and either the response body parsed as JSON under `json` + * or, if the response was received with a content-type header other than + * `application/json`, an empty object under `error`. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, or 3) the response was received with a non-2xx status. + * + * @example + * ``` + * type ResJson = { myNumber: number }; + * type ResErr = { reason: string }; + * + * try { + * const { res, json, error } = await jsonFetch( + * 'api/endpoint', + * { rejectIfNotOk: true } + * ); + * + * if (error) { + * console.error(error?.reason ?? 'bad json'); + * } else { + * console.log(`number is: ${json?.myNumber}`); + * } + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-2xx responses + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit< + JsonRequestInit, + 'swr' | 'rejectIfNotOk' | 'rejectIfNonJsonContentType' + > & { + swr?: false; + rejectIfNotOk: true; + rejectIfNonJsonContentType?: false; + } +): Promise<{ + res: Response; + json: JsonType | undefined; + error: Partial | undefined; +}>; +/** + * Fetches a resource and returns an object containing two items: the response + * itself under `res` and and the response body parsed as JSON under `json`. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, 3) the response was received with a content-type header + * other than `application/json`, or 4) the response was received with a non-2xx + * status. + * + * Hence, when jsonFetch is called in this way, `json` will always be defined + * and `error` will always be undefined. + * + * @example + * ``` + * try { + * const url = 'https://some.resource.com/data.json'; + * const { json } = await jsonFetch(url, { + * rejectIfNotOk: true, + * rejectIfNonJsonContentType: true + * }); + * doSomethingWith(json); + * } catch(e) { + * if(e instanceof JsonFetchError) { + * // Special handling for non-2xx/non-json response bodies + * specialHandler(e.res.status, e.json); + * } else { + * throw e; + * } + * } + * ``` + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ErrorType extends JsonObject = JsonType +>( + url: string, + init: Omit< + JsonRequestInit, + 'swr' | 'rejectIfNotOk' | 'rejectIfNonJsonContentType' + > & { + swr?: false; + rejectIfNotOk: true; + rejectIfNonJsonContentType: true; + } +): Promise<{ + res: Response; + json: JsonType; + error: undefined; +}>; +/** + * Fetches a resource and returns the response body parsed as a JSON object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, 3) the response was received with a content-type header + * other than `application/json`, or 4) the response was received with a non-2xx + * status. + * + * The object SWR returns will contain the rejection reason under the `error` + * property. Usually, `error` is as an instance of JsonUnfetchError complete + * with `json` and `res` properties. If unfetch itself fails, the `error` + * object returned will not have these properties. + * + * @example + * ``` + * const { data: json, error } = useSwr('api/endpoint', swrFetch); + * // Or: ... = useSwr('api/endpoint', key => jsonFetch(key, { swr: true })); + * + * if(error)
Error: {error.message}
; + * return
Hello, your data is: {json.data}
; + * ``` + * + * @see https://swr.vercel.app + */ +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ErrorType extends JsonObject = JsonType +>(url: string, init: Omit & { swr: true }): Promise; +export async function jsonFetch< + JsonType extends JsonObject = JsonObject, + ErrorType extends JsonObject = JsonType +>(url: string, init?: JsonRequestInit): Promise { + const parsedOptions = { + ...globalJsonRequestOptions, + ...(init?.swr ? { method: 'GET' } : {}), + ...init + }; + + // ? A case-insensitive check since unfetch doesn't use a Headers instance + const hasJsonContentType = !!Object.entries(parsedOptions.headers || {}).some( + ([k, v]) => k.toLowerCase() === 'content-type' && v === JsonContentType + ); + + if (hasJsonContentType) { + try { + parsedOptions.body = JSON.stringify(parsedOptions.body); + } catch (error) { + throw new JsonUnfetchError( + undefined, + undefined, + `failed to stringify request body: ${ + isNativeError(error) ? error.message : error + }` + ); + } + } + + const res = await unfetch(url, parsedOptions as RequestInit); + const responseContentType = res.headers.get('content-type'); + + let parseError = ''; + let json: JsonType | undefined = undefined; + let error: Partial | undefined = undefined; + + try { + json = await res.json(); + } catch (error) { + parseError = `${isNativeError(error) ? error.message : error}`; + } + + if (!res.ok && (parsedOptions.rejectIfNotOk || parsedOptions.swr)) { + throw new JsonUnfetchError( + res, + json, + `response status code ${res.status} was not in the range 200-299` + ); + } + + if ( + responseContentType !== JsonContentType && + (parsedOptions.rejectIfNonJsonContentType || parsedOptions.swr) + ) { + throw new JsonUnfetchError( + res, + json, + `received response ${ + responseContentType + ? `with unexpected content-type "${responseContentType}"` + : 'without a content-type' + } (expected "application/json")` + ); + } + + if (parseError && responseContentType === JsonContentType) { + throw new JsonUnfetchError( + res, + json, + `failed to parse response body: ${parseError}` + ); + } + + if (responseContentType !== JsonContentType || parseError) { + json = undefined; + error = {}; + } else if (!res.ok) { + error = json as unknown as Partial; + json = undefined; + } + + return parsedOptions.swr ? json : { res, json, error }; +} + +/** + * Fetches a resource and returns the response body parsed as a JSON object. + * + * This function rejects if 1) the request body cannot be parsed as JSON but is + * being sent with an `application/json` content-type header, 2) the response + * body cannot be parsed as JSON but was received with an `application/json` + * content-type header, 3) the response was received with a content-type header + * other than `application/json`, or 4) the response was received with a non-2xx + * status. + * + * The object SWR returns will contain the rejection reason under the `error` + * property. Usually, `error` is as an instance of JsonUnfetchError complete + * with `json` and `res` properties. If unfetch itself fails, the `error` + * object returned will not have these properties. + * + * @example + * ``` + * const { data: json, error } = useSwr('api/endpoint', swrFetch); + * + * if(error)
Error: {error.message}
; + * return
Hello, your data is: {json.data}
; + * ``` + * + * @see https://swr.vercel.app + */ +export function swrFetch( + init?: JsonRequestInit +): (key: string) => Promise { + return (key) => { + return jsonFetch(key, { + ...init, + swr: true + }); + }; +} diff --git a/lib/json-unfetch/package.json b/lib/json-unfetch/package.json new file mode 100644 index 0000000..c47f576 --- /dev/null +++ b/lib/json-unfetch/package.json @@ -0,0 +1,3 @@ +{ + "name": "json-unfetch" +} diff --git a/lib/json-unfetch/test/integration.test.ts b/lib/json-unfetch/test/integration.test.ts new file mode 100644 index 0000000..d1bee23 --- /dev/null +++ b/lib/json-unfetch/test/integration.test.ts @@ -0,0 +1,188 @@ +/** + * @jest-environment jsdom + */ +import { createServer, type ServerResponse, type IncomingMessage } from 'node:http'; +import { createHttpTerminator } from 'http-terminator'; +import { globalJsonRequestOptions, jsonFetch } from 'multiverse/json-unfetch'; + +let cleanupFn = async () => undefined; + +afterAll(async () => cleanupFn()); + +describe('::jsonFetch', () => { + it('works', async () => { + expect.hasAssertions(); + + let status = 200; + let header: Record = { 'content-type': 'application/json' }; + let data: unknown = { hello: 'world!' }; + + const server = createServer((_req: IncomingMessage, res: ServerResponse) => { + res.statusCode = status; + + res.setHeader('access-control-allow-origin', '*'); + const headers = Object.entries(header); + headers.length && res.setHeader(...headers[0]); + + let dat; + try { + if (data !== '{"broken') { + dat = JSON.stringify(data); + } + } catch { + dat = data; + } + + res.end(dat); + }); + + // ? Unlike node-fetch, unfetch keeps connection handles open a while after + // ? they complete, perhaps for pooling/caching reasons... + const httpTerminator = createHttpTerminator({ server }); + cleanupFn = async () => void (await httpTerminator.terminate()); + + const port = await new Promise((resolve, reject) => { + server.listen(() => { + const addr = server.address(); + !addr || typeof addr === 'string' + ? reject(new Error('assertion failed unexpectedly')) + : resolve(addr.port); + }); + }); + + const localUrl = `http://localhost:${port}/`; + let { res, json, error } = await jsonFetch(localUrl); + + expect(res.url).toBe(localUrl); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + status = 555; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual(data); + + status = 200; + header = {}; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + await expect( + jsonFetch(localUrl, { rejectIfNonJsonContentType: true }) + ).rejects.toThrow('without a content-type'); + + header = { 'content-type': 'text/unknown' }; + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + await expect( + jsonFetch(localUrl, { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toThrow('"text/unknown"'); + + data = { hello: 'world!' }; + + await expect( + jsonFetch(localUrl, { rejectIfNonJsonContentType: true }).catch( + ({ json }) => json + ) + ).resolves.toStrictEqual(data); + + header = { 'content-type': 'application/json' }; + status = 666; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual(data); + + data = {}; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + globalJsonRequestOptions.rejectIfNotOk = true; + + await expect(jsonFetch(localUrl)).rejects.toThrow('666'); + + data = { hello: 'worlds!' }; + status = 201; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + delete globalJsonRequestOptions.rejectIfNotOk; + + header = { 'CONTENT-TYPE': 'application/json' }; + status = 200; + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toStrictEqual(data); + expect(error).toBeUndefined(); + + await expect( + jsonFetch(localUrl, { + headers: { 'CONTENT-TYPE': 'application/json' }, + method: 'POST', + body: { hi: 'world' } + }) + ).resolves.toBeDefined(); + + header = {}; + status = 500; + data = '{"broken'; + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true + }) + ).rejects.toThrow(/without a content-type/); + + ({ res, json, error } = await jsonFetch(localUrl)); + + expect(res.status).toBe(status); + expect(json).toBeUndefined(); + expect(error).toStrictEqual({}); + + header = { 'content-type': 'application/json' }; + + await expect(jsonFetch(localUrl)).rejects.toThrow( + /failed to parse response body:/ + ); + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true, + rejectIfNotOk: true + }) + ).rejects.toThrow(/500/); + + await expect( + jsonFetch(localUrl, { + rejectIfNonJsonContentType: true + }) + ).rejects.toThrow(/failed to parse response body:/); + + const badObj = { badObj: {} }; + badObj.badObj = badObj; + + await expect( + jsonFetch(localUrl, { + body: badObj + }) + ).rejects.toThrow(/failed to stringify request body:/); + }); +}); diff --git a/lib/json-unfetch/test/unit.test.ts b/lib/json-unfetch/test/unit.test.ts new file mode 100644 index 0000000..1cc441e --- /dev/null +++ b/lib/json-unfetch/test/unit.test.ts @@ -0,0 +1,383 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import unfetch from 'unfetch'; +import { toss } from 'toss-expression'; + +import { + globalJsonRequestOptions, + jsonFetch, + swrFetch +} from 'multiverse/json-unfetch'; + +import type { JsonObject } from 'type-fest'; +import type { Response } from 'multiverse/json-unfetch'; + +jest.mock('unfetch'); + +const mockFetch = asMockedFunction(unfetch); + +const mockedFetchResult = {} as unknown as Omit & { + headers: Omit & { + set: (k: string, v: string) => void; + }; +}; + +let mockedFetchResultJson = {} as JsonObject | Error | string; + +beforeEach(() => { + mockFetch.mockImplementation(async () => mockedFetchResult as Response); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = true; + mockedFetchResult.status = 200; + mockedFetchResult.headers = + new Map() as unknown as (typeof mockedFetchResult)['headers']; + + mockedFetchResult.json = jest.fn(async () => { + return typeof mockedFetchResultJson === 'string' || + mockedFetchResultJson instanceof Error + ? toss(mockedFetchResultJson) + : mockedFetchResultJson; + }); +}); + +describe('::jsonFetch', () => { + it('fetches a resource and returns the response itself and the body as json', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + }); + + it('returns empty error if the response has a non-json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + + mockedFetchResult.headers.set('content-type', 'something/else'); + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + }); + + it('rejects if the response has a non-json content-type and rejectIfNonJsonContentType is true', async () => { + expect.hasAssertions(); + + mockedFetchResultJson = { hello: 'world' }; + + await expect( + jsonFetch('some-url', { rejectIfNonJsonContentType: true }) + ).rejects.toThrow( + 'received response without a content-type (expected "application/json")' + ); + + mockedFetchResult.headers.set('content-type', 'something/else'); + + await expect( + jsonFetch('some-url', { rejectIfNonJsonContentType: true }) + ).rejects.toThrow( + 'received response with unexpected content-type "something/else" (expected "application/json")' + ); + }); + + it('rejects if the response has a json content-type but non-json body regardless of status code', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ? in JSON at position ??' + ); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ?? in JSON at position ?' + ); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 413; + + await expect(jsonFetch('some-url')).rejects.toThrow( + 'failed to parse response body: unexpected token ?? in JSON at position ?' + ); + }); + + it('returns error if the response has a non-2xx status code', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 567; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: mockedFetchResultJson + }); + }); + + it('returns empty error if the response has a non-2xx status code and non-json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/something-or-other'); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = false; + mockedFetchResult.status = 403; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: undefined, + error: {} + }); + }); + + it('rejects if the response has a non-2xx status code and rejectIfNotOk is true', async () => { + expect.hasAssertions(); + + globalJsonRequestOptions.rejectIfNotOk = true; + + try { + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + + mockedFetchResult.ok = false; + mockedFetchResult.status = 567; + globalJsonRequestOptions.rejectIfNotOk = false; + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + + // ? Should also reject with an HttpError even if JSON is not parsable + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect(jsonFetch('some-url', { rejectIfNotOk: true })).rejects.toThrow( + 'response status code 567 was not in the range 200-299' + ); + } finally { + delete globalJsonRequestOptions.rejectIfNotOk; + } + }); + + it('rejects if the response has a non-2xx status code, non-json content-type, or non-json body when both rejectIfNonJsonContentType and rejectIfNotOk are true', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + mockedFetchResult.ok = false; + mockedFetchResult.status = 404; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: mockedFetchResultJson }); + + mockedFetchResult.headers.set('content-type', 'application/something'); + mockedFetchResult.ok = true; + mockedFetchResult.status = 200; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: mockedFetchResultJson }); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = new SyntaxError( + 'unexpected token ? in JSON at position ??' + ); + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: undefined }); + + mockedFetchResultJson = 'unexpected token ?? in JSON at position ?'; + + await expect( + jsonFetch('some-url', { rejectIfNotOk: true, rejectIfNonJsonContentType: true }) + ).rejects.toMatchObject({ res: mockedFetchResult, json: undefined }); + }); + + it('rejects on failure to stringify request body with json content-type', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + + const badObj = { badObj: {} }; + badObj.badObj = badObj; + + await expect( + jsonFetch('some-url', { + headers: { 'content-type': 'application/json' }, + body: 'hello' + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: { hello: 'world' } + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + headers: { 'content-type': 'something/else' }, + body: badObj + }) + ).resolves.toBeDefined(); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: badObj + }) + ).rejects.toThrow('failed to stringify request body:'); + + const spy = jest + .spyOn(JSON, 'stringify') + .mockImplementation(() => + toss(new SyntaxError('unexpected token ? in JSON at position ??')) + ); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: 'whatever' + }) + ).rejects.toThrow( + 'failed to stringify request body: unexpected token ? in JSON at position ??' + ); + + spy.mockImplementation(() => toss('unexpected token ?? in JSON at position ?')); + + await expect( + jsonFetch('some-url', { + // headers: { 'content-type': 'application/json' }, // ? Default + body: 'whatever' + }) + ).rejects.toThrow( + 'failed to stringify request body: unexpected token ?? in JSON at position ?' + ); + }); + + it('handles empty global options', async () => { + expect.hasAssertions(); + + const oldValue = globalJsonRequestOptions.headers; + delete globalJsonRequestOptions.headers; + + try { + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world' }; + + await expect(jsonFetch('some-url')).resolves.toStrictEqual({ + res: mockedFetchResult, + json: mockedFetchResultJson, + error: undefined + }); + } finally { + globalJsonRequestOptions.headers = oldValue; + } + }); +}); + +describe('::swrFetch', () => { + it('returns the json response directly', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(swrFetch()('some-x-url')).resolves.toBe(mockedFetchResultJson); + expect(asMockedFunction(unfetch)).toBeCalledWith( + 'some-x-url', + expect.objectContaining({ swr: true }) + ); + }); + + it('sets the request method to GET even if it is set to POST globally (still locally overridable)', async () => { + expect.hasAssertions(); + + globalJsonRequestOptions.method = 'POST'; + + try { + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResultJson = { hello: 'world!' }; + + await expect(swrFetch()('some-x-url')).resolves.toBe(mockedFetchResultJson); + expect(asMockedFunction(unfetch)).toBeCalledWith( + 'some-x-url', + expect.objectContaining({ method: 'GET' }) + ); + + await expect(swrFetch({ method: 'PUT' })('some-x-url')).resolves.toBe( + mockedFetchResultJson + ); + expect(asMockedFunction(unfetch)).toBeCalledWith( + 'some-x-url', + expect.objectContaining({ method: 'PUT' }) + ); + } finally { + delete globalJsonRequestOptions.method; + } + }); + + it('rejects if the response has a non-2xx status code even if rejectIfNotOk is false', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/json'); + mockedFetchResult.ok = false; + mockedFetchResult.status = 789; + + await expect(swrFetch({ rejectIfNotOk: false })('some-x-url')).rejects.toThrow( + 'response status code 789 was not in the range 200-299' + ); + }); + + it('rejects if the response has a non-json content-type even if rejectIfNonJsonContentType is false', async () => { + expect.hasAssertions(); + + mockedFetchResult.headers.set('content-type', 'application/bad'); + + await expect( + swrFetch({ rejectIfNonJsonContentType: false })('some-x-url') + ).rejects.toThrow('application/bad'); + }); +}); diff --git a/lib/mongo-common/index.ts b/lib/mongo-common/index.ts new file mode 100644 index 0000000..67ce5db --- /dev/null +++ b/lib/mongo-common/index.ts @@ -0,0 +1,182 @@ +import cloneDeep from 'clone-deep'; +import { ObjectId } from 'mongodb'; +import { InvalidAppConfigurationError } from 'named-app-errors'; + +import { mockDateNowMs } from 'multiverse/jest-mock-date'; + +import { + BANNED_BEARER_TOKEN, + DEV_BEARER_TOKEN, + DUMMY_BEARER_TOKEN +} from 'multiverse/next-auth'; + +import type { DbSchema } from 'multiverse/mongo-schema'; +import type { DummyData } from 'multiverse/mongo-test'; +import type { InternalAuthEntry } from 'multiverse/next-auth'; +import type { InternalLimitedLogEntry } from 'multiverse/next-limit'; +import type { InternalRequestLogEntry } from 'multiverse/next-log'; + +export * from 'multiverse/jest-mock-date'; + +/** + * A JSON representation of the backend Mongo database structure. This is used + * for common consistent "well-known" db structure across projects. + * + * Well-known databases and their well-known collections currently include: + * - `root` (collections: `auth`, `request-log`, `limited-log`) + */ +export function getCommonSchemaConfig(additionalSchemaConfig?: DbSchema): DbSchema { + const schema: DbSchema = { + databases: { + root: { + collections: [ + { + name: 'auth', + indices: [ + { spec: 'attributes.owner' }, + { spec: 'deleted' }, + // ! When performing equality matches on embedded documents, field + // ! order matters and the embedded documents must match exactly. + // * https://xunn.at/mongo-docs-query-embedded-docs + // ! Additionally, field order determines internal sort order. + { spec: ['scheme', 'token'], options: { unique: true } } + ] + }, + { + name: 'request-log', + indices: [{ spec: 'header' }, { spec: 'ip' }, { spec: 'durationMs' }] + }, + { + name: 'limited-log', + indices: [{ spec: 'header' }, { spec: 'ip' }, { spec: { until: -1 } }] + } + ] + }, + ...additionalSchemaConfig?.databases + }, + aliases: { ...additionalSchemaConfig?.aliases } + }; + + const actualDatabaseNames = Object.keys(schema.databases); + + Object.entries(schema.aliases).every(([alias, actual]) => { + if (!actualDatabaseNames.includes(actual)) { + throw new InvalidAppConfigurationError( + `aliased database "${actual}" (referred to by alias "${alias}") does not exist in database schema or is not aliasable. Existing aliasable databases: ${actualDatabaseNames.join( + ', ' + )}` + ); + } + + if (actualDatabaseNames.includes(alias)) { + throw new InvalidAppConfigurationError( + `database alias "${alias}" (referring to actual database "${actual}") is invalid: an actual database with that name already exists in the database schema. You must choose a different alias` + ); + } + }); + + return schema; +} + +/** + * Returns data used to hydrate well-known databases and their well-known + * collections. + * + * Well-known databases and their well-known collections currently include: + * - `root` (collections: `auth`, `request-log`, `limited-log`) + */ +export function getCommonDummyData(additionalDummyData?: DummyData): DummyData { + return cloneDeep({ root: dummyRootData, ...additionalDummyData }); +} + +/** + * Calls `new ObjectId(...)` explicitly passing {@link mockDateNowMs} as the + * inception time, which is the same thing that {@link ObjectId} does internally + * with the real `Date.now`. + * + * **This should only be used in modules with import side-effects that execute + * before `useMockDateNow` is called** later in downstream code. If you are + * unsure, you probably don't need to use this function and should just call + * `new ObjectId()` instead. + * + * The point of this function is to avoid race conditions when mocking parts of + * the {@link Date} object that _sometimes_ resulted in _later_ calls to + * {@link ObjectId} generating IDs that were _less_ than the IDs generated + * _before_ it. + */ +export function generateMockSensitiveObjectId() { + // * Adopted from ObjectId::generate function. Turns out this is the cause of + // * some flakiness with tests where order is determined by ObjectId. + return new ObjectId(Math.floor(mockDateNowMs / 1000)); +} + +/** + * The shape of the well-known `root` database's collections and their test + * data. + */ +export type DummyRootData = { + _generatedAt: number; + auth: InternalAuthEntry[]; + 'request-log': InternalRequestLogEntry[]; + 'limited-log': InternalLimitedLogEntry[]; +}; + +/** + * Test data for the well-known `root` database. + */ +export const dummyRootData: DummyRootData = { + _generatedAt: mockDateNowMs, + auth: [ + // ! Must maintain order or various unit tests across projects will fail ! + { + _id: generateMockSensitiveObjectId(), + deleted: false, + attributes: { owner: 'local developer', isGlobalAdmin: true }, + scheme: 'bearer', + token: { bearer: DEV_BEARER_TOKEN } + }, + { + _id: generateMockSensitiveObjectId(), + deleted: false, + attributes: { owner: 'dummy owner' }, + scheme: 'bearer', + token: { bearer: DUMMY_BEARER_TOKEN } + }, + { + _id: generateMockSensitiveObjectId(), + deleted: false, + attributes: { owner: 'banned dummy owner' }, + scheme: 'bearer', + token: { bearer: BANNED_BEARER_TOKEN } + } + ], + 'request-log': Array.from({ length: 22 }).map((_, ndx) => ({ + _id: generateMockSensitiveObjectId(), + ip: '1.2.3.4', + header: ndx % 2 ? null : `bearer ${BANNED_BEARER_TOKEN}`, + method: ndx % 3 ? 'GET' : 'POST', + route: 'fake/route', + endpoint: '/fake/:route', + createdAt: mockDateNowMs + 10 ** 6, + resStatusCode: 200, + durationMs: 1234 + })), + 'limited-log': [ + // ! Must maintain order or various unit tests will fail + { + _id: generateMockSensitiveObjectId(), + ip: '1.2.3.4', + until: mockDateNowMs + 1000 * 60 * 15 + }, + { + _id: generateMockSensitiveObjectId(), + ip: '5.6.7.8', + until: mockDateNowMs + 1000 * 60 * 15 + }, + { + _id: generateMockSensitiveObjectId(), + header: `bearer ${BANNED_BEARER_TOKEN}`, + until: mockDateNowMs + 1000 * 60 * 60 + } + ] +}; diff --git a/lib/mongo-common/package.json b/lib/mongo-common/package.json new file mode 100644 index 0000000..b1a8a42 --- /dev/null +++ b/lib/mongo-common/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/mongo-common" +} diff --git a/lib/mongo-common/unit.test.ts b/lib/mongo-common/unit.test.ts new file mode 100644 index 0000000..5abbc9e --- /dev/null +++ b/lib/mongo-common/unit.test.ts @@ -0,0 +1,59 @@ +import { getCommonSchemaConfig, getCommonDummyData } from 'multiverse/mongo-common'; + +describe('::getCommonSchemaConfig', () => { + it('returns an object with dummy root schema and additional dummy schema', async () => { + expect.hasAssertions(); + + expect( + getCommonSchemaConfig({ + databases: { someDb: { collections: [] } }, + aliases: {} + }) + ).toStrictEqual({ + databases: { + root: expect.toBeObject(), + someDb: expect.toBeObject() + }, + aliases: {} + }); + }); + + it('throws if an alias references a non-existent database name', async () => { + expect.hasAssertions(); + + expect(() => + getCommonSchemaConfig({ + databases: { someDb: { collections: [] } }, + aliases: { app: 'badDb' } + }) + ).toThrow( + /aliased database "badDb" \(referred to by alias "app"\) does not exist/ + ); + }); + + it('throws if an alias itself conflicts with a database name', async () => { + expect.hasAssertions(); + + expect(() => + getCommonSchemaConfig({ + databases: { someDb: { collections: [] } }, + aliases: { someDb: 'root' } + }) + ).toThrow( + /database alias "someDb" \(referring to actual database "root"\) is invalid/ + ); + }); +}); + +describe('::getCommonDummyData', () => { + it('returns an object with dummy root data and additional dummy data', async () => { + expect.hasAssertions(); + + const customDummyData = { someDb: { _generatedAt: 123 } }; + + expect(getCommonDummyData(customDummyData)).toStrictEqual({ + root: expect.toBeObject(), + someDb: expect.toBeObject() + }); + }); +}); diff --git a/lib/mongo-item/index.ts b/lib/mongo-item/index.ts new file mode 100644 index 0000000..7a13440 --- /dev/null +++ b/lib/mongo-item/index.ts @@ -0,0 +1,248 @@ +import { ObjectId, type Collection, type Document, type WithId } from 'mongodb'; +import { AppValidationError, ErrorMessage, ValidationError } from 'named-app-errors'; + +/** + * Represents the value of the `_id` property of a MongoDB collection entry. + * Optionally, a key other than `_id` can be specified using the `{ key: ..., + * id: ... }` syntax. + */ +export type ItemExistsIdParam = + | string + | ObjectId + | { key: string; id: string | ObjectId }; + +/** + * Available options for the `itemExists` function. + */ +export type ItemExistsOptions = { + /** + * Items matching excludeId will be completely ignored by this function. + * + * @default undefined + */ + excludeId?: ItemExistsIdParam; + /** + * If `true`, ids will be matched in a case-insensitive manner (via locale). + * + * @default false + */ + caseInsensitive?: boolean; + /** + * When looking for an item matching `{ _id: id }`, where the descriptor key + * is the string `"_id"`, `id` will be optimistically wrapped in a `new + * ObjectId(id)` call. Set this to `false` to prevent this. + * + * @default true + */ + optimisticCoercion?: boolean; +}; + +// TODO: the following needs to work with composite keys +/** + * Checks if an item matching `{ _id: id }` exists within `collection`. + */ +export async function itemExists( + collection: Collection, + id: string | ObjectId, + options?: ItemExistsOptions +): Promise; +/** + * Checks if an item matching `{ [descriptor.key]: descriptor.id }` exists + * within `collection`. + */ +export async function itemExists( + collection: Collection, + descriptor: { key: string; id: string | ObjectId }, + options?: ItemExistsOptions +): Promise; +export async function itemExists( + collection: Collection, + id: ItemExistsIdParam, + options?: ItemExistsOptions +): Promise { + let excludeIdProperty: string | null = null; + let excludeId: string | ObjectId | null = null; + const idProperty = + typeof id === 'string' || id instanceof ObjectId ? '_id' : id.key; + id = typeof id === 'string' || id instanceof ObjectId ? id : id.id; + + if (options?.excludeId) { + excludeIdProperty = + typeof options.excludeId === 'string' || options.excludeId instanceof ObjectId + ? '_id' + : options.excludeId.key; + + excludeId = + typeof options.excludeId === 'string' || options.excludeId instanceof ObjectId + ? options.excludeId + : options.excludeId.id; + } + + if (idProperty === excludeIdProperty) { + throw new AppValidationError( + `cannot lookup an item by property "${idProperty}" while also filtering results by that same property` + ); + } + + if ( + options?.optimisticCoercion !== false && + typeof id === 'string' && + idProperty === '_id' + ) { + id = itemToObjectId(id); + } + + return ( + (await collection.countDocuments( + { + [idProperty]: id, + ...(excludeIdProperty ? { [excludeIdProperty]: { $ne: excludeId } } : {}) + } as unknown as Parameters[0], + { + ...(options?.caseInsensitive + ? { collation: { locale: 'en', strength: 2 } } + : {}) + } + )) !== 0 + ); +} + +/** + * The shape of an object that can be translated into an `ObjectId` (or `T`) + * instance or is `null`/`undefined`. + */ +export type IdItem = + | WithId + | string + | T + | null + | undefined; + +/** + * The shape of an array of objects that can be translated into an array of + * `ObjectId` (or `T`) instances or are `null`/`undefined`. + */ +export type IdItemArray = IdItem[]; + +export type ItemToObjectIdOptions = { + /** + * If `true`, inputs that cannot be coerced into an {@link ObjectId} will be + * replaced with `null` instead of throwing a {@link ValidationError}. + * + * @default false + */ + ignoreInvalidId?: boolean; +}; + +/** + * Reduces an `item` down to its `ObjectId` instance. + * + * When `options.ignoreInvalidId` is `true`, result may be `null`. + */ +export function itemToObjectId( + item: IdItem, + options: Exclude & { + ignoreInvalidId: true; + } +): T | null; +/** + * Reduces an array of `items` down to their respective `ObjectId` instances. + * + * An attempt is made to eliminate duplicates via `new Set(...)`, but the + * absence of duplicates is not guaranteed when `items` contains `WithId<...>` + * objects. + * + * When `options.ignoreInvalidId` is `true`, result may contain `null`s. + */ +export function itemToObjectId( + items: IdItemArray, + options: Exclude & { + ignoreInvalidId: true; + } +): (T | null)[]; +/** + * Reduces an `item` down to its `ObjectId` instance. + */ +export function itemToObjectId( + item: IdItem, + options?: ItemToObjectIdOptions +): T; +/** + * Reduces an array of `items` down to their respective `ObjectId` instances. + * + * An attempt is made to eliminate duplicates via `new Set(...)`, but the + * absence of duplicates is not guaranteed when `items` contains `WithId<...>` + * objects. + */ +export function itemToObjectId( + items: IdItemArray, + options?: ItemToObjectIdOptions +): T[]; +export function itemToObjectId( + item: IdItem | IdItemArray, + options?: ItemToObjectIdOptions +): (T | null) | (T | null)[] { + let _id: unknown = item; + try { + if (item instanceof ObjectId) { + return item; + } else if (Array.isArray(item)) { + const objectIdArray = Array.from(new Set<(typeof item)[0]>(item)); + return objectIdArray.map((id) => { + _id = id; + + if (id instanceof ObjectId) { + return id; + } else if (typeof id !== 'string' && id?._id instanceof ObjectId) { + return id._id as T; + } else { + try { + return new ObjectId(String(id)) as T; + } catch (error) { + if (options?.ignoreInvalidId) { + return null; + } else { + throw error; + } + } + } + }); + } else if (typeof item !== 'string' && item?._id instanceof ObjectId) { + return item._id as T; + } else { + try { + return new ObjectId(String(item)) as T; + } catch (error) { + if (options?.ignoreInvalidId) { + return null; + } else { + throw error; + } + } + } + } catch (error) { + const throwable = new ValidationError( + ErrorMessage.InvalidItem(String(_id), 'ObjectId') + ); + throwable.cause = error; + throw throwable; + } +} + +/** + * Reduces an `item` down to the string representation of its `ObjectId` + * instance. + */ +export function itemToStringId(item: IdItem): string; +/** + * Reduces an array of `items` down to the string representations of their + * respective `ObjectId` instances. + */ +export function itemToStringId(items: IdItemArray): string[]; +export function itemToStringId( + item: IdItem | IdItemArray +): string | string[] { + return Array.isArray(item) + ? itemToObjectId(item).map(String) + : itemToObjectId(item).toString(); +} diff --git a/lib/mongo-item/package.json b/lib/mongo-item/package.json new file mode 100644 index 0000000..c8883c3 --- /dev/null +++ b/lib/mongo-item/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/mongo-item" +} diff --git a/lib/mongo-item/unit.test.ts b/lib/mongo-item/unit.test.ts new file mode 100644 index 0000000..eb6411a --- /dev/null +++ b/lib/mongo-item/unit.test.ts @@ -0,0 +1,249 @@ +import { ObjectId } from 'mongodb'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { itemExists, itemToObjectId, itemToStringId } from 'multiverse/mongo-item'; +import { getDb } from 'multiverse/mongo-schema'; +import { toss } from 'toss-expression'; +import { TrialError } from 'named-app-errors'; +import { DUMMY_BEARER_TOKEN, NULL_BEARER_TOKEN } from 'multiverse/next-auth'; + +import type { InternalAuthBearerEntry } from 'multiverse/next-auth'; +import { expectExceptionsWithMatchingErrors } from 'multiverse/jest-expect-matching-errors'; + +setupMemoryServerOverride(); + +describe('::itemExists', () => { + it('returns true if an item exists in a collection where [key] === id', async () => { + expect.hasAssertions(); + + const col = (await getDb({ name: 'root' })).collection('auth'); + const item = + (await col.findOne()) || + toss(new TrialError('assert failed')); + + await expect(itemExists(col, item._id)).resolves.toBeTrue(); + await expect(itemExists(col, new ObjectId())).resolves.toBeFalse(); + + await expect( + itemExists(col, { key: 'token.bearer', id: DUMMY_BEARER_TOKEN }) + ).resolves.toBeTrue(); + + await expect( + itemExists(col, { key: 'token.bearer', id: NULL_BEARER_TOKEN }) + ).resolves.toBeFalse(); + }); + + it('optimistically coerces strings to ObjectIds unless optimisticCoercion is false', async () => { + expect.hasAssertions(); + + const col = (await getDb({ name: 'root' })).collection('auth'); + const item = + (await col.findOne()) || + toss(new TrialError('assert failed')); + + await expect(itemExists(col, item._id.toString())).resolves.toBeTrue(); + await expect( + itemExists(col, item._id.toString(), { optimisticCoercion: false }) + ).resolves.toBeFalse(); + }); + + it('respects excludeId option', async () => { + expect.hasAssertions(); + + const col = (await getDb({ name: 'root' })).collection('auth'); + const item = + (await col.findOne()) || + toss(new TrialError('assert failed')); + + await expect(itemExists(col, item._id)).resolves.toBeTrue(); + await expect( + itemExists(col, item._id, { + excludeId: { key: 'token.bearer', id: item.token.bearer } + }) + ).resolves.toBeFalse(); + + await expect( + itemExists( + col, + { key: 'token.bearer', id: item.token.bearer }, + { excludeId: item._id } + ) + ).resolves.toBeFalse(); + }); + + it('rejects if attempting to exclude using same property as id', async () => { + expect.hasAssertions(); + + const col = (await getDb({ name: 'root' })).collection('auth'); + const item = + (await col.findOne()) || + toss(new TrialError('assert failed')); + + await expect(itemExists(col, item._id, { excludeId: item._id })).rejects.toThrow( + 'cannot lookup an item by property "_id"' + ); + + await expect( + itemExists( + col, + { key: 'token.bearer', id: item.token.bearer }, + { excludeId: { key: 'token.bearer', id: item.token.bearer } } + ) + ).rejects.toThrow('cannot lookup an item by property "token.bearer"'); + }); + + it('respects caseInsensitive option', async () => { + expect.hasAssertions(); + + const col = (await getDb({ name: 'root' })).collection('auth'); + + await expect( + itemExists( + col, + { key: 'token.bearer', id: DUMMY_BEARER_TOKEN.toUpperCase() }, + { caseInsensitive: true } + ) + ).resolves.toBeTrue(); + + await expect( + itemExists(col, { key: 'token.bearer', id: DUMMY_BEARER_TOKEN.toUpperCase() }) + ).resolves.toBeFalse(); + + const item = + (await col.findOne()) || + toss(new TrialError('assert failed')); + + await expect(itemExists(col, item._id)).resolves.toBeTrue(); + + await expect( + itemExists(col, item._id, { + excludeId: { key: 'token.bearer', id: item.token.bearer.toUpperCase() } + }) + ).resolves.toBeTrue(); + + await expect( + itemExists(col, item._id, { + excludeId: { key: 'token.bearer', id: item.token.bearer.toUpperCase() }, + caseInsensitive: true + }) + ).resolves.toBeFalse(); + }); +}); + +describe('::itemToObjectId', () => { + it('reduces an item down to its ObjectId instance', async () => { + expect.hasAssertions(); + + const id = new ObjectId(); + + expect(itemToObjectId({ _id: id })).toBe(id); + expect(itemToObjectId(id.toString())).toStrictEqual(id); + expect(itemToObjectId(id)).toBe(id); + }); + + it('reduces an array of items down to their respective ObjectId instances', async () => { + expect.hasAssertions(); + + const ids = [new ObjectId(), new ObjectId(), new ObjectId()]; + + expect(itemToObjectId(ids)).toStrictEqual(ids); + + expect( + itemToObjectId([{ _id: ids[0] }, { _id: ids[1] }, { _id: ids[2] }]) + ).toStrictEqual(ids); + + expect( + itemToObjectId([ids[0].toString(), ids[1].toString(), ids[2].toString()]) + ).toStrictEqual(ids); + }); + + it('duplicate ObjectIds are eliminated during simple array reduction', async () => { + expect.hasAssertions(); + + const id = new ObjectId(); + const ids = [id]; + + expect(itemToObjectId(ids)).toStrictEqual(ids); + + expect( + itemToObjectId([id.toString(), id.toString(), id.toString()]) + ).toStrictEqual(ids); + }); + + it('throws if item is irreducible or invalid', async () => { + expect.hasAssertions(); + + type ParamsType = Parameters[0]; + const errors: [params: ParamsType, error: string][] = [ + [null as unknown as ParamsType, 'invalid ObjectId "null"'], + [undefined as unknown as ParamsType, 'invalid ObjectId "undefined"'], + [[null], 'invalid ObjectId "null"'], + [[undefined], 'invalid ObjectId "undefined"'], + [{} as unknown as ParamsType, 'invalid ObjectId "[object Object]"'], + [[{}] as unknown as ParamsType, 'invalid ObjectId "[object Object]"'], + ['bad' as unknown as ParamsType, 'invalid ObjectId "bad"'], + [['bad'], 'invalid ObjectId "bad"'], + [[new ObjectId(), 'bad'], 'invalid ObjectId "bad"'] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + itemToObjectId(params) + ); + }); + + it.todo('does not throw if item is irreducible/invalid if ignoreInvalidId is true'); +}); + +describe('::itemToStringId', () => { + it('reduces an item down to its string representation', async () => { + expect.hasAssertions(); + + const id = new ObjectId(); + const idString = id.toString(); + + expect(itemToStringId({ _id: id })).toBe(idString); + expect(itemToStringId(idString)).toBe(idString); + expect(itemToStringId(id)).toBe(idString); + }); + + it('reduces an array of items down to string representations', async () => { + expect.hasAssertions(); + + const ids = [new ObjectId(), new ObjectId(), new ObjectId()]; + const idStrings = ids.map(String); + + expect(itemToStringId(ids)).toStrictEqual(idStrings); + + expect( + itemToStringId([{ _id: ids[0] }, { _id: ids[1] }, { _id: ids[2] }]) + ).toStrictEqual(idStrings); + + expect( + itemToStringId([ids[0].toString(), ids[1].toString(), ids[2].toString()]) + ).toStrictEqual(idStrings); + }); + + it('throws if item is irreducible', async () => { + expect.hasAssertions(); + + // TODO: replace these error message strings here and elsewhere with + // TODO: ErrorMessage package + type ParamsType = Parameters[0]; + const errors: [params: ParamsType, error: string][] = [ + [null as unknown as ParamsType, 'invalid ObjectId "null"'], + [undefined as unknown as ParamsType, 'invalid ObjectId "undefined"'], + [[null], 'invalid ObjectId "null"'], + [[undefined], 'invalid ObjectId "undefined"'], + [{} as unknown as ParamsType, 'invalid ObjectId "[object Object]"'], + [[{}] as unknown as ParamsType, 'invalid ObjectId "[object Object]"'], + ['bad' as unknown as ParamsType, 'invalid ObjectId "bad"'], + [['bad'], 'invalid ObjectId "bad"'], + [[new ObjectId(), 'bad'], 'invalid ObjectId "bad"'] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + itemToStringId(params) + ); + }); + + it.todo('does not throw if item is irreducible/invalid if ignoreInvalidId is true'); +}); diff --git a/lib/mongo-schema/index.ts b/lib/mongo-schema/index.ts new file mode 100644 index 0000000..68fbb09 --- /dev/null +++ b/lib/mongo-schema/index.ts @@ -0,0 +1,319 @@ +import { MongoClient } from 'mongodb'; +import { InvalidAppConfigurationError } from 'named-app-errors'; +import { getEnv } from 'multiverse/next-env'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { Db } from 'mongodb'; + +// TODO: this package must be published transpiled to cjs by babel but NOT +// TODO: webpacked! + +const debug = debugFactory('mongo-schema:db'); +let memory: InternalMemory = getInitialInternalMemoryState(); + +type createIndexParams = Parameters; + +/** + * An internal cache of connection, server schema, and database state. + */ +export type InternalMemory = { + /** + * Memoized resolved database schemas and aliases. + */ + schema: DbSchema | null; + /** + * Memoized MongoDB driver client connection. + */ + client: MongoClient | null; + /** + * Memoized MongoDB driver Database instances. + */ + databases: Record; +}; + +/** + * A configuration object representing a MongoDB collection. + */ +export type CollectionSchema = { + /** + * The valid MongoDB name of the collection. + */ + name: string; + /** + * An object passed directly to the MongoDB `createCollection` function via + * the `createOptions` parameter. + */ + createOptions?: Parameters[1]; + /** + * An object representing indices to be created on the MongoDB collection via + * `createIndex`. + */ + indices?: { + spec: createIndexParams[1]; + options?: createIndexParams[2]; + }[]; +}; + +/** + * A configuration object representing one or more MongoDB databases and their + * aliases. + */ +export type DbSchema = { + /** + * All databases known to this system. These can be accessed via `getDb`. + */ + databases: Record< + string, + { + /** + * An array of MongoDB collections. + */ + collections: (string | CollectionSchema)[]; + } + >; + + /** + * These are alternative names to use with `getDb` that map to the names of + * databases known to this system. Aliases are specified as `alias: + * real-name`. + */ + aliases: Record; +}; + +/** + * Returns a copy of the initial state of internal memory. Useful when + * overwriting internal memory. + */ +export function getInitialInternalMemoryState(): InternalMemory { + return { + schema: null, + client: null, + databases: {} + }; +} + +/** + * Imports `getSchemaConfig` from "configverse/get-schema-config", calls it, and + * memoizes the result. + */ +export async function getSchemaConfig(): Promise { + if (memory.schema) { + debug('returning schema configuration from memory'); + return memory.schema; + } else { + try { + debug('importing `getSchemaConfig` from "configverse/get-schema-config"'); + return (memory.schema = await ( + await import('configverse/get-schema-config') + ).getSchemaConfig()); + } catch (error) { + debug.warn( + `failed to import getSchemaConfig from "configverse/get-schema-config": ${error}` + ); + + throw new InvalidAppConfigurationError( + 'could not resolve mongodb schema configuration: failed to import getSchemaConfig from "configverse/get-schema-config". Did you forget to register "configverse/get-schema-config" as an import alias/path?' + ); + } + } +} + +/** + * Mutates internal memory. Used for testing purposes. + */ +export function overwriteMemory(newMemory: InternalMemory) { + memory = newMemory; + debug('internal memory overwritten'); +} + +/** + * Lazily connects to the server on-demand, memoizing the result. + */ +export async function getClient() { + if (!memory.client) { + const uri = getEnv().MONGODB_URI; + debug(`connecting to mongo server at ${uri}`); + memory.client = await MongoClient.connect(uri); + } else { + debug('connected (from memory) to mongo server'); + } + + return memory.client; +} + +/** + * Kills the MongoClient instance and any related database connections and + * clears internal memory. + */ +export async function closeClient() { + /* istanbul ignore else */ + if (memory?.client) { + debug('closing server connection'); + await memory.client.close(true); + } + + memory = getInitialInternalMemoryState(); +} + +/** + * Accepts a database alias (or real name) and returns its real name. If the + * actual database is not listed in the schema, an error is thrown. + */ +export async function getNameFromAlias(alias: string) { + const schema = await getSchemaConfig(); + const nameActual = schema.aliases[alias] || alias; + + if (alias !== nameActual) { + debug(`mapped alias "${alias}" to database name "${nameActual}"`); + } + + if (!schema.databases[nameActual]?.collections) { + throw new InvalidAppConfigurationError( + `database "${nameActual}" is not defined in schema` + ); + } + + return nameActual; +} + +/** + * Accepts a database name (or an alias) and returns one or more aliases. If the + * named database has no aliases listed in the schema, said database name is + * returned as a single-element array. If said database name is not listed in + * the schema, an error is thrown. + */ +export async function getAliasFromName(nameActual: string) { + const schema = await getSchemaConfig(); + + if (!schema.databases[nameActual]?.collections) { + throw new InvalidAppConfigurationError( + `database "${nameActual}" is not defined in schema` + ); + } + + const aliases = Object.entries(schema.aliases) + .filter(([, name]) => name === nameActual) + .map(([alias]) => alias); + + if (aliases.length) { + debug( + `reverse-mapped database name "${nameActual}" to alias${ + aliases.length === 1 + ? ` "${aliases.toString()}"` + : `es: ${aliases.join(', ')}` + }` + ); + + return aliases; + } else { + return [nameActual]; + } +} + +/** + * Lazily connects to a database on-demand, memoizing the result. If the + * database does not yet exist, it is both created and initialized by this + * function. The latter can be prevented by setting `initialize` to `false`. + */ +export async function getDb({ + name, + initialize +}: { + /** + * The name or alias of the database to retrieve. + */ + name: string; + /** + * Set to `false` to prevent `getDb` from calling `initializeDb` if the + * database does not exist prior to acquiring it. + * + * @default true + */ + initialize?: boolean; +}) { + const nameActual = await getNameFromAlias(name); + + if (!memory.databases[nameActual]) { + debug(`acquiring mongo database "${nameActual}"`); + + const client = await getClient(); + const existingDatabases = ( + await client.db('admin').admin().listDatabases() + ).databases.map(({ name }) => name); + + memory.databases[nameActual] = client.db(nameActual); + + if (initialize !== false && !existingDatabases.includes(nameActual)) { + debug(`calling initializeDb since "${nameActual}" was just created`); + await initializeDb({ name: nameActual }); + } + } else { + debug(`acquired (from memory) mongo database "${nameActual}"`); + } + + return memory.databases[nameActual]; +} + +/** + * Drops a database, destroying its collections. If the database does not exist + * before calling this function, it will be created first then dropped. + * + * Note that this function does not clear the destroyed database's Db instance + * from internal memory for performance reasons. + */ +export async function destroyDb({ + name +}: { + /** + * The name or alias of the database to destroy. + */ + name: string; +}) { + const nameActual = await getNameFromAlias(name); + debug(`destroying database "${nameActual}" and its collections`); + return !memory.databases[nameActual] || (await getDb({ name })).dropDatabase(); +} + +/** + * Creates a database and initializes its collections. If the database does not + * exist before calling this function, it will be created first. This function + * should only be called on empty or brand new databases **and not on databases + * with pre-existing collections.** + */ +export async function initializeDb({ + name +}: { + /** + * The name or alias of the database to initialize. + */ + name: string; +}) { + const db = await getDb({ name, initialize: false }); + const nameActual = await getNameFromAlias(name); + + debug(`initializing database "${nameActual}"`); + + await Promise.all( + (await getSchemaConfig()).databases[nameActual].collections.map( + (colNameOrSchema) => { + const colSchema: CollectionSchema = + typeof colNameOrSchema === 'string' + ? { + name: colNameOrSchema + } + : colNameOrSchema; + + debug(`initializing collection "${nameActual}.${colSchema.name}"`); + return db + .createCollection(colSchema.name, colSchema.createOptions) + .then((col) => { + return Promise.all( + colSchema.indices?.map((indexSchema) => + col.createIndex(indexSchema.spec, indexSchema.options || {}) + ) || [] + ); + }); + } + ) + ); +} diff --git a/lib/mongo-schema/package.json b/lib/mongo-schema/package.json new file mode 100644 index 0000000..a62764a --- /dev/null +++ b/lib/mongo-schema/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/mongo-schema" +} diff --git a/lib/mongo-schema/unit.test.ts b/lib/mongo-schema/unit.test.ts new file mode 100644 index 0000000..be3bc81 --- /dev/null +++ b/lib/mongo-schema/unit.test.ts @@ -0,0 +1,347 @@ +import { asMockedClass } from '@xunnamius/jest-types'; +import { MongoClient, type Db } from 'mongodb'; +import { isolatedImportFactory, mockEnvFactory } from 'testverse/setup'; + +import { getInitialInternalMemoryState } from 'multiverse/mongo-schema'; +import type { TestCustomizations } from 'multiverse/mongo-test'; + +jest.mock('mongodb'); +jest.mock( + 'configverse/get-schema-config', + (): typeof import('configverse/get-schema-config') => + mockedMongoCustomizations as unknown as typeof import('configverse/get-schema-config') +); + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); + +const mockMongoClient = asMockedClass(MongoClient); +let mockedMongoCustomizations: TestCustomizations; + +const importDbLib = isolatedImportFactory({ + path: 'multiverse/mongo-schema' +}); + +beforeEach(() => { + mockedMongoCustomizations = mockedMongoCustomizations || {}; + + mockedMongoCustomizations.getSchemaConfig = async () => { + return { + databases: { + 'fake-db-1': { + collections: ['col'] + }, + + 'fake-db-2': { + collections: [ + 'col-1', + { name: 'col-2', createOptions: { capped: true } }, + { name: 'col-3', indices: [{ spec: 'some-key' }] }, + { + name: 'col-4', + indices: [{ spec: ['some-key', -1], options: { comment: '' } }] + } + ] + }, + + 'fake-db-3': { + collections: ['col'] + } + }, + aliases: { + 'fake-alias-1': 'fake-db-1', + 'fake-alias-2': 'fake-db-2', + 'fake-alias-3': 'fake-db-2' + } + }; + }; + + mockMongoClient.connect = jest.fn((url: string) => + Promise.resolve( + new (class { + url = url; + + db(name: string) { + return new (class { + parentUrl = url; + databaseName = name; + dropDatabase; + createCollection; + createIndex; + collection; + admin; + + constructor() { + this.dropDatabase = jest.fn(); + this.createIndex = jest.fn(); + // ? Reuse this.createIndex method for easy access to mock + this.collection = jest.fn(() => ({ insertMany: this.createIndex })); + this.createCollection = jest.fn(() => + Promise.resolve({ createIndex: this.createIndex }) + ); + this.admin = jest.fn(() => ({ + listDatabases: jest.fn(() => ({ + databases: [ + { name: 'auth' }, + { name: 'request-log' }, + { name: 'limited-log' } + ] + })) + })); + } + })(); + } + + close() { + return url; + } + })() as unknown as MongoClient + ) + ); +}); + +describe('::getSchemaConfig', () => { + it('dynamically imports customizations', async () => { + expect.hasAssertions(); + + await expect(importDbLib().getSchemaConfig()).resolves.toStrictEqual( + await mockedMongoCustomizations.getSchemaConfig() + ); + }); + + it('rejects if customizations are unavailable', async () => { + expect.hasAssertions(); + + // @ts-expect-error: don't care that we're deleting a non-optional prop + delete mockedMongoCustomizations.getSchemaConfig; + await expect(importDbLib().getSchemaConfig()).rejects.toThrow( + 'configverse/get-schema-config' + ); + }); +}); + +describe('::getClient', () => { + it("creates client if it doesn't already exist", async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const client = await lib.getClient(); + await expect(lib.getClient()).resolves.toBe(client); + expect(mockMongoClient.connect).toBeCalledTimes(1); + expect(client.close()).toBe('abc'); + }, + { MONGODB_URI: 'abc' } + ); + }); +}); + +describe('::getDb', () => { + it("creates db and connection if it doesn't already exist", async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + expect(mockMongoClient.connect).toBeCalledTimes(0); + const db = await lib.getDb({ name: 'fake-db-1' }); + await expect(lib.getDb({ name: 'fake-db-1' })).resolves.toBe(db); + expect(mockMongoClient.connect).toBeCalledTimes(1); + await expect(lib.getDb({ name: 'fake-db-2' })).resolves.not.toBe(db); + expect(mockMongoClient.connect).toBeCalledTimes(1); + expect(db.databaseName).toBe('fake-db-1'); + }, + { MONGODB_URI: 'abc' } + ); + }); + + it('automatically initializes newly created databases unless initialize is false', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const db = await lib.getDb({ name: 'fake-db-1', initialize: false }); + expect(db.createCollection).not.toHaveBeenCalled(); + await lib.getDb({ name: 'fake-db-1' }); + expect(db.createCollection).not.toHaveBeenCalled(); + const db2 = await lib.getDb({ name: 'fake-db-2' }); + expect(db2.createCollection).toHaveBeenCalled(); + await lib.getDb({ name: 'fake-db-2' }); + expect(db2.createCollection).toHaveBeenCalled(); + }, + { MONGODB_URI: 'abc' } + ); + }); + + it('returns db using alias', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const db1 = await lib.getDb({ name: 'fake-db-1' }); + await expect(lib.getDb({ name: 'fake-alias-1' })).resolves.toBe(db1); + + const db2 = await lib.getDb({ name: 'fake-alias-2' }); + await expect(lib.getDb({ name: 'fake-db-2' })).resolves.toBe(db2); + }, + { MONGODB_URI: 'abc' } + ); + }); +}); + +describe('::overwriteMemory', () => { + it('replaces memory when called', async () => { + expect.hasAssertions(); + + const client = new (class {})() as MongoClient; + const databases = { 'fake-db-1': new (class {})() as Db }; + const lib = importDbLib(); + + lib.overwriteMemory({ ...getInitialInternalMemoryState(), client, databases }); + + await expect(lib.getClient()).resolves.toBe(client); + await expect(lib.getDb({ name: 'fake-db-1' })).resolves.toBe( + databases['fake-db-1'] + ); + }); +}); + +describe('::closeClient', () => { + it('closes client and deletes memory', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const client = await lib.getClient(); + await expect(lib.getClient()).resolves.toBe(client); + await lib.closeClient(); + await expect(lib.getClient()).resolves.not.toBe(client); + }, + { MONGODB_URI: 'abc' } + ); + }); +}); + +describe('::destroyDb', () => { + it('drops database', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const db = await lib.getDb({ name: 'fake-db-1' }); + expect(db.dropDatabase).toBeCalledTimes(0); + await lib.destroyDb({ name: 'fake-db-2' }); + expect(db.dropDatabase).toBeCalledTimes(0); + await lib.destroyDb({ name: 'fake-db-1' }); + expect(db.dropDatabase).toBeCalledTimes(1); + }, + { MONGODB_URI: 'abc' } + ); + }); +}); + +describe('::getNameFromAlias', () => { + it('returns an actual database name', async () => { + expect.hasAssertions(); + await expect(importDbLib().getNameFromAlias('fake-alias-2')).resolves.toBe( + 'fake-db-2' + ); + }); + + it('passes through actual database name if given', async () => { + expect.hasAssertions(); + + await expect(importDbLib().getNameFromAlias('fake-db-3')).resolves.toBe( + 'fake-db-3' + ); + }); + + it('throws if database is not in schema', async () => { + expect.hasAssertions(); + await expect(importDbLib().getNameFromAlias('fake-alias-x')).rejects.toThrow( + 'database "fake-alias-x" is not defined' + ); + }); +}); + +describe('::getAliasFromName', () => { + it('returns one or more aliases', async () => { + expect.hasAssertions(); + + await expect(importDbLib().getAliasFromName('fake-db-1')).resolves.toStrictEqual([ + 'fake-alias-1' + ]); + + await expect(importDbLib().getAliasFromName('fake-db-2')).resolves.toStrictEqual([ + 'fake-alias-2', + 'fake-alias-3' + ]); + }); + + it('passes through actual database name if given', async () => { + expect.hasAssertions(); + + await expect(importDbLib().getAliasFromName('fake-db-3')).resolves.toStrictEqual([ + 'fake-db-3' + ]); + }); + + it('throws if database is not in schema', async () => { + expect.hasAssertions(); + await expect(importDbLib().getAliasFromName('fake-alias-3')).rejects.toThrow( + 'database "fake-alias-3" is not defined' + ); + }); +}); + +describe('::initializeDb', () => { + it("initializes a database's collections according to schema", async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + + await withMockedEnv( + async () => { + const schema = await lib.getSchemaConfig(); + const db1 = await lib.getDb({ name: 'fake-db-1' }); + const db2 = await lib.getDb({ name: 'fake-db-2' }); + + await lib.initializeDb({ name: 'fake-db-1' }); + await lib.initializeDb({ name: 'fake-db-2' }); + + schema.databases['fake-db-1'].collections.forEach((col) => { + expect(db1.createCollection).toBeCalledWith( + ...(typeof col === 'string' + ? [col, undefined] + : [col.name, col.createOptions]) + ); + }); + + schema.databases['fake-db-2'].collections.forEach((col) => { + if (typeof col === 'string') { + expect(db2.createCollection).toBeCalledWith(col, undefined); + } else { + expect(db2.createCollection).toBeCalledWith(col.name, col.createOptions); + + if (col.indices) { + col.indices.forEach((spec) => + expect(db2.createIndex).toBeCalledWith(spec.spec, spec.options || {}) + ); + } + } + }); + }, + { MONGODB_URI: 'abc' } + ); + }); +}); diff --git a/lib/mongo-test/index.ts b/lib/mongo-test/index.ts new file mode 100644 index 0000000..117ef26 --- /dev/null +++ b/lib/mongo-test/index.ts @@ -0,0 +1,262 @@ +import { MongoClient } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { InvalidAppConfigurationError, TrialError } from 'named-app-errors'; +import inspector from 'node:inspector'; + +import { debugFactory } from 'multiverse/debug-extended'; +import { getEnv } from 'multiverse/next-env'; + +import { + closeClient, + destroyDb, + getAliasFromName, + getDb, + getInitialInternalMemoryState, + getNameFromAlias, + getSchemaConfig, + initializeDb, + overwriteMemory +} from 'multiverse/mongo-schema'; + +import type { Document } from 'mongodb'; +import type { DbSchema } from 'multiverse/mongo-schema'; + +// TODO: this package must be published transpiled to cjs by babel but NOT +// TODO: webpacked! + +const debug = debugFactory('mongo-test:test-db'); + +/** + * Generic dummy data used to hydrate databases and their collections. + */ +export type DummyData = { + /** + * The data inserted into each collection in the named database. + * `databaseName` can also be an alias. + */ + [databaseName: string]: { + /** + * Timestamp of when this dummy data was generated (in ms since unix epoch). + */ + _generatedAt: number; + + /** + * The objects (if array) or object (if non-array) inserted into the + * named collection. + */ + [collectionName: string]: unknown; + }; +}; + +/** + * For use when mocking the contents of files containing `getDummyData` and/or + * `getSchemaConfig`. + */ +export type TestCustomizations = { + getDummyData: () => Promise; + getSchemaConfig: () => Promise; +}; + +/** + * Imports `getDummyData` from "configverse/get-dummy-data" and calls it. + */ +export async function getDummyData(): Promise { + try { + debug('importing `getDummyData` from "configverse/get-dummy-data"'); + return await (await import('configverse/get-dummy-data')).getDummyData(); + } catch (error) { + debug.warn( + `failed to import getDummyData from "configverse/get-dummy-data": ${error}` + ); + + throw new InvalidAppConfigurationError( + 'could not resolve mongodb dummy data: failed to import getDummyData from "configverse/get-dummy-data". Did you forget to register "configverse/get-dummy-data" as an import alias/path?' + ); + } +} + +/** + * Fill an initialized database with data. You should call {@link initializeDb} + * before calling this function. + */ +export async function hydrateDb({ + name +}: { + /** + * The name or alias of the database to hydrate. + */ + name: string; +}) { + const db = await getDb({ name }); + const nameActual = await getNameFromAlias(name); + const aliases = await getAliasFromName(nameActual); + + debug(`hydrating database ${nameActual}`); + + const rawDummyData = await getDummyData(); + let dummyData = rawDummyData[nameActual]; + + if (aliases[0] !== nameActual) { + const foundAliases = aliases.filter((alias) => !!rawDummyData[alias]); + + if (foundAliases.length > 1) { + throw new InvalidAppConfigurationError( + `the following aliases have duplicate dummy data specifications (only one may exist): ${foundAliases.join( + ', ' + )}` + ); + } + + const alias = foundAliases[0]; + + if (alias) { + if (dummyData) { + throw new InvalidAppConfigurationError( + `duplicate dummy data specifications for database "${nameActual}" and alias "${alias}"` + ); + } + + debug(`(using alias "${alias}")`); + dummyData = rawDummyData[alias]; + } + } + + if (!dummyData) { + throw new InvalidAppConfigurationError( + `dummy data for database "${nameActual}" does not exist` + ); + } + + // eslint-disable-next-line unicorn/prefer-set-has + const collectionNames = (await getSchemaConfig()).databases[ + nameActual + ].collections.map((col) => (typeof col === 'string' ? col : col.name)); + + await Promise.all( + Object.entries(dummyData).map(([colName, colSchema]) => { + if (colName !== '_generatedAt') { + if (!collectionNames.includes(colName)) { + throw new InvalidAppConfigurationError( + `collection "${nameActual}.${colName}" referenced in dummy data is not defined in source db schema` + ); + } + + return db.collection(colName).insertMany([colSchema].flat() as Document[]); + } + }) + ); +} + +/** + * Setup per-test versions of the mongodb client and database connections using + * jest lifecycle hooks. + */ +export function setupMemoryServerOverride(params?: { + /** + * If `true`, `beforeEach` and `afterEach` lifecycle hooks are skipped and the + * database is initialized and hydrated once before all tests are run. **In + * this mode, all tests will share the same database state!** + * + * @default false + */ + defer?: boolean; +}) { + // ? If an error (like a bad schema config or misconfigured dummy dataset) + // ? occurs at any point (e.g. in one of the hooks), the other hooks should + // ? become noops. Without this, test database state may leak outside the test + // ? environment. If an .env file is defined, test state could leak into a + // ? real mongodb instance (super bad!!!) + let errored = false; + + const port = + // * https://stackoverflow.com/a/67445850/1367414 + ((getEnv().DEBUG_INSPECTING || inspector.url() !== undefined) && + getEnv().MONGODB_MS_PORT) || + undefined; + + debug(`using ${port ? `port ${port}` : 'random port'} for mongo memory server`); + + // * The in-memory server is not started until it's needed later on + const server = new MongoMemoryServer({ + instance: { + port + // ? MongoDB errors WITHOUT this line as of version 4.x + // ? However, MongoDB errors WITH this line as of version 5.x πŸ™ƒ + // args: ['--enableMajorityReadConcern=0'] + } + }); + + /** + * Reset the dummy MongoDb server databases back to their initial states. + */ + const reinitializeServer = async () => { + try { + if (errored) { + debug.warn( + 'reinitialization was skipped due to a previous jest lifecycle errors' + ); + } else { + const databases = Object.keys((await getSchemaConfig()).databases); + debug('reinitializing mongo databases'); + await Promise.all( + databases.map((name) => + destroyDb({ name }) + .then(() => initializeDb({ name })) + .then(() => hydrateDb({ name })) + ) + ); + } + } catch (error) { + errored = true; + debug.error('an error occurred during reinitialization'); + throw error; + } + }; + + beforeAll(async () => { + try { + if (errored) { + debug.warn( + '"beforeAll" jest lifecycle hook was skipped due to previous errors' + ); + } else { + await server.ensureInstance(); + const uri = server.getUri(); + debug(`connecting to in-memory dummy mongo server at ${uri}`); + + if (port && !(uri.endsWith(`:${port}/`) || uri.endsWith(`:${port}`))) { + throw new TrialError( + `unable to start mongodb memory server: port ${port} seems to be in use` + ); + } + + overwriteMemory({ + ...getInitialInternalMemoryState(), + client: await MongoClient.connect(uri) + }); + + if (params?.defer) await reinitializeServer(); + } + } catch (error) { + errored = true; + debug.error('an error occurred within "beforeAll" lifecycle hook'); + throw error; + } + }); + + if (!params?.defer) { + beforeEach(reinitializeServer); + } + + afterAll(async () => { + await closeClient(); + await server.stop({ force: true }); + }); + + return { + /** + * Reset the dummy MongoDb server databases back to their initial states. + */ + reinitializeServer + }; +} diff --git a/lib/mongo-test/package.json b/lib/mongo-test/package.json new file mode 100644 index 0000000..0dfe940 --- /dev/null +++ b/lib/mongo-test/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/mongo-test" +} diff --git a/lib/mongo-test/test/integration.test.ts b/lib/mongo-test/test/integration.test.ts new file mode 100644 index 0000000..3008717 --- /dev/null +++ b/lib/mongo-test/test/integration.test.ts @@ -0,0 +1,115 @@ +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { getClient } from 'multiverse/mongo-schema'; + +import type { TestCustomizations } from 'multiverse/mongo-test'; + +jest.mock( + 'configverse/get-schema-config', + (): typeof import('configverse/get-schema-config') => + mockedMongoCustomizations as unknown as typeof import('configverse/get-schema-config') +); + +jest.mock( + 'configverse/get-dummy-data', + (): typeof import('configverse/get-dummy-data') => + mockedMongoCustomizations as unknown as typeof import('configverse/get-dummy-data') +); + +const now = Date.now(); + +let mockedMongoCustomizations: TestCustomizations; + +beforeEach(() => { + mockedMongoCustomizations = mockedMongoCustomizations || {}; + + mockedMongoCustomizations.getSchemaConfig = async () => { + return { + databases: { + 'db-1': { + collections: ['col'] + }, + + 'db-2': { + collections: [ + 'col-1', + { name: 'col-2', createOptions: { capped: true, size: 256 } }, + { + name: 'col-3', + indices: [ + { spec: ['key', -1], options: { unique: true } }, + { spec: 'item' } + ] + } + ] + } + }, + aliases: { + 'alias-1': 'db-1', + 'alias-2': 'db-2' + } + }; + }; + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'db-1': { + _generatedAt: now, + col: [{ item: 1 }, { item: 2 }, { item: 3 }] + }, + 'db-2': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }], + 'col-2': [{ item: 'c' }], + 'col-3': [{ key: 1, item: 'd' }] + } + }; + }; +}); + +describe('[run using non-deferred setupMemoryServerOverride]', () => { + setupMemoryServerOverride(); + + test('setupMemoryServerOverride works', async () => { + expect.hasAssertions(); + + const client = await getClient(); + const db1 = client.db('db-1'); + const db2 = client.db('db-2'); + + await expect(db1.listCollections().next()).resolves.toStrictEqual( + expect.objectContaining({ name: 'col', options: {} }) + ); + + await expect(db2.listCollections().toArray()).resolves.toIncludeAllPartialMembers( + [ + { name: 'col-1', options: {} }, + { name: 'col-2', options: { capped: true, size: 256 } }, + { name: 'col-3', options: {} } + ] + ); + + await expect( + db2.collection('col-3').listIndexes().toArray() + ).resolves.toIncludeAllPartialMembers([ + { key: { _id: 1 } }, + { key: { key: -1 }, unique: true }, + { key: { item: 1 } } + ]); + + await expect( + db1.collection('col').find().toArray() + ).resolves.toIncludeAllPartialMembers([{ item: 1 }, { item: 2 }, { item: 3 }]); + + await expect( + db2.collection('col-1').find().toArray() + ).resolves.toIncludeAllPartialMembers([{ item: 'a' }, { item: 'b' }]); + + await expect( + db2.collection('col-2').find().toArray() + ).resolves.toIncludeAllPartialMembers([{ item: 'c' }]); + + await expect( + db2.collection('col-3').find().toArray() + ).resolves.toIncludeAllPartialMembers([{ key: 1, item: 'd' }]); + }); +}); diff --git a/lib/mongo-test/test/unit.test.ts b/lib/mongo-test/test/unit.test.ts new file mode 100644 index 0000000..41b5536 --- /dev/null +++ b/lib/mongo-test/test/unit.test.ts @@ -0,0 +1,707 @@ +import { asMockedClass, asMockedFunction } from '@xunnamius/jest-types'; +import { MongoClient } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { isolatedImportFactory, mockEnvFactory } from 'testverse/setup'; + +import type { TestCustomizations } from 'multiverse/mongo-test'; +import { DummyError } from 'named-app-errors'; +import { toss } from 'toss-expression'; + +jest.mock('mongodb'); +jest.mock('mongodb-memory-server'); + +jest.mock('multiverse/mongo-schema', (): typeof import('multiverse/mongo-schema') => { + return mockedMongoSchema ?? jest.requireActual('multiverse/mongo-schema'); +}); + +jest.mock( + 'configverse/get-schema-config', + (): typeof import('configverse/get-schema-config') => + mockedMongoCustomizations as unknown as typeof import('configverse/get-schema-config') +); + +jest.mock( + 'configverse/get-dummy-data', + (): typeof import('configverse/get-dummy-data') => + mockedMongoCustomizations as unknown as typeof import('configverse/get-dummy-data') +); + +const now = Date.now(); +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); + +type MongoSchemaPackage = typeof import('multiverse/mongo-schema'); + +let mockedMongoSchema: MongoSchemaPackage | undefined; +let mockedMongoCustomizations: TestCustomizations; + +const mockMongoClient = asMockedClass(MongoClient); +const mockMongoMemoryServer = asMockedClass(MongoMemoryServer); + +const mockedMongoMemoryServer = { + ensureInstance: jest.fn(), + getUri: jest.fn(), + stop: jest.fn() +} as unknown as MongoMemoryServer; + +const importDbLib = isolatedImportFactory({ + path: 'multiverse/mongo-schema' +}); + +const importTestDbLib = isolatedImportFactory( + { + path: 'multiverse/mongo-test' + } +); + +beforeEach(() => { + mockedMongoSchema = undefined; + mockedMongoCustomizations = mockedMongoCustomizations || {}; + + mockedMongoCustomizations.getSchemaConfig = async () => { + return { + databases: { + 'fake-db-1': { + collections: ['col'] + }, + 'fake-db-2': { + collections: [ + 'col-1', + { name: 'col-2', createOptions: { capped: true } }, + { name: 'col-3', indices: [{ spec: 'some-key' }] }, + { + name: 'col-4', + indices: [{ spec: ['some-key', -1], options: { comment: '' } }] + } + ] + } + }, + aliases: { + 'fake-alias-1': 'fake-db-1', + 'fake-alias-2': 'fake-db-2', + 'fake-alias-3': 'fake-db-2' + } + }; + }; + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: now, + col: [{ item: 1 }, { item: 2 }, { item: 3 }] + }, + 'fake-db-2': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }], + 'col-does-not-exist': [{ fake: true }] + } + }; + }; + + mockMongoClient.connect = jest.fn(async (url: string) => { + return new (class { + url = url; + + db(name: string) { + return new (class { + parentUrl = url; + databaseName = name; + dropDatabase; + createCollection; + createIndex; + collection; + admin; + + constructor() { + this.dropDatabase = jest.fn(); + this.createIndex = jest.fn(); + // ? Reuse this.createIndex method for easy access to mock + this.collection = jest.fn(() => ({ insertMany: this.createIndex })); + this.createCollection = jest.fn(() => + Promise.resolve({ createIndex: this.createIndex }) + ); + this.admin = jest.fn(() => ({ + listDatabases: jest.fn(() => ({ + databases: [ + { name: 'auth' }, + { name: 'request-log' }, + { name: 'limited-log' } + ] + })) + })); + } + })(); + } + + close() { + return url; + } + })() as unknown as MongoClient; + }); + + mockMongoMemoryServer.mockImplementation(() => mockedMongoMemoryServer); +}); + +describe('::getDummyData', () => { + it('dynamically imports customizations', async () => { + expect.hasAssertions(); + + await expect(importTestDbLib().getDummyData()).resolves.toStrictEqual( + await mockedMongoCustomizations.getDummyData() + ); + }); + + it('rejects if customizations are unavailable', async () => { + expect.hasAssertions(); + + // @ts-expect-error: don't care that we're deleting a non-optional prop + delete mockedMongoCustomizations.getDummyData; + await expect(importTestDbLib().getDummyData()).rejects.toThrow( + 'configverse/get-dummy-data' + ); + }); +}); + +describe('::hydrateDb', () => { + it('fills a database with dummy data (multi-item collections)', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + const db = await lib.getDb({ name: 'fake-db-1' }); + + await expect(testLib.hydrateDb({ name: 'fake-db-1' })).resolves.toBeUndefined(); + + Object.entries((await testLib.getDummyData())['fake-db-1']).forEach( + ([colName, colData]) => { + if (colName !== '_generatedAt') { + expect(db.collection).toBeCalledWith(colName); + // ? The createIndex method is reused for easy access to the insertMany mock + expect(db.createIndex).toBeCalledWith(colData); + } + } + ); + }); + + it('handles non-aliased databases with single-item collections', async () => { + expect.hasAssertions(); + + mockedMongoCustomizations.getSchemaConfig = async () => { + return { + databases: { + 'fake-db-1': { + collections: ['col'] + } + }, + aliases: {} + }; + }; + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: 0, + col: { item: 'single', name: 'just-the-one' } + } + }; + }; + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + const db = await lib.getDb({ name: 'fake-db-1' }); + + await expect(testLib.hydrateDb({ name: 'fake-db-1' })).resolves.toBeUndefined(); + + expect(db.collection).toBeCalledWith('col'); + // ? The createIndex method is reused for easy access to the insertMany mock + expect(db.createIndex).toBeCalledWith([ + (await testLib.getDummyData())['fake-db-1'].col + ]); + }); + + it('accepts an alias as a name', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + const db = await lib.getDb({ name: 'fake-alias-1' }); + + await expect( + testLib.hydrateDb({ name: 'fake-alias-1' }) + ).resolves.toBeUndefined(); + + Object.entries((await testLib.getDummyData())['fake-db-1']).forEach( + ([colName, colData]) => { + if (colName !== '_generatedAt') { + expect(db.collection).toBeCalledWith(colName); + // ? The createIndex method is reused for easy access to the insertMany mock + expect(db.createIndex).toBeCalledWith(colData); + } + } + ); + }); + + it('reverse-maps database name to alias when necessary', async () => { + expect.hasAssertions(); + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: now, + col: [{ item: 1 }, { item: 2 }, { item: 3 }] + }, + 'fake-alias-3': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }] + } + }; + }; + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + const db = await lib.getDb({ name: 'fake-db-2' }); + + await expect(testLib.hydrateDb({ name: 'fake-db-2' })).resolves.toBeUndefined(); + + Object.entries((await testLib.getDummyData())['fake-alias-3']).forEach( + ([colName, colData]) => { + if (colName !== '_generatedAt') { + expect(db.collection).toBeCalledWith(colName); + // ? The createIndex method is reused for easy access to the insertMany mock + expect(db.createIndex).toBeCalledWith(colData); + } + } + ); + }); + + it('throws if both actual database name and alias represent the same dummy data', async () => { + expect.hasAssertions(); + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: now, + col: [{ item: 1 }, { item: 2 }, { item: 3 }] + }, + 'fake-db-2': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }] + }, + 'fake-alias-3': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }] + } + }; + }; + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + + await expect(testLib.hydrateDb({ name: 'fake-alias-3' })).rejects.toThrow( + /duplicate dummy data specifications for database "fake-db-2" and alias "fake-alias-3"/ + ); + + await expect(testLib.hydrateDb({ name: 'fake-db-2' })).rejects.toThrow( + /duplicate dummy data specifications for database "fake-db-2" and alias "fake-alias-3"/ + ); + }); + + it('throws if two aliases represent the same dummy data', async () => { + expect.hasAssertions(); + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: now, + col: [{ item: 1 }, { item: 2 }, { item: 3 }] + }, + 'fake-alias-2': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }] + }, + 'fake-alias-3': { + _generatedAt: now, + 'col-1': [{ item: 'a' }, { item: 'b' }] + } + }; + }; + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + + await expect(testLib.hydrateDb({ name: 'fake-alias-2' })).rejects.toThrow( + /the following aliases have duplicate dummy data specifications \(only one may exist\): fake-alias-2, fake-alias-3/ + ); + + await expect(testLib.hydrateDb({ name: 'fake-alias-3' })).rejects.toThrow( + /the following aliases have duplicate dummy data specifications \(only one may exist\): fake-alias-2, fake-alias-3/ + ); + }); + + it('throws if database in schema has no corresponding dummy data', async () => { + expect.hasAssertions(); + + mockedMongoCustomizations.getSchemaConfig = async () => { + return { + databases: { + 'fake-db-2': { + collections: ['col'] + } + }, + aliases: {} + }; + }; + + mockedMongoCustomizations.getDummyData = async () => { + return { + 'fake-db-1': { + _generatedAt: 0, + col: { item: 'single', name: 'just-the-one' } + } + }; + }; + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + + await expect(testLib.hydrateDb({ name: 'fake-db-2' })).rejects.toThrow( + /dummy data for database "fake-db-2" does not exist/ + ); + }); + + it('throws if collection referenced in dummy data is not in schema', async () => { + expect.hasAssertions(); + + const lib = importDbLib(); + mockedMongoSchema = lib; + const testLib = importTestDbLib(); + + await expect(testLib.hydrateDb({ name: 'fake-db-2' })).rejects.toThrow( + /collection "fake-db-2.col-does-not-exist" referenced in dummy data is not defined in source db schema/ + ); + }); +}); + +describe('::setupMemoryServerOverride', () => { + it('registers jest hooks with respect to defer', async () => { + expect.hasAssertions(); + + const oldBeforeAll = beforeAll; + const oldBeforeEach = beforeEach; + const oldAfterAll = afterAll; + + try { + const testLib = importTestDbLib(); + + // eslint-disable-next-line no-global-assign + beforeAll = jest.fn(); + // eslint-disable-next-line no-global-assign + beforeEach = jest.fn(); + // eslint-disable-next-line no-global-assign + afterAll = jest.fn(); + + testLib.setupMemoryServerOverride(); + + expect(beforeAll).toBeCalledTimes(1); + expect(beforeEach).toBeCalledTimes(1); + expect(afterAll).toBeCalledTimes(1); + + testLib.setupMemoryServerOverride({ defer: true }); + + expect(beforeAll).toBeCalledTimes(2); + expect(beforeEach).toBeCalledTimes(1); + expect(afterAll).toBeCalledTimes(2); + } finally { + // eslint-disable-next-line no-global-assign + beforeAll = oldBeforeAll; + // eslint-disable-next-line no-global-assign + beforeEach = oldBeforeEach; + // eslint-disable-next-line no-global-assign + afterAll = oldAfterAll; + } + }); + + it('non-deferred hooks run', async () => { + expect.hasAssertions(); + + const oldBeforeAll = beforeAll; + const oldBeforeEach = beforeEach; + const oldAfterAll = afterAll; + + try { + await withMockedEnv(async () => { + const lib = importDbLib(); + + mockedMongoSchema = lib; + + const testLib = importTestDbLib(); + + const destroySpy = jest + .spyOn(lib, 'destroyDb') + .mockImplementation(async () => true); + + const initializeDbSpy = jest + .spyOn(lib, 'initializeDb') + .mockImplementation(async () => undefined); + + const hydrateDbSpy = jest + .spyOn(testLib, 'hydrateDb') + .mockImplementation(async () => undefined); + + const closeClientSpy = jest + .spyOn(lib, 'closeClient') + .mockImplementation(async () => undefined); + + // eslint-disable-next-line no-global-assign + beforeAll = jest.fn(); + // eslint-disable-next-line no-global-assign + beforeEach = jest.fn(); + // eslint-disable-next-line no-global-assign + afterAll = jest.fn(); + + testLib.setupMemoryServerOverride(); + + expect(beforeAll).toBeCalledTimes(1); + expect(beforeEach).toBeCalledTimes(1); + expect(afterAll).toBeCalledTimes(1); + + await asMockedFunction(beforeAll).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ); + + expect(destroySpy).not.toHaveBeenCalled(); + expect(initializeDbSpy).not.toHaveBeenCalled(); + expect(hydrateDbSpy).not.toHaveBeenCalled(); + expect(closeClientSpy).not.toHaveBeenCalled(); + // eslint-disable-next-line jest/unbound-method + expect(asMockedFunction(mockedMongoMemoryServer.stop)).not.toHaveBeenCalled(); + + await asMockedFunction(afterAll).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ); + + expect(closeClientSpy).toHaveBeenCalled(); + // eslint-disable-next-line jest/unbound-method + expect(asMockedFunction(mockedMongoMemoryServer.stop)).toHaveBeenCalled(); + + testLib.setupMemoryServerOverride({ defer: true }); + + expect(beforeAll).toBeCalledTimes(2); + expect(beforeEach).toBeCalledTimes(1); + expect(afterAll).toBeCalledTimes(2); + + await asMockedFunction(beforeAll).mock.calls[1][0]( + undefined as unknown as jest.DoneCallback + ); + + Object.keys( + (await mockedMongoCustomizations.getSchemaConfig()).databases + ).map((name) => { + expect(destroySpy).toBeCalledWith({ name }); + expect(initializeDbSpy).toBeCalledWith({ name }); + expect(hydrateDbSpy).toBeCalledWith({ name }); + }); + + await asMockedFunction(afterAll).mock.calls[1][0]( + undefined as unknown as jest.DoneCallback + ); + + expect(closeClientSpy).toBeCalledTimes(2); + // eslint-disable-next-line jest/unbound-method + expect(asMockedFunction(mockedMongoMemoryServer.stop)).toBeCalledTimes(2); + }); + } finally { + // eslint-disable-next-line no-global-assign + beforeAll = oldBeforeAll; + // eslint-disable-next-line no-global-assign + beforeEach = oldBeforeEach; + // eslint-disable-next-line no-global-assign + afterAll = oldAfterAll; + } + }); + + it('uses the debug port when inspecting', async () => { + expect.hasAssertions(); + + const oldBeforeAll = beforeAll; + const oldBeforeEach = beforeEach; + const oldAfterAll = afterAll; + + try { + const testLib = importTestDbLib(); + + // eslint-disable-next-line no-global-assign + beforeAll = jest.fn(); + // eslint-disable-next-line no-global-assign + beforeEach = jest.fn(); + // eslint-disable-next-line no-global-assign + afterAll = jest.fn(); + + await withMockedEnv( + async () => { + testLib.setupMemoryServerOverride(); + expect(mockMongoMemoryServer).toBeCalledWith({ + instance: expect.objectContaining({ port: 5678 }) + }); + }, + { + VSCODE_INSPECTOR_OPTIONS: 'exists', + MONGODB_MS_PORT: '5678' + } + ); + } finally { + // eslint-disable-next-line no-global-assign + beforeAll = oldBeforeAll; + // eslint-disable-next-line no-global-assign + beforeEach = oldBeforeEach; + // eslint-disable-next-line no-global-assign + afterAll = oldAfterAll; + } + }); + + it('hook rejects if port does not match uri (EADDRINUSE)', async () => { + expect.hasAssertions(); + + const oldBeforeAll = beforeAll; + const oldBeforeEach = beforeEach; + const oldAfterAll = afterAll; + + try { + const testLib = importTestDbLib(); + + // eslint-disable-next-line no-global-assign + beforeAll = jest.fn(); + // eslint-disable-next-line no-global-assign + beforeEach = jest.fn(); + // eslint-disable-next-line no-global-assign + afterAll = jest.fn(); + + // eslint-disable-next-line jest/unbound-method + asMockedFunction(mockedMongoMemoryServer.getUri).mockImplementationOnce( + () => 'uri-not-ending-in-colon-5678' + ); + + await withMockedEnv( + async () => { + testLib.setupMemoryServerOverride(); + + await expect( + asMockedFunction(beforeAll).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).rejects.toThrow('port 5678 seems to be in use'); + }, + { + VSCODE_INSPECTOR_OPTIONS: 'exists', + MONGODB_MS_PORT: '5678' + } + ); + } finally { + // eslint-disable-next-line no-global-assign + beforeAll = oldBeforeAll; + // eslint-disable-next-line no-global-assign + beforeEach = oldBeforeEach; + // eslint-disable-next-line no-global-assign + afterAll = oldAfterAll; + } + }); + + it('any rejection turns lifecycle hooks into noops', async () => { + expect.hasAssertions(); + + const oldBeforeAll = beforeAll; + const oldBeforeEach = beforeEach; + const oldAfterAll = afterAll; + + try { + const lib = importDbLib(); + + mockedMongoSchema = lib; + + const testLib = importTestDbLib(); + + const destroySpy = jest + .spyOn(lib, 'destroyDb') + .mockImplementation(async () => true); + + // eslint-disable-next-line no-global-assign + beforeAll = jest.fn(); + // eslint-disable-next-line no-global-assign + beforeEach = jest.fn(); + // eslint-disable-next-line no-global-assign + afterAll = jest.fn(); + + // eslint-disable-next-line jest/unbound-method + asMockedFunction(mockedMongoMemoryServer.getUri).mockImplementationOnce( + () => 'uri-not-ending-in-colon-5678' + ); + + await withMockedEnv( + async () => { + testLib.setupMemoryServerOverride(); + + await expect( + asMockedFunction(beforeAll).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).rejects.toThrow('port 5678 seems to be in use'); + + // ? Calling it a second time turns it into a noop + await expect( + asMockedFunction(beforeAll).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).resolves.toBeUndefined(); + + // ? Other hooks are also noops + await expect( + asMockedFunction(beforeEach).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).resolves.toBeUndefined(); + + expect(destroySpy).not.toHaveBeenCalled(); + }, + { + VSCODE_INSPECTOR_OPTIONS: 'exists', + MONGODB_MS_PORT: '5678' + } + ); + + asMockedFunction(beforeAll).mockReset(); + asMockedFunction(beforeEach).mockReset(); + asMockedFunction(afterAll).mockReset(); + jest + .spyOn(lib, 'getSchemaConfig') + .mockImplementation(() => toss(new DummyError())); + + testLib.setupMemoryServerOverride(); + + await expect( + asMockedFunction(beforeEach).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).rejects.toThrowError(DummyError); + + // ? Calling it a second time turns it into a noop + await expect( + asMockedFunction(beforeEach).mock.calls[0][0]( + undefined as unknown as jest.DoneCallback + ) + ).resolves.toBeUndefined(); + } finally { + // eslint-disable-next-line no-global-assign + beforeAll = oldBeforeAll; + // eslint-disable-next-line no-global-assign + beforeEach = oldBeforeEach; + // eslint-disable-next-line no-global-assign + afterAll = oldAfterAll; + } + }); +}); diff --git a/lib/next-adhesive/add-raw-body.ts b/lib/next-adhesive/add-raw-body.ts new file mode 100644 index 0000000..8ac4c8d --- /dev/null +++ b/lib/next-adhesive/add-raw-body.ts @@ -0,0 +1,169 @@ +import querystring from 'node:querystring'; +import { isNativeError } from 'node:util/types'; +import { parse } from 'content-type'; +import getRawBody, { type RawBodyError } from 'raw-body'; + +import { + ClientValidationError, + InvalidAppConfigurationError +} from 'named-app-errors'; + +import { sendHttpTooLarge } from 'multiverse/next-api-respond'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:add-raw-body'); + +// * https://xunn.at/source-nextjs-defaultbodylimit +const defaultRequestBodySizeLimit = '1mb'; + +const isRawBodyError = (error: unknown): error is RawBodyError => { + return isNativeError(error) && typeof (error as RawBodyError).type === 'string'; +}; + +/** + * The shape of an object (typically a NextApiRequest object) that has a rawBody + * property. + */ +export type WithRawBody = T & { + /** + * The raw request body exactly as it was received (as a string parsed by + * `raw-body`). + */ + rawBody: string; +}; + +export type Options = { + /** + * The byte limit of the request body. This is the number of bytes or any + * string format supported by bytes, for example `1000`, `'500kb'` or `'3mb'`. + * + * @default defaultRequestBodySizeLimit (see source) + * + * @see https://nextjs.org/docs/api-routes/api-middlewares#custom-config + */ + requestBodySizeLimit?: number | string | null; +}; + +/** + * Type predicate function that returns `true` if the request object can + * satisfy the `WithRawBody` type constraint. + */ +export function isNextApiRequestWithRawBody( + req: NextApiRequest +): req is WithRawBody { + return (req as WithRawBody).rawBody !== undefined; +} + +/** + * Type guard function similar to the `isNextApiRequestWithRawBody` type + * predicate except an error is thrown if the request object cannot satisfy the + * `WithRawBody` type constraint. + */ +export function ensureNextApiRequestHasRawBody( + req: NextApiRequest +): req is WithRawBody { + if ((req as WithRawBody).rawBody !== undefined) { + return true; + } else { + throw new InvalidAppConfigurationError( + 'encountered a NextApiRequest object without a rawBody property' + ); + } +} + +/** + * Adds a `rawBody` property onto the NextApiRequest object, which contains the + * raw unparsed content of the request body if it exists or `undefined` if it + * does not. The body is still parsed (using `bodyParser`) like normal using a + * custom implementation of Next.js's `parseBody` function. + * + * To use this middleware, `bodyParser` must be disabled via Next.js API route + * configuration like so: + * + * ```TypeScript + * export const config = { + * api: { + * bodyParser: false + * }, + * } + * ``` + * + * Note that this middleware cannot be used with other middleware or routes that + * also directly consume the request body in a special way, such as when using + * streams. + * + * @see https://nextjs.org/docs/api-routes/api-middlewares#custom-config + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + if (req.body !== undefined) { + throw new InvalidAppConfigurationError( + "Next.js's body parser must be disabled when using add-raw-body middleware" + ); + } else if (isNextApiRequestWithRawBody(req)) { + throw new InvalidAppConfigurationError( + 'NextApiRequest object already has a defined "rawBody" property (is the add-raw-body middleware obsolete?)' + ); + } else { + debug('adding "rawBody" property to request object via custom body parsing'); + + // * The below code was adapted from https://xunn.at/source-nextjs-parsebody + + let contentType; + + try { + contentType = parse(req.headers['content-type'] || 'text/plain'); + } catch { + contentType = parse('text/plain'); + } + + const { type, parameters } = contentType; + const encoding = parameters.charset || 'utf8'; + const limit = context.options.requestBodySizeLimit || defaultRequestBodySizeLimit; + + let buffer; + + try { + buffer = (await getRawBody(req, { encoding, limit })).toString(); + } catch (error) { + if (isRawBodyError(error) && error.type === 'entity.too.large') { + sendHttpTooLarge(res, { error: `body exceeded ${limit} size limit` }); + } else { + throw new ClientValidationError('invalid body'); + } + } + + if (buffer !== undefined) { + const finalReq = req as WithRawBody; + finalReq.rawBody = buffer; + + if (type === 'application/json' || type === 'application/ld+json') { + debug('secondary parsing of body as JSON data'); + if (finalReq.rawBody.length === 0) { + // special-case empty json body, as it's a common client-side mistake + finalReq.body = {}; + } else { + try { + finalReq.body = JSON.parse(finalReq.rawBody); + } catch { + throw new ClientValidationError('invalid JSON body'); + } + } + } else if (type === 'application/x-www-form-urlencoded') { + debug('secondary parsing of body as urlencoded form data'); + finalReq.body = querystring.decode(finalReq.rawBody); + } else { + debug('no secondary parsing of body (passthrough)'); + finalReq.body = finalReq.rawBody; + } + } + } +} diff --git a/lib/next-adhesive/auth-request.ts b/lib/next-adhesive/auth-request.ts new file mode 100644 index 0000000..d8a2a32 --- /dev/null +++ b/lib/next-adhesive/auth-request.ts @@ -0,0 +1,118 @@ +import { InvalidAppConfigurationError } from 'named-app-errors'; + +import { + authenticateHeader, + authorizeHeader, + type AuthenticationScheme, + type AuthorizationConstraint +} from 'multiverse/next-auth'; + +import { + sendHttpUnauthenticated, + sendHttpUnauthorized +} from 'multiverse/next-api-respond'; + +import { debugFactory } from 'multiverse/debug-extended'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:auth-request'); + +export type Options = { + /** + * If not `false` or falsy, accessing this endpoint requires a valid (yet + * unfortunately named) Authorization header. + * + * If one or more schemes are provided, the request will be authenticated + * using one of said schemes. If no schemes are provided, the request will be + * authenticated using any available scheme. + * + * Additionally, if one or more constraints are provided, the request will be + * authorized conditioned upon said constraints. If no constraints are + * provided, all requests will be vacuously authorized. + */ + requiresAuth?: + | boolean + | { + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; + constraints?: AuthorizationConstraint | AuthorizationConstraint[]; + }; +}; + +/** + * Rejects unauthenticatable and unauthorizable requests (via Authorization + * header). + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + const { authorization: header } = req.headers; + + if ( + typeof context.options.requiresAuth !== 'boolean' && + (!context.options.requiresAuth || + typeof context.options.requiresAuth !== 'object') + ) { + throw new InvalidAppConfigurationError( + 'a valid "requiresAuth" option is missing from middleware configuration' + ); + } + + if (context.options.requiresAuth) { + const allowedSchemes = + context.options.requiresAuth !== true + ? context.options.requiresAuth?.allowedSchemes + : undefined; + + const { authenticated, error: authenticationError } = await authenticateHeader({ + header, + allowedSchemes + }); + + if (!authenticated || authenticationError) { + debug( + `authentication check failed: ${ + authenticationError || 'bad Authorization header' + }` + ); + sendHttpUnauthenticated(res); + } else { + debug('authentication check succeeded: client is authenticated'); + + const constraints = + context.options.requiresAuth !== true + ? context.options.requiresAuth?.constraints + : undefined; + + if (constraints) { + debug(`authorization check required: ${constraints}`); + + const { authorized, error: authorizationError } = await authorizeHeader({ + header, + constraints + }); + + if (!authorized || authorizationError) { + debug( + `authorization check failed: ${ + authorizationError || 'bad Authorization header' + }` + ); + + sendHttpUnauthorized(res); + } + + debug('authorization check succeeded: client is authorized'); + } else { + debug('skipped authorization check'); + } + } + } else { + debug('skipped authentication and authorization checks'); + } +} diff --git a/lib/next-adhesive/check-content-type.ts b/lib/next-adhesive/check-content-type.ts new file mode 100644 index 0000000..c24fce1 --- /dev/null +++ b/lib/next-adhesive/check-content-type.ts @@ -0,0 +1,179 @@ +import { debugFactory } from 'multiverse/debug-extended'; +import { InvalidAppConfigurationError } from 'named-app-errors'; +import { toss } from 'toss-expression'; + +import { + sendHttpBadContentType, + sendHttpBadRequest +} from 'multiverse/next-api-respond'; + +import type { ValidHttpMethod } from '@xunnamius/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:check-content-type'); + +/** + * The shape of a simple configuration object. + */ +export type AllowedContentTypesConfig = string[] | 'any' | 'none'; + +/** + * The shape of a complex configuration object. + */ +export type AllowedContentTypesPerMethodConfig = { + [method in ValidHttpMethod]?: AllowedContentTypesConfig; +}; + +export type Options = { + /** + * A string, a mapping, or an array of media types this endpoint is + * allowed to receive. + * + * If the string `"any"` is provided, any Content-Type header will be allowed, + * including requests without a Content-Type header. + * + * If the string `"none"` is provided, only requests without a Content-Type + * header will be allowed. Similarly, `"none"` can also be included in the + * array form to indicate that requests without a Content-Type header are + * allowed in addition to those with a listed media type. + * + * If a plain object is provided, it is assumed to be a mapping of HTTP method + * keys and media type values where each value is one of the string `"any"` or + * `"none"` or an array of media types / `"none"`s. In this form, these + * constraints are applied per request method. + * + * By default, _all_ requests using `POST`, `PUT`, and `PATCH` methods, or any + * request _with_ a Content-Type header, _will always be rejected_ unless + * configured otherwise. Requests _without_ a Content-Type header that are + * using methods other than `POST`, `PUT`, and `PATCH` _will always be + * allowed_ unless explicitly configured via mapping. + * + * @see https://www.iana.org/assignments/media-types/media-types.xhtml + */ + allowedContentTypes?: + | AllowedContentTypesConfig + | AllowedContentTypesPerMethodConfig; +}; + +/** + * Rejects requests that are not using an allowed content type. This middleware + * should usually come _after_ check-method. + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + const { allowedContentTypes: rawAllowedContentTypes } = context.options; + const contentType = req.headers['content-type']?.toLowerCase(); + const method = req.method?.toUpperCase(); + + const configToLowercase = ( + c: AllowedContentTypesConfig + ): AllowedContentTypesConfig => { + return typeof c === 'string' + ? (c.toLowerCase() as typeof c) + : Array.isArray(c) + ? c.map((s) => s.toLowerCase()) + : toss( + new InvalidAppConfigurationError( + 'allowedContentTypes must adhere to type constraints' + ) + ); + }; + + // ? Ensure everything is lowercased before we begin + const allowed = (() => { + if (rawAllowedContentTypes) { + if ( + Array.isArray(rawAllowedContentTypes) || + typeof rawAllowedContentTypes === 'string' + ) { + return configToLowercase(rawAllowedContentTypes); + } else { + for (const [subMethod, config] of Object.entries(rawAllowedContentTypes)) { + if (config) { + rawAllowedContentTypes[subMethod as ValidHttpMethod] = + configToLowercase(config); + } + } + + return rawAllowedContentTypes; + } + } + })(); + + const sendError = () => { + const error = `unrecognized or disallowed Content-Type header for method ${method}: ${ + contentType ? `"${contentType}"` : '(none)' + }`; + + debug(`content-type check failed: ${error}`); + sendHttpBadContentType(res, { error }); + }; + + if (!method) { + debug('content-type check failed: method is undefined'); + sendHttpBadRequest(res, { error: 'undefined method' }); + } else { + const isPayloadMethod = ['PUT', 'POST', 'PATCH'].includes(method); + + if (!allowed) { + if (isPayloadMethod || contentType) { + debug( + 'content-type check failed: this request cannot be handled with the current configuration' + ); + sendHttpBadContentType(res, { + error: 'the server is not configured to handle this type of request' + }); + } + } else { + if (allowed === 'none') { + if (contentType) { + return sendError(); + } + } else if (allowed !== 'any') { + if (Array.isArray(allowed)) { + if (isPayloadMethod || contentType) { + const allowsNone = allowed.includes('none'); + if (!contentType) { + if (!allowsNone) { + return sendError(); + } + } else if (contentType === 'none' || !allowed.includes(contentType)) { + return sendError(); + } + } + } else { + if (Object.keys(allowed).includes(method)) { + const allowedSubset = allowed[method as ValidHttpMethod]; + + if (allowedSubset === 'none') { + if (contentType) { + return sendError(); + } + } else if (allowedSubset && allowedSubset !== 'any') { + const allowsNone = allowedSubset.includes('none'); + if (!contentType) { + if (!allowsNone) { + return sendError(); + } + } else if ( + contentType === 'none' || + !allowedSubset.includes(contentType) + ) { + return sendError(); + } + } + } else if (isPayloadMethod || contentType) { + return sendError(); + } + } + } + + debug(`content-type check succeeded: type "${contentType}" is allowed`); + } + } +} diff --git a/lib/next-adhesive/check-method.ts b/lib/next-adhesive/check-method.ts new file mode 100644 index 0000000..8abc2e2 --- /dev/null +++ b/lib/next-adhesive/check-method.ts @@ -0,0 +1,49 @@ +import { getEnv } from 'multiverse/next-env'; +import { sendHttpBadMethod } from 'multiverse/next-api-respond'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { ValidHttpMethod } from '@xunnamius/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:check-method'); + +export type Options = { + /** + * An array of HTTP methods this endpoint is allowed to serve. + */ + allowedMethods?: ValidHttpMethod[]; +}; + +/** + * Rejects requests that are either using a disallowed method or not using an + * allowed method. + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + debug(`original method: ${req.method}`); + + const method = req.method?.toUpperCase(); + const allowedMethods = context.options.allowedMethods?.map((m) => m.toUpperCase()); + + if ( + !method || + // ? Already guaranteed uppercase thanks to next-env + getEnv().DISALLOWED_METHODS.includes(method) || + !allowedMethods || + !allowedMethods.includes(method as ValidHttpMethod) + ) { + debug( + `method check failed: unrecognized or disallowed method "${method || '(none)'}"` + ); + + res.setHeader('Allow', allowedMethods?.join(',') || ''); + sendHttpBadMethod(res); + } else { + debug(`method check succeeded: method "${method}" is allowed`); + } +} diff --git a/lib/next-adhesive/check-version.ts b/lib/next-adhesive/check-version.ts new file mode 100644 index 0000000..ee4a738 --- /dev/null +++ b/lib/next-adhesive/check-version.ts @@ -0,0 +1,37 @@ +import { getEnv } from 'multiverse/next-env'; +import { sendHttpNotFound } from 'multiverse/next-api-respond'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:check-version'); + +export type Options = { + /** + * The version of the api this endpoint serves. + */ + apiVersion?: string; +}; + +/** + * Rejects requests to disabled versions of the API. + */ +export default async function ( + _req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + if (context.options.apiVersion !== undefined) { + if (getEnv().DISABLED_API_VERSIONS.includes(context.options.apiVersion)) { + debug('version check failed: endpoint is disabled'); + sendHttpNotFound(res); + } else { + debug('version check succeeded: endpoint is available'); + } + } else { + debug('skipped version check'); + } +} diff --git a/lib/next-adhesive/contrive-error.ts b/lib/next-adhesive/contrive-error.ts new file mode 100644 index 0000000..bec2e03 --- /dev/null +++ b/lib/next-adhesive/contrive-error.ts @@ -0,0 +1,39 @@ +import { isDueForContrivedError } from 'multiverse/next-contrived'; +import { sendHttpContrivedError } from 'multiverse/next-api-respond'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:contrive-error'); + +export type Options = { + /** + * If `true`, every Nth request will fail with a contrived error. + * + * @default false + */ + enableContrivedErrors?: boolean; +}; + +/** + * Rejects every Nth request with a dummy error (see .env.example). + */ +export default async function ( + _req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + if (context.options.enableContrivedErrors) { + if (await isDueForContrivedError()) { + debug('contrived error check determined client IS due for contrived error'); + sendHttpContrivedError(res); + } else { + debug('contrived error check determined client IS NOT due for contrived error'); + } + } else { + debug('skipped contrived error check'); + } +} diff --git a/lib/next-adhesive/handle-error.ts b/lib/next-adhesive/handle-error.ts new file mode 100644 index 0000000..939911c --- /dev/null +++ b/lib/next-adhesive/handle-error.ts @@ -0,0 +1,126 @@ +import { debugFactory } from 'multiverse/debug-extended'; + +import { + NotImplementedError, + GuruMeditationError, + ValidationError, + NotFoundError, + AuthError, + AppError, + AppValidationError +} from 'named-app-errors'; + +import { + sendHttpError, + sendHttpNotFound, + sendHttpUnauthorized, + sendHttpBadRequest, + sendNotImplemented +} from 'multiverse/next-api-respond'; + +import type { JsonError } from '@xunnamius/types'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { Promisable } from 'type-fest'; + +const debug = debugFactory('next-adhesive:handle-error'); + +/** + * Special middleware used to handle custom errors. + */ +export type ErrorHandler = ( + res: NextApiResponse, + errorJson: Partial +) => Promisable; + +/** + * A Map of Error class constructors to the special middleware that handles + * them. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ErrorHandlerMap = Map Error, ErrorHandler>; + +export type Options = { + /** + * A mapping of Error classes and the functions that handle them. + */ + errorHandlers?: ErrorHandlerMap; +}; + +/** + * Generic error handling middleware. **This should be among the final + * middleware to run on the error handling middleware chain.** + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + const { + runtime: { error }, + options: { errorHandlers } + } = context; + + if (res.writableEnded) { + // ? We're past the point where we're able to change the response. + debug('cannot handle error: response is no longer writable'); + debug('throwing unhandleable error'); + throw error; + } + + const errorJson: Partial = (error as { message: string }).message + ? { error: (error as { message: string }).message } + : {}; + + debug('handling error: %O', errorJson.error || '(no message)'); + + if (errorHandlers) { + for (const [errorType, errorHandler] of errorHandlers) { + if (error instanceof errorType) { + debug(`using custom error handler for type "${error.name}"`); + // eslint-disable-next-line no-await-in-loop + await errorHandler(res, errorJson); + return; + } + } + } + + debug( + `using default error handler${ + error instanceof Error ? ` for type "${error.name}"` : '' + }` + ); + + if (error instanceof GuruMeditationError) { + // eslint-disable-next-line no-console + console.error(`error - sanity check failed on request: ${req.url}\n`, error); + sendHttpError(res, { + error: 'sanity check failed: please report exactly what you did just now!' + }); + } else if (error instanceof AppValidationError) { + // eslint-disable-next-line no-console + console.error( + `error - server-side validation exception on request: ${req.url}\n`, + error + ); + sendHttpError(res, errorJson); + } else if (error instanceof ValidationError) { + sendHttpBadRequest(res, errorJson); + } else if (error instanceof AuthError) { + sendHttpUnauthorized(res, errorJson); + } else if (error instanceof NotFoundError) { + sendHttpNotFound(res, errorJson); + } else if (error instanceof NotImplementedError) { + sendNotImplemented(res); + } else if (error instanceof AppError) { + // eslint-disable-next-line no-console + console.error(`error - named exception on request: ${req.url}\n`, error); + sendHttpError(res, errorJson); + } else { + // eslint-disable-next-line no-console + console.error(`error - unnamed exception on request: ${req.url}\n`, error); + sendHttpError(res); + } +} diff --git a/lib/next-adhesive/limit-request.ts b/lib/next-adhesive/limit-request.ts new file mode 100644 index 0000000..2318357 --- /dev/null +++ b/lib/next-adhesive/limit-request.ts @@ -0,0 +1,42 @@ +import { getEnv } from 'multiverse/next-env'; +import { clientIsRateLimited } from 'multiverse/next-limit'; +import { debugFactory } from 'multiverse/debug-extended'; + +import { + sendHttpRateLimited, + sendHttpUnauthorized +} from 'multiverse/next-api-respond'; + +import type { NextApiRequest, NextApiResponse } from 'next'; + +const debug = debugFactory('next-adhesive:limit-request'); + +export type Options = { + // No options +}; + +/** + * Rejects requests from clients that have sent too many previous requests. + */ +export default async function (req: NextApiRequest, res: NextApiResponse) { + debug('entered middleware runtime'); + + if (getEnv().LOCKOUT_ALL_CLIENTS) { + debug('rate-limit check failed: all clients locked out'); + sendHttpUnauthorized(res, { + error: 'backend has temporarily locked out all clients' + }); + } else if (getEnv().IGNORE_RATE_LIMITS) { + debug('skipped rate-limit check'); + } else { + const { isLimited, retryAfter } = await clientIsRateLimited(req); + + if (isLimited) { + debug('rate-limit check failed: client is rate-limited'); + res.setHeader('Retry-After', Math.ceil(retryAfter / 1000)); + sendHttpRateLimited(res, { retryAfter }); + } else { + debug('rate-limit check succeeded: client not rate-limited'); + } + } +} diff --git a/lib/next-adhesive/log-request.ts b/lib/next-adhesive/log-request.ts new file mode 100644 index 0000000..7c27e4b --- /dev/null +++ b/lib/next-adhesive/log-request.ts @@ -0,0 +1,45 @@ +import { randomUUID } from 'node:crypto'; +import { performance as perf } from 'node:perf_hooks'; + +import { addToRequestLog } from 'multiverse/next-log'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; + +const debug = debugFactory('next-adhesive:log-request'); + +export type Options = { + // No options +}; + +/** + * Logs the response to each request after it is sent (i.e. `res.end()`). + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + const perfUUID = randomUUID(); + perf.mark(perfUUID); + + const send = res.end; + res.end = ((...args: Parameters) => { + const sent = res.writableEnded; + send(...args); + + if (!sent) { + debug('logging request after initial call to res.end'); + // ! Note that this async function is NOT awaited!!! + void addToRequestLog({ + req, + res, + endpoint: context.runtime.endpoint.descriptor, + durationMs: Math.floor(perf.measure(randomUUID(), perfUUID).duration) + }); + } + }) as typeof res.end; +} diff --git a/lib/next-adhesive/package.json b/lib/next-adhesive/package.json new file mode 100644 index 0000000..d8c4037 --- /dev/null +++ b/lib/next-adhesive/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-adhesive" +} diff --git a/lib/next-adhesive/test/unit-add-raw-body.test.ts b/lib/next-adhesive/test/unit-add-raw-body.test.ts new file mode 100644 index 0000000..9bb6323 --- /dev/null +++ b/lib/next-adhesive/test/unit-add-raw-body.test.ts @@ -0,0 +1,305 @@ +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { noopHandler, withMockedOutput, wrapHandler } from 'testverse/setup'; + +import addRawBody, { + ensureNextApiRequestHasRawBody, + isNextApiRequestWithRawBody +} from 'multiverse/next-adhesive/add-raw-body'; + +import type { Options, WithRawBody } from 'multiverse/next-adhesive/add-raw-body'; +import type { NextApiRequest } from 'next'; + +describe('::', () => { + it('throws if bodyParser is not disabled', async () => { + expect.hasAssertions(); + + const pagesHandler = wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody] + }), + {} + ); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler, + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('body parser must be disabled') + }); + }); + + pagesHandler.config = { api: { bodyParser: false } }; + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + }); + + it('throws if rawBody property already defined on request object', async () => { + expect.hasAssertions(); + + const normalHandler = wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody] + }) + ); + + normalHandler.config = { api: { bodyParser: false } }; + + await testApiHandler({ + pagesHandler: normalHandler, + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + + const obsoleterHandler = wrapHandler(async (req, res) => { + (req as WithRawBody).rawBody = 'fake raw body'; + return withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody] + })(req, res); + }); + + obsoleterHandler.config = { api: { bodyParser: false } }; + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: obsoleterHandler, + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('already has a defined "rawBody" property') + }); + }); + }); + + it('throws on bad JSON body', async () => { + expect.hasAssertions(); + + const pagesHandler = wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody] + }) + ); + + pagesHandler.config = { api: { bodyParser: false } }; + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler, + test: async ({ fetch }) => + void (await fetch({ + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: '' + })) + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('invalid JSON body') + }); + }); + }); + + it('throws on invalid body (raw-body chokes)', async () => { + expect.hasAssertions(); + + const pagesHandler = wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody] + }) + ); + + pagesHandler.config = { api: { bodyParser: false } }; + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler, + requestPatcher(req) { + req.destroy(); + }, + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('invalid body') + }); + }); + }); + + it('adds rawBody to request object while still providing parsed body', async () => { + expect.hasAssertions(); + + const pagesHandler = wrapHandler( + withMiddleware( + (req, res) => { + if (ensureNextApiRequestHasRawBody(req)) { + res.status(200).send({ body: req.body, rawBody: req.rawBody }); + } + }, + { + descriptor: '/fake', + use: [addRawBody] + } + ) + ); + + pagesHandler.config = { api: { bodyParser: false } }; + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + let res, json, rawBody, jsonBody; + + // ? Works with empty body (which otherwise evaluates falsy) + res = await fetch(); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: '', rawBody: '' }); + + // ? Works with empty body as JSON + res = await fetch({ headers: { 'content-type': 'application/json' } }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: {}, rawBody: '' }); + + jsonBody = { a: 1, b: 'c', d: true }; + rawBody = JSON.stringify(jsonBody); + res = await fetch({ + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: rawBody + }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: jsonBody, rawBody }); + + jsonBody = { a: 2, b: 'z', d: false }; + rawBody = JSON.stringify(jsonBody); + res = await fetch({ + method: 'PUT', + headers: { 'content-type': 'application/ld+json' }, + body: rawBody + }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: jsonBody, rawBody }); + + jsonBody = { a: '3', b: 'd', e: 'true' }; + rawBody = 'a=3&b=d&e=true'; + res = await fetch({ + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: rawBody + }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: jsonBody, rawBody }); + + rawBody = 'hello, world!'; + res = await fetch({ + method: 'POST', + headers: { 'content-type': 'text/plain' }, + body: rawBody + }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: rawBody, rawBody }); + + // ? Works with really bad content types + rawBody = 'hello, world!'; + res = await fetch({ + method: 'POST', + headers: { 'content-type': '/' }, + body: rawBody + }); + json = await res.json(); + + expect(res.status).toBe(200); + expect(json).toStrictEqual({ body: rawBody, rawBody }); + } + }); + }); + + it('respects requestBodySizeLimit option', async () => { + expect.hasAssertions(); + + const pagesHandler = wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [addRawBody], + options: { requestBodySizeLimit: 1 } + }) + ); + + pagesHandler.config = { api: { bodyParser: false } }; + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + expect((await fetch({ method: 'POST', body: 'x' })).status).toBe(200); + expect((await fetch({ method: 'POST', body: 'xx' })).status).toBe(413); + } + }); + }); +}); + +describe('::isNextApiRequestWithRawBody', () => { + it('functions properly as type predicate', async () => { + expect.hasAssertions(); + + const req = { rawBody: '' } as WithRawBody; + + if (isNextApiRequestWithRawBody(req)) { + // ? This test will "fail" during type checking if there is an error here + expect(req.rawBody).toBe(''); + } else { + // @ts-expect-error: test will "fail" during type checking if no error + expect(req.rawBody).toBe(''); + } + }); +}); + +describe('::ensureNextApiRequestHasRawBody', () => { + it('functions properly as type guard', async () => { + expect.hasAssertions(); + + const req = { rawBody: '' } as WithRawBody; + + if (ensureNextApiRequestHasRawBody(req)) { + // ? This test will "fail" during type checking if there is an error here + expect(req.rawBody).toBe(''); + } + }); + + it('throws if NextApiRequest object does not have raw body', async () => { + expect.hasAssertions(); + + expect(() => ensureNextApiRequestHasRawBody({} as NextApiRequest)).toThrow( + 'encountered a NextApiRequest object without a rawBody property' + ); + + expect(() => + ensureNextApiRequestHasRawBody({ rawBody: '' } as WithRawBody) + ).not.toThrow(); + }); +}); diff --git a/lib/next-adhesive/test/unit-auth-request.test.ts b/lib/next-adhesive/test/unit-auth-request.test.ts new file mode 100644 index 0000000..9348cea --- /dev/null +++ b/lib/next-adhesive/test/unit-auth-request.test.ts @@ -0,0 +1,228 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import authRequest, { type Options } from 'multiverse/next-adhesive/auth-request'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { authenticateHeader, authorizeHeader } from 'multiverse/next-auth'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { noopHandler, wrapHandler } from 'testverse/setup'; + +jest.mock('multiverse/next-auth'); + +const mockAuthenticateHeader = asMockedFunction(authenticateHeader); +const mockAuthorizeHeader = asMockedFunction(authorizeHeader); + +beforeEach(() => { + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: false })); + mockAuthorizeHeader.mockReturnValue(Promise.resolve({ authorized: false })); +}); + +it('throws if missing requiresAuth option', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + useOnError: [ + (_, res, context) => { + expect(context.runtime.error).toMatchObject({ + message: expect.stringContaining( + 'a valid "requiresAuth" option is missing from middleware configuration' + ) + }); + res.send(200); + } + ] + }) + ), + test: async ({ fetch }) => void (await fetch()) + }); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + useOnError: [ + (_, res, context) => { + expect(context.runtime.error).toMatchObject({ + message: expect.stringContaining( + 'a valid "requiresAuth" option is missing from middleware configuration' + ) + }); + res.send(200); + } + ], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: { requiresAuth: 'isGlobalAdmin' as any } + }) + ), + test: async ({ fetch }) => void (await fetch()) + }); +}); + +it('passes allowedSchemes to authenticateHeader', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: true })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: { allowedSchemes: 'bearer' } } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ headers: { authorization: 'token' } })).status).toBe(200); + expect(mockAuthenticateHeader).toBeCalledWith({ + header: 'token', + allowedSchemes: 'bearer' + }); + } + }); +}); + +it('passes constraints to authorizeHeader', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: true })); + mockAuthorizeHeader.mockReturnValue(Promise.resolve({ authorized: true })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: { constraints: ['isGlobalAdmin'] } } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ headers: { authorization: 'token' } })).status).toBe(200); + expect(mockAuthorizeHeader).toBeCalledWith({ + header: 'token', + constraints: ['isGlobalAdmin'] + }); + } + }); +}); + +it('does not send 401 if requires auth and authenticateHeader returns ok', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: true })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: true } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ headers: { authorization: 'token' } })).status).toBe(200); + } + }); +}); + +it('sends 401 if requires auth and authenticateHeader returns not-ok or error', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: false })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: true } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(401) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: false } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + + mockAuthenticateHeader.mockReturnValue( + Promise.resolve({ authenticated: true, error: 'some error' }) + ); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: true } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(401) + }); +}); + +it('does not send 403 if requires auth and authorizeHeader returns ok', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: true })); + mockAuthorizeHeader.mockReturnValue(Promise.resolve({ authorized: true })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: { constraints: ['isGlobalAdmin'] } } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ headers: { authorization: 'token' } })).status).toBe(200); + expect(mockAuthenticateHeader).toBeCalledTimes(1); + expect(mockAuthorizeHeader).toBeCalledTimes(1); + } + }); +}); + +it('sends 403 if requires auth and authorizeHeader returns ok or error', async () => { + expect.hasAssertions(); + + mockAuthenticateHeader.mockReturnValue(Promise.resolve({ authenticated: true })); + mockAuthorizeHeader.mockReturnValue(Promise.resolve({ authorized: false })); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: { constraints: 'isGlobalAdmin' } } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(403) + }); + + mockAuthorizeHeader.mockReturnValue( + Promise.resolve({ authorized: true, error: 'an error' }) + ); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [authRequest], + options: { requiresAuth: { constraints: 'isGlobalAdmin' } } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(403) + }); +}); diff --git a/lib/next-adhesive/test/unit-check-content-type.test.ts b/lib/next-adhesive/test/unit-check-content-type.test.ts new file mode 100644 index 0000000..0ac4fb4 --- /dev/null +++ b/lib/next-adhesive/test/unit-check-content-type.test.ts @@ -0,0 +1,719 @@ +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import randomizeCase from 'random-case'; +import { noopHandler, withMockedOutput, wrapHandler } from 'testverse/setup'; + +import checkContentType, { + type Options +} from 'multiverse/next-adhesive/check-content-type'; + +it('sends 415 by default for POST, PUT, and PATCH requests with or without a Content-Type header', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType] + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'PUT', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'PATCH', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + } + }); +}); + +it('sends 200 by default for requests not using POST, PUT, or PATCH methods if they do not have a Content-Type header', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType] + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + } + }); +}); + +it('sends 415 by default for requests not using POST, PUT, or PATCH methods if they have a Content-Type header', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType] + }) + ), + test: async ({ fetch }) => { + expect( + (await fetch({ method: 'GET', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'HEAD', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'DELETE', headers: { 'content-type': 'a/j' } })).status + ).toBe(415); + + // expect( + // (await fetch({ method: 'CONNECT', headers: { 'content-type': 'a/j' } })).status + // ).toBe(415); + + expect( + (await fetch({ method: 'OPTIONS', headers: { 'content-type': 'a/j' } })) + .status + ).toBe(415); + + // expect( + // (await fetch({ method: 'TRACE', headers: { 'content-type': 'a/j' } })).status + // ).toBe(415); + } + }); +}); + +it('sends 200 for POST, PUT, and PATCH requests with allowed Content-Type headers and 415 otherwise', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['a1', 'a2'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a1' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'PUT', headers: { 'content-type': 'a2' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'PATCH', headers: { 'content-type': 'a1' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a3' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'PUT', headers: { 'content-type': 'a3' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'PATCH', headers: { 'content-type': 'a3' } })).status + ).toBe(415); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { + allowedContentTypes: { POST: ['a1'], PUT: ['a2', 'a3'] } + } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a1' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'PUT', headers: { 'content-type': 'a2' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'PATCH', headers: { 'content-type': 'a1' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a3' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'PUT', headers: { 'content-type': 'a3' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'PATCH', headers: { 'content-type': 'a3' } })).status + ).toBe(415); + } + }); +}); + +it(`ignores requests without a Content-Type header that aren't POST, PUT, or PATCH unless explicitly configured`, async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['a1', 'a2'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { + allowedContentTypes: { GET: ['a1'], POST: ['a1'], PUT: ['a2', 'a3'] } + } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(415); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + + expect( + (await fetch({ method: 'GET', headers: { 'content-type': 'a1' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'DELETE', headers: { 'content-type': 'a1' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a1' } })).status + ).toBe(200); + + expect( + (await fetch({ method: 'GET', headers: { 'content-type': 'a2' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'DELETE', headers: { 'content-type': 'a2' } })).status + ).toBe(415); + + expect( + (await fetch({ method: 'POST', headers: { 'content-type': 'a2' } })).status + ).toBe(415); + } + }); +}); + +it(`does not ignore requests that include a Content-Type header`, async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['a1', 'a2'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + + let headers = { 'content-type': 'a1' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + + headers = { 'content-type': 'bad' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + } + }); +}); + +it('respects explicit configuration for all request methods regardless of header presence', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { + allowedContentTypes: { + GET: ['a1'], + HEAD: ['a1'], + POST: ['a1'], + PUT: ['a1'], + DELETE: ['a1'], + CONNECT: ['a1'], + OPTIONS: ['a1'], + TRACE: ['a1'], + PATCH: ['a1'] + } + } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(415); + expect((await fetch({ method: 'HEAD' })).status).toBe(415); + expect((await fetch({ method: 'DELETE' })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(415); + // expect((await fetch({ method: 'TRACE' })).status).toBe(415); + expect((await fetch({ method: 'POST' })).status).toBe(415); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + + let headers = { 'content-type': 'a1' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(200); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(200); + + headers = { 'content-type': 'bad' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(415); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + } + }); +}); + +it('ignores Content-Type header case for all requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['application/json'] } + }) + ), + test: async ({ fetch }) => { + const headers = { + get 'content-type'() { + return randomizeCase('application/json'); + } + }; + + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(200); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(200); + } + }); +}); + +it('allows all (even missing) Content-Type header if set to "any"', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: 'any' } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(200); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(200); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(200); + expect((await fetch({ method: 'PATCH' })).status).toBe(200); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: { GET: 'any', POST: 'any' } } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + } + }); +}); + +it('requires all requests to be sent without a Content-Type header if set to "none"', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: 'none' } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(415); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(200); + expect((await fetch({ method: 'PATCH' })).status).toBe(200); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: { POST: 'none' } } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(415); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + } + }); +}); + +it('allows requests without a Content-Type header in addition to other constraints if array (as a mapped value or top-level) includes "none" value', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['none', 'application/json'] } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(200); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(200); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(200); + expect((await fetch({ method: 'PATCH' })).status).toBe(200); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: { POST: ['none', 'application/json'] } } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'application/json' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'HEAD' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT' })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS' })).status).toBe(200); + // expect((await fetch({ method: 'TRACE' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(415); + expect((await fetch({ method: 'PATCH' })).status).toBe(415); + } + }); +}); + +it('sends 415 if Content-Type is literally the string "none"', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: ['none', 'application/json'] } + }) + ), + test: async ({ fetch }) => { + const headers = { 'content-type': 'none' }; + expect((await fetch({ method: 'GET', headers })).status).toBe(415); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(415); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(415); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(415); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(415); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(415); + expect((await fetch({ method: 'POST', headers })).status).toBe(415); + expect((await fetch({ method: 'PUT', headers })).status).toBe(415); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(415); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: { POST: ['none', 'application/json'] } } + }) + ), + test: async ({ fetch }) => { + // ? Works even if strange case is used + const headers = { 'content-type': 'NoNe' }; + expect((await fetch({ method: 'POST', headers })).status).toBe(415); + } + }); +}); + +it('sends 400 is method is undefined', async () => { + expect.hasAssertions(); + + await testApiHandler({ + requestPatcher(req) { + req.method = undefined; + }, + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: [randomizeCase('application/json')] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(400); + } + }); +}); + +it('works even if allowedContentTypes mapped value is strange or undefined', async () => { + expect.hasAssertions(); + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: { GET: undefined } } + }) + ), + async test({ fetch }) { + expect((await fetch({ method: 'GET' })).status).toBe(200); + } + }); + + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: { allowedContentTypes: { GET: new BigInt64Array() as any } } + }) + ), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ + message: expect.stringContaining( + 'allowedContentTypes must adhere to type constraints' + ) + }); + }); +}); + +it('works even if allowedContentTypes not specified in lowercase', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkContentType], + options: { allowedContentTypes: [randomizeCase('application/json')] } + }) + ), + test: async ({ fetch }) => { + const headers = { + get 'content-type'() { + return randomizeCase('application/json'); + } + }; + + expect((await fetch({ method: 'GET', headers })).status).toBe(200); + expect((await fetch({ method: 'HEAD', headers })).status).toBe(200); + expect((await fetch({ method: 'DELETE', headers })).status).toBe(200); + // expect((await fetch({ method: 'CONNECT', headers })).status).toBe(200); + expect((await fetch({ method: 'OPTIONS', headers })).status).toBe(200); + // expect((await fetch({ method: 'TRACE', headers })).status).toBe(200); + expect((await fetch({ method: 'POST', headers })).status).toBe(200); + expect((await fetch({ method: 'PUT', headers })).status).toBe(200); + expect((await fetch({ method: 'PATCH', headers })).status).toBe(200); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-check-method.test.ts b/lib/next-adhesive/test/unit-check-method.test.ts new file mode 100644 index 0000000..f7d4971 --- /dev/null +++ b/lib/next-adhesive/test/unit-check-method.test.ts @@ -0,0 +1,173 @@ +import checkMethod, { type Options } from 'multiverse/next-adhesive/check-method'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { mockEnvFactory, noopHandler, wrapHandler } from 'testverse/setup'; + +import type { ValidHttpMethod } from '@xunnamius/types'; + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); + +it('sends 200 for allowed methods', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { allowedMethods: ['GET', 'DELETE', 'POST', 'PUT'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + } + }); +}); + +it('is restrictive by default', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod] + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(405); + expect((await fetch({ method: 'POST' })).status).toBe(405); + expect((await fetch({ method: 'PUT' })).status).toBe(405); + expect((await fetch({ method: 'DELETE' })).status).toBe(405); + } + }); +}); + +it('sends 405 when request.method is undefined', async () => { + expect.hasAssertions(); + + await testApiHandler({ + requestPatcher: (req) => (req.method = undefined), + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod] + }) + ), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(405); + } + }); +}); + +it('sends 405 when encountering unlisted methods', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { allowedMethods: ['POST', 'PUT'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(405); + expect((await fetch({ method: 'POST' })).status).toBe(200); + expect((await fetch({ method: 'PUT' })).status).toBe(200); + expect((await fetch({ method: 'DELETE' })).status).toBe(405); + } + }); +}); + +it('sends 405 when encountering globally disallowed methods', async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(200); + expect((await fetch({ method: 'POST' })).status).toBe(405); + expect((await fetch({ method: 'PUT' })).status).toBe(405); + expect((await fetch({ method: 'DELETE' })).status).toBe(405); + } + }); + }, + { DISALLOWED_METHODS: 'POST,PUT,DELETE' } + ); +}); + +it('ignores spacing when parsing DISALLOWED_METHODS', async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'] } + }) + ), + test: async ({ fetch }) => { + expect((await fetch({ method: 'GET' })).status).toBe(405); + expect((await fetch({ method: 'POST' })).status).toBe(405); + expect((await fetch({ method: 'PUT' })).status).toBe(405); + expect((await fetch({ method: 'DELETE' })).status).toBe(200); + } + }); + }, + { DISALLOWED_METHODS: ' POST , PUT, GET ' } + ); +}); + +it('sends an Allow header in 405 responses', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { allowedMethods: ['GET', 'POST', 'HEAD'] } + }) + ), + test: async ({ fetch }) => { + const res = await fetch({ method: 'PUT' }); + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toBe('GET,POST,HEAD'); + } + }); +}); + +it('works even if allowedMethods specified in lowercase', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkMethod], + options: { + allowedMethods: ['get'] as unknown as ValidHttpMethod[] + } + }) + ), + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-check-version.test.ts b/lib/next-adhesive/test/unit-check-version.test.ts new file mode 100644 index 0000000..dd458ca --- /dev/null +++ b/lib/next-adhesive/test/unit-check-version.test.ts @@ -0,0 +1,174 @@ +import checkVersion, { type Options } from 'multiverse/next-adhesive/check-version'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { mockEnvFactory, noopHandler, wrapHandler } from 'testverse/setup'; + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); + +it('is a noop by default', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion] + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: 'one' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); +}); + +it('sends 404 if its corresponding version is disabled', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '1' } + }) + ), + test: async ({ fetch }) => { + await withMockedEnv( + async () => { + expect((await fetch()).status).toBe(404); + }, + { DISABLED_API_VERSIONS: '1' } + ); + + await withMockedEnv( + async () => { + expect((await fetch()).status).toBe(200); + }, + { DISABLED_API_VERSIONS: '2' } + ); + + await withMockedEnv( + async () => { + expect((await fetch()).status).toBe(404); + }, + { DISABLED_API_VERSIONS: '2,1' } + ); + + await withMockedEnv( + async () => { + expect((await fetch()).status).toBe(200); + }, + { DISABLED_API_VERSIONS: '3,2' } + ); + } + }); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '1' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '2' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(404) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: 'three' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(404) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '4' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(404) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(async () => undefined, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '4' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(404) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion] + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + }, + { DISABLED_API_VERSIONS: 'three,4,2,five' } + ); +}); + +it('is a noop if DISABLED_API_VERSIONS is an empty string', async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion], + options: { apiVersion: '4' } + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [checkVersion] + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(200) + }); + }, + { DISABLED_API_VERSIONS: '' } + ); +}); diff --git a/lib/next-adhesive/test/unit-contrive-error.test.ts b/lib/next-adhesive/test/unit-contrive-error.test.ts new file mode 100644 index 0000000..9eeee06 --- /dev/null +++ b/lib/next-adhesive/test/unit-contrive-error.test.ts @@ -0,0 +1,53 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import contriveError, { type Options } from 'multiverse/next-adhesive/contrive-error'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { isDueForContrivedError } from 'multiverse/next-contrived'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { noopHandler, wrapHandler } from 'testverse/setup'; + +jest.mock('multiverse/next-contrived'); + +const mockIsDueForContrivedError = asMockedFunction(isDueForContrivedError); + +beforeEach(() => { + mockIsDueForContrivedError.mockReturnValue(Promise.resolve(false)); +}); + +it('does not inject contrived errors by default', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [contriveError] + }) + ), + test: async ({ fetch }) => { + mockIsDueForContrivedError.mockReturnValue(Promise.resolve(true)); + await expect(fetch().then((r) => r.status)).resolves.toBe(200); + } + }); +}); + +it('injects contrived errors when due if enabled', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [contriveError], + options: { enableContrivedErrors: true } + }) + ), + test: async ({ fetch }) => { + mockIsDueForContrivedError.mockReturnValue(Promise.resolve(false)); + await expect(fetch().then((r) => r.status)).resolves.toBe(200); + mockIsDueForContrivedError.mockReturnValue(Promise.resolve(true)); + await expect(fetch().then((r) => r.status)).resolves.toBe(555); + mockIsDueForContrivedError.mockReturnValue(Promise.resolve(false)); + await expect(fetch().then((r) => r.status)).resolves.toBe(200); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-handle-error.test.ts b/lib/next-adhesive/test/unit-handle-error.test.ts new file mode 100644 index 0000000..010756a --- /dev/null +++ b/lib/next-adhesive/test/unit-handle-error.test.ts @@ -0,0 +1,183 @@ +import handleError, { type Options } from 'multiverse/next-adhesive/handle-error'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { + itemFactory, + noopHandler, + withMockedOutput, + wrapHandler +} from 'testverse/setup'; +import { toss } from 'toss-expression'; + +import { + AppError, + AppValidationError, + AuthError, + ClientValidationError, + DummyError, + GuruMeditationError, + HttpError, + InvalidAppConfigurationError, + InvalidAppEnvironmentError, + InvalidClientConfigurationError, + InvalidItemError, + InvalidSecretError, + ItemNotFoundError, + ItemsNotFoundError, + NotAuthenticatedError, + NotAuthorizedError, + NotFoundError, + NotImplementedError, + TrialError, + ValidationError +} from 'universe/error'; + +it('sends correct HTTP error codes when certain errors occur', async () => { + expect.hasAssertions(); + + const factory = itemFactory<[AppError | string, number]>([ + [new ValidationError(), 400], + [new ValidationError(''), 400], // ! Edge case for code coverage + [new AppValidationError(), 500], + [new InvalidAppConfigurationError(), 500], + [new InvalidAppEnvironmentError(), 500], + [new ClientValidationError(), 400], + [new InvalidClientConfigurationError(), 400], + [new InvalidItemError(), 400], + [new InvalidSecretError(), 400], + [new AuthError(), 403], + [new NotAuthenticatedError(), 403], + [new NotAuthorizedError(), 403], + [new NotFoundError(), 404], + [new ItemNotFoundError(), 404], + [new ItemsNotFoundError(), 404], + [new HttpError(), 500], + [new TrialError(), 500], + [new DummyError(), 500], + [new AppError(), 500], + [new GuruMeditationError(), 500], + [new NotImplementedError(), 501], + [new Error('bad'), 500], // ? Every other error type should return 500 + ['strange error', 500] // ? This too + ]); + + await withMockedOutput(async () => { + await Promise.all( + factory.items.map(async (item) => { + const [expectedError, expectedStatus] = item; + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(async () => toss(expectedError), { + descriptor: '/fake', + use: [], + useOnError: [handleError] + }) + ), + test: async ({ fetch }) => + fetch().then((res) => expect(res.status).toStrictEqual(expectedStatus)) + }); + }) + ); + }); +}); + +it('throws without calling res.end if response is no longer writable', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: async (rq, rs) => { + await expect( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [ + (_req, res) => { + // eslint-disable-next-line jest/unbound-method + const send = res.end; + res.end = ((...args: Parameters) => { + send(...args); + throw new Error('bad bad not good'); + }) as unknown as typeof res.end; + } + ], + useOnError: [handleError] + })(rq, rs) + ).rejects.toMatchObject({ message: 'bad bad not good' }); + }, + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + } + }); +}); + +it('supports pluggable error handlers', async () => { + expect.hasAssertions(); + + const MyError = class extends DummyError {}; + const MyUnusedError = class extends Error {}; + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + () => { + throw new MyError('bad bad not good'); + } + ], + useOnError: [handleError], + options: { + errorHandlers: new Map([ + [ + MyUnusedError, + (res) => { + res.status(555).end(); + } + ], + [ + MyError, + (res, errorJson) => { + res.status(200).send(errorJson); + } + ] + ]) + } + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + await expect((await fetch()).json()).resolves.toStrictEqual({ + error: 'bad bad not good' + }); + } + }); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + () => { + throw new MyError('bad good not good'); + } + ], + useOnError: [handleError], + options: { + errorHandlers: new Map([ + [ + // ? Should catch every error + Error, + (res, errorJson) => { + res.status(201).send(errorJson); + } + ] + ]) + } + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(201); + await expect((await fetch()).json()).resolves.toStrictEqual({ + error: 'bad good not good' + }); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-limit-request.test.ts b/lib/next-adhesive/test/unit-limit-request.test.ts new file mode 100644 index 0000000..a3b0232 --- /dev/null +++ b/lib/next-adhesive/test/unit-limit-request.test.ts @@ -0,0 +1,158 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import limitRequest from 'multiverse/next-adhesive/limit-request'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { clientIsRateLimited } from 'multiverse/next-limit'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { mockEnvFactory, noopHandler, wrapHandler } from 'testverse/setup'; + +jest.mock('multiverse/next-limit'); + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); +const mockClientIsRateLimited = asMockedFunction(clientIsRateLimited); + +beforeEach(() => { + mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: false, retryAfter: 0 }) + ); +}); + +it('rate limits requests according to backend determination', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [limitRequest] + }) + ), + test: async ({ fetch }) => { + await withMockedEnv( + async () => { + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: false, retryAfter: 0 }) + ); + + await expect( + fetch().then(async (r) => [r.status, await r.json()]) + ).resolves.toStrictEqual([200, {}]); + + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: true, retryAfter: 100 }) + ); + + await expect( + fetch().then(async (r) => [r.status, await r.json()]) + ).resolves.toStrictEqual([ + 429, + expect.objectContaining({ + retryAfter: 100 + }) + ]); + }, + { IGNORE_RATE_LIMITS: 'false' } + ); + } + }); +}); + +it('does not rate limit requests when ignoring rate limits', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [limitRequest] + }) + ), + test: async ({ fetch }) => { + await withMockedEnv( + async () => { + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: false, retryAfter: 0 }) + ); + + await expect( + fetch().then(async (r) => [r.status, await r.json()]) + ).resolves.toStrictEqual([200, {}]); + + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: true, retryAfter: 100 }) + ); + + await expect( + fetch().then(async (r) => [r.status, await r.json()]) + ).resolves.toStrictEqual([200, {}]); + }, + { IGNORE_RATE_LIMITS: 'true' } + ); + } + }); +}); + +it('treats otherwise valid requests as unauthenticatable only when locking out all clients', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [limitRequest] + }) + ), + test: async ({ fetch }) => { + await withMockedEnv( + async () => { + const res = await fetch(); + expect(res.status).toBe(403); + }, + { + LOCKOUT_ALL_CLIENTS: 'true' + } + ); + + await withMockedEnv(async () => expect((await fetch()).status).toBe(200), { + LOCKOUT_ALL_CLIENTS: 'false' + }); + } + }); +}); + +it('includes retry-after value in header (s) and in response JSON (ms)', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [limitRequest] + }) + ), + test: async ({ fetch }) => { + await withMockedEnv( + async () => { + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: false, retryAfter: 0 }) + ); + + await expect( + fetch().then(async (r) => [r.headers.get('retry-after'), await r.json()]) + ).resolves.toStrictEqual([null, {}]); + + void mockClientIsRateLimited.mockReturnValue( + Promise.resolve({ isLimited: true, retryAfter: 12_344 }) + ); + + await expect( + fetch().then(async (r) => [r.headers.get('retry-after'), await r.json()]) + ).resolves.toStrictEqual([ + '13', + expect.objectContaining({ retryAfter: 12_344 }) + ]); + }, + { IGNORE_RATE_LIMITS: 'false' } + ); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-log-request.test.ts b/lib/next-adhesive/test/unit-log-request.test.ts new file mode 100644 index 0000000..780039e --- /dev/null +++ b/lib/next-adhesive/test/unit-log-request.test.ts @@ -0,0 +1,104 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import logRequest from 'multiverse/next-adhesive/log-request'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { addToRequestLog } from 'multiverse/next-log'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { noopHandler, wrapHandler } from 'testverse/setup'; +import { toss } from 'toss-expression'; + +jest.mock('multiverse/next-log'); + +const mockAddToRequestLog = asMockedFunction(addToRequestLog); + +beforeEach(() => { + mockAddToRequestLog.mockReturnValue(Promise.resolve()); +}); + +it('logs requests on call to res.send', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + wrapHandler( + withMiddleware(async (_req, res) => res.status(404).send({}), { + descriptor: '/fake', + use: [logRequest] + }) + ) + ), + test: async ({ fetch }) => { + await Promise.all([fetch(), fetch(), fetch()]); + expect(mockAddToRequestLog).toBeCalledTimes(3); + } + }); +}); + +it('logs requests on call to res.end', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + wrapHandler( + withMiddleware(async (_req, res) => void res.status(404).end(), { + descriptor: '/fake', + use: [logRequest] + }) + ) + ), + test: async ({ fetch }) => { + await Promise.all([fetch(), fetch(), fetch()]); + expect(mockAddToRequestLog).toBeCalledTimes(3); + } + }); +}); + +it('logs requests once on multiple calls to res.end', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + wrapHandler( + withMiddleware( + async (_req, res) => { + res.status(404).end(); + res.end(); + }, + { + descriptor: '/fake', + use: [logRequest] + } + ) + ) + ), + test: async ({ fetch }) => { + await Promise.all([fetch(), fetch(), fetch()]); + expect(mockAddToRequestLog).toBeCalledTimes(3); + } + }); +}); + +it('handles request log errors after res.end as gracefully as possible', async () => { + expect.hasAssertions(); + + mockAddToRequestLog.mockImplementation(() => toss(new Error('fake error'))); + let called = false; + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [logRequest], + useOnError: [ + (_req, _res, context) => { + expect(context.runtime.error).toMatchObject({ message: 'fake error' }); + called = true; + } + ] + }) + ), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + expect(called).toBeTrue(); + } + }); +}); diff --git a/lib/next-adhesive/test/unit-use-cors.test.ts b/lib/next-adhesive/test/unit-use-cors.test.ts new file mode 100644 index 0000000..4c825f7 --- /dev/null +++ b/lib/next-adhesive/test/unit-use-cors.test.ts @@ -0,0 +1,79 @@ +import useCors, { type Options } from 'multiverse/next-adhesive/use-cors'; +import { withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { isolatedImport, noopHandler, wrapHandler } from 'testverse/setup'; + +afterEach(() => { + jest.dontMock('cors'); +}); + +it('works', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [] + }) + ), + test: async ({ fetch }) => { + const res = await fetch({ method: 'OPTIONS' }); + expect(res.status).toBe(200); + expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull(); + expect(res.headers.get('Access-Control-Allow-Methods')).toBeNull(); + } + }); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [useCors], + options: { allowedMethods: ['GET', 'POST', 'HEAD'] } + }) + ), + test: async ({ fetch }) => { + let res = await fetch({ method: 'OPTIONS' }); + expect(res.status).toBe(204); + expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); + expect(res.headers.get('Access-Control-Allow-Methods')).toBe('GET,POST,HEAD'); + + res = await fetch({ method: 'GET' }); + expect(res.status).toBe(200); + } + }); +}); + +it('handles cors package errors gracefully', async () => { + expect.hasAssertions(); + + jest.doMock( + 'cors', + (): typeof import('cors') => + () => + (_req: unknown, _res: unknown, callback: (error: Error) => void) => { + return callback(new Error('fake error')); + } + ); + + await testApiHandler({ + pagesHandler: wrapHandler( + withMiddleware(noopHandler, { + descriptor: '/fake', + use: [ + isolatedImport({ + path: 'multiverse/next-adhesive/use-cors' + }) + ], + useOnError: [ + (_req, res, context) => { + expect(context.runtime.error).toMatchObject({ message: 'fake error' }); + res.status(555).end(); + } + ] + }) + ), + test: async ({ fetch }) => expect((await fetch()).status).toBe(555) + }); +}); diff --git a/lib/next-adhesive/use-cors.ts b/lib/next-adhesive/use-cors.ts new file mode 100644 index 0000000..0fea1fc --- /dev/null +++ b/lib/next-adhesive/use-cors.ts @@ -0,0 +1,36 @@ +import Cors from 'cors'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { Options as CheckMethodOptions } from 'multiverse/next-adhesive/check-method'; +import type { MiddlewareContext } from 'multiverse/next-api-glue'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +const debug = debugFactory('next-adhesive:use-cors'); + +export type Options = { + allowedMethods?: CheckMethodOptions['allowedMethods']; +}; + +/** + * Allows _cross-origin_ requests for the most popular request types. **Note + * that this can be dangerous (huge security hole) and should only be used for + * public APIs**. + * + * When present, this should be among the very first middleware in the chain and + * certainly before _check-method_. + * + * By default, allowed CORS methods are: `GET`, `HEAD`, `PUT`, `PATCH`, `POST`, + * and `DELETE`. + */ +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) { + debug('entered middleware runtime'); + + const cors = Cors({ methods: context.options.allowedMethods }); + await new Promise((resolve, reject) => + cors(req, res, (error) => (error ? reject(error) : resolve(undefined))) + ); +} diff --git a/lib/next-api-glue/index.ts b/lib/next-api-glue/index.ts new file mode 100644 index 0000000..b0e2f81 --- /dev/null +++ b/lib/next-api-glue/index.ts @@ -0,0 +1,385 @@ +import { debugFactory } from 'multiverse/debug-extended'; +import { sendNotImplemented } from 'multiverse/next-api-respond'; +import { toss } from 'toss-expression'; + +import type { NoInfer } from '@xunnamius/types'; +import type { Debugger } from 'multiverse/debug-extended'; +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; + +const debug = debugFactory('next-api-glue:runtime'); + +/** + * The shape of a custom middleware function. + */ +export type Middleware< + Options extends Record = Record +> = ( + req: NextApiRequest, + res: NextApiResponse, + context: MiddlewareContext +) => unknown; + +/** + * The shape of a middleware context object, potentially customized with + * additional middleware-specific options. + * + * Note that type checking cannot enforce that certain options are passed in the + * case that an options argument is omitted when calling `withMiddleware`. So, + * to be safe, all custom middleware context options should be declared as + * optional (i.e. `{ myOpt?: aType }` instead of `{ myOpt: aType })`. + * + * Middleware should default to the most restrictive configuration possible if + * its respective options are missing. + */ +export type MiddlewareContext< + Options extends Record = Record +> = { + /** + * Contains middleware use chain control functions and various metadata. + */ + runtime: { + /** + * Metadata describing the current endpoint. + */ + endpoint: { + /** + * A parameterized path string in the form of a URI path corresponding to + * the current endpoint. For example: `/my-endpoint/:some_id`. + */ + descriptor?: string; + }; + /** + * Call the next middleware function in the use chain. If not called + * explicitly before a middleware function resolves, and `done()` was also + * not called, `next()` will be called automatically. This means calling + * `next()` in a middleware function is entirely optional. + */ + readonly next: () => Promise; + /** + * Stop calling middleware functions, effectively aborting execution of the + * use chain. If `response.end` hasn't been called before calling this + * function, it will be called automatically. On abort, the handler will + * also be skipped. + */ + readonly done: () => void; + /** + * For middleware run via `useOnError`, the `error` property will contain + * the thrown error object. + */ + readonly error: unknown; + }; + /** + * Options expected by middleware functions at runtime. + */ + options: Options & { + /** + * If `true`, `context.runtime.done` is called whenever `response.end` is + * called before the middleware chain completes execution. If `false`, the + * entire primary middleware chain will always run to completion, even if + * the response has already been sent before it completes. + * + * @default true + */ + callDoneOnEnd: boolean; + }; +}; + +/** + * Generic middleware runner. Decorates a request handler. + * + * Passing `undefined` as `handler` or not calling `res.end()` (and not sending + * headers) in your handler or use chain will trigger an `HTTP 501 Not + * Implemented` response. This can be used to to stub out endpoints and their + * middleware for later implementation. + */ +export function withMiddleware< + Options extends Record = Record +>( + pagesHandler: NextApiHandler | undefined, + { + descriptor, + use, + useOnError, + options + }: { + descriptor: MiddlewareContext['runtime']['endpoint']['descriptor']; + use: Middleware>[]; + useOnError?: Middleware>[]; + options?: Partial>['options']> & + NoInfer; + } +) { + if (!Array.isArray(use)) { + throw new TypeError('withMiddleware `use` parameter must be an array'); + } + + if (useOnError && !Array.isArray(useOnError)) { + throw new Error('withMiddleware `useOnError` parameter must be an array'); + } + + return async (req: NextApiRequest, res: NextApiResponse) => { + /* istanbul ignore next */ + const middlewareContext: MiddlewareContext> = { + runtime: { + endpoint: { + descriptor + }, + next: () => toss(new Error('runtime.next was called unexpectedly')), + done: () => toss(new Error('runtime.done was called unexpectedly')), + error: undefined + }, + options: { callDoneOnEnd: true, ...options } as MiddlewareContext< + NoInfer + >['options'] + }; + + /** + * Async middleware chain iteration. Returns `true` if execution was aborted + * or `false` otherwise. + */ + const startPullingChain = async ( + chain: IterableIterator>>, + localDebug: Debugger + ) => { + let executionWasAborted = false; + let executionCompleted = false; + let ranAtLeastOneMiddleware = false; + + try { + if (middlewareContext.options.callDoneOnEnd) { + localDebug( + 'chain will automatically call runtime.done after first call to res.end' + ); + + const send = res.end; + res.end = ((...args: Parameters) => { + const sent = res.writableEnded || res.headersSent; + send(...args); + + if (!sent) { + if (!executionWasAborted && !executionCompleted) { + localDebug('calling runtime.done after first call to res.end'); + middlewareContext.runtime.done(); + } else { + localDebug( + 'NOTICE: skipped calling runtime.done since chain already finished executing' + ); + } + } + }) as typeof res.end; + } else { + localDebug('chain will NOT automatically call runtime.done'); + } + + const pullChain = async () => { + let chainWasPulled = false; + const { value: currentMiddleware, done } = chain.next(); + + // @ts-expect-error: next is readonly to everyone but us + middlewareContext.runtime.next = async () => { + if (!executionCompleted) { + if (executionWasAborted) { + debug.warn( + 'runtime.next: chain was aborted; calling runtime.next() at this point is a noop' + ); + } else { + chainWasPulled = true; + localDebug( + 'runtime.next: manually selecting next middleware in chain' + ); + await pullChain(); + } + } else { + debug.warn( + 'runtime.next: chain already finished executing; calling runtime.next() at this point is a noop' + ); + } + }; + + // @ts-expect-error: done is readonly to everyone but us + middlewareContext.runtime.done = () => { + if (!executionCompleted) { + if (!executionWasAborted) { + localDebug('runtime.done: aborting middleware execution chain'); + executionWasAborted = true; + } else { + debug.warn( + 'runtime.done: chain already aborted; calling runtime.done() at this point is a noop' + ); + } + } else { + debug.warn( + 'runtime.done: chain already finished executing; calling runtime.done() at this point is a noop' + ); + } + }; + + if (!done) { + if (typeof currentMiddleware === 'function') { + localDebug('executing middleware'); + await currentMiddleware(req, res, middlewareContext); + ranAtLeastOneMiddleware = true; + } else { + debug.warn('skipping execution of non-function item in chain'); + } + + if (executionWasAborted) { + localDebug('execution chain aborted manually'); + } else if (!chainWasPulled) { + localDebug('selecting next middleware in chain'); + await pullChain(); + } + } else { + localDebug('no more middleware to execute'); + !executionCompleted && + localDebug('deactivated runtime control functions'); + executionCompleted = true; + } + }; + + await pullChain(); + localDebug('stopped middleware execution chain'); + localDebug( + `at least one middleware executed: ${ + ranAtLeastOneMiddleware ? 'yes' : 'no' + }` + ); + + return executionWasAborted; + } catch (error) { + executionWasAborted = true; + debug.warn('execution chain aborted due to error'); + throw error; + } + }; + + debug('-- begin --'); + + try { + let primaryChainWasAborted = false; + + try { + debug('selecting first middleware in primary middleware chain'); + primaryChainWasAborted = await startPullingChain( + use[Symbol.iterator](), + debug + ); + } catch (error) { + debug('error in primary middleware chain'); + throw error; + } + + if (typeof pagesHandler === 'function') { + if (primaryChainWasAborted) { + debug('not executing handler since primary chain execution was aborted'); + } else { + debug('executing handler'); + await pagesHandler(req, res); + debug('finished executing handler'); + } + } else { + debug('no handler function available'); + } + + if (!res.writableEnded && !res.headersSent) { + debug('response was not sent: sending "not implemented" error'); + sendNotImplemented(res); + } + + debug('-- done --'); + } catch (error) { + try { + debug.error('attempting to handle error: %O', error); + + // @ts-expect-error: error is readonly to everyone but us + middlewareContext.runtime.error = error; + + if (useOnError) { + try { + debug.error( + 'selecting first middleware in error handling middleware chain' + ); + await startPullingChain(useOnError[Symbol.iterator](), debug.error); + } catch (subError) { + // ? Error in error handler was unhandled + debug.error('error in error handling middleware chain: %O', subError); + debug.error('throwing unhandled error'); + throw subError; + } + } else { + debug.error('no error handling middleware found'); + debug.error('throwing unhandled error'); + throw error; + } + + // ? Error was unhandled, kick it up to the caller (usually Next itself) + if (!res.writableEnded && !res.headersSent) { + debug.error('throwing unhandled error'); + throw error; + } + } finally { + debug('-- done (with errors) --'); + } + } + }; +} + +/** + * Returns a `withMiddleware` function decorated with a preset configuration. + * `withMiddleware` optionally accepts its usual parameters, which will be + * appended onto the arguments to `withMiddlewareFactory` (the "preset + * parameters"); however, note that passed option keys will overwrite their + * preset counterparts. + * + * Useful when you don't want to repeatedly import, configure, and list a bunch + * of middleware every time you want to call `withMiddleware`. + */ +export function middlewareFactory< + Options extends Record = Record +>({ + use: defaultUse, + useOnError: defaultUseOnError, + options: defaultOptions +}: { + use: Middleware>[]; + useOnError?: Middleware>[]; + options?: Partial>['options']> & + NoInfer; +}) { + return = Record>( + pagesHandler: NextApiHandler | undefined, + params: { + descriptor: MiddlewareContext['runtime']['endpoint']['descriptor']; + prependUse?: Middleware>[]; + appendUse?: Middleware>[]; + prependUseOnError?: Middleware>[]; + appendUseOnError?: Middleware>[]; + options?: Partial>['options']> & + NoInfer; + } + ) => { + const { + descriptor, + prependUse, + appendUse, + prependUseOnError, + appendUseOnError, + options: passedOptions + } = { ...params }; + + return withMiddleware & NoInfer>(pagesHandler, { + descriptor, + use: [...(prependUse || []), ...defaultUse, ...(appendUse || [])], + useOnError: [ + ...(prependUseOnError || []), + ...(defaultUseOnError || []), + ...(appendUseOnError || []) + ], + options: { ...defaultOptions, ...passedOptions } as Partial< + MiddlewareContext & NoInfer>['options'] + > & + NoInfer & + NoInfer + }); + }; +} diff --git a/lib/next-api-glue/package.json b/lib/next-api-glue/package.json new file mode 100644 index 0000000..6b12909 --- /dev/null +++ b/lib/next-api-glue/package.json @@ -0,0 +1,3 @@ +{ + "name": "next-api-glue" +} diff --git a/lib/next-api-glue/unit.test.ts b/lib/next-api-glue/unit.test.ts new file mode 100644 index 0000000..94a90cb --- /dev/null +++ b/lib/next-api-glue/unit.test.ts @@ -0,0 +1,1155 @@ +import { middlewareFactory, withMiddleware } from 'multiverse/next-api-glue'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { mockOutputFactory, withDebugEnabled } from 'testverse/setup'; +import { toss } from 'toss-expression'; +import { DummyError } from 'universe/error'; + +import type { Middleware, MiddlewareContext } from 'multiverse/next-api-glue'; +import type { NextApiRequest, NextApiResponse, NextConfig } from 'next'; + +const MAX_CONTENT_LENGTH_BYTES = 100_000; +const MAX_CONTENT_LENGTH_BYTES_PLUS_1 = 100_001; + +const withMockedOutput = mockOutputFactory({ passthrough: { stdErrSpy: false } }); + +const noopHandler = async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).send({}); +}; + +describe('::withMiddleware', () => { + it('throws on bad parameters', async () => { + expect.hasAssertions(); + + expect(() => + withMiddleware(async () => undefined, { + // @ts-expect-error: testing bad param + use: true + }) + ).toThrow(/`use` parameter must be an array/); + + expect(() => + withMiddleware(async () => undefined, { + descriptor: '/fake', + use: [], + // @ts-expect-error: testing bad param + useOnError: true + }) + ).toThrow(/`useOnError` parameter must be an array/); + }); + + it('rejects requests that are too big when exporting config (next.js)', async () => { + expect.hasAssertions(); + + const pagesHandler = withMiddleware(noopHandler, { + descriptor: '/fake', + use: [] + }) as ReturnType & { config: NextConfig }; + + pagesHandler.config = { + api: { + bodyParser: { + get sizeLimit() { + return MAX_CONTENT_LENGTH_BYTES; + } + } + } + }; + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + method: 'POST', + body: 'x'.repeat(MAX_CONTENT_LENGTH_BYTES_PLUS_1) + }).then((r) => r.status) + ).resolves.toBe(413); + } + }); + }); + + it('lowercases headers automatically', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware( + async (req, res) => { + res.status(req.headers.key === '1234' ? 200 : 555).send({}); + }, + { descriptor: '/fake', use: [] } + ), + test: async ({ fetch }) => + expect((await fetch({ headers: { KEY: '1234' } })).status).toBe(200) + }); + }); + + it('parses url parameters', async () => { + expect.hasAssertions(); + + await testApiHandler({ + requestPatcher: (req) => { + req.url = '/?some=url&yes'; + }, + rejectOnHandlerError: true, + pagesHandler: withMiddleware( + async (req, res) => { + expect(req.query).toStrictEqual({ some: 'url', yes: '' }); + res.status(200).send({}); + }, + { descriptor: '/fake', use: [] } + ), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + } + }); + }); + + it('runs one middleware in primary chain', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [middleware] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + expect(middleware).toBeCalledTimes(1); + } + }); + }); + + it('runs multiple middleware in primary chain', async () => { + expect.hasAssertions(); + + const middleware = [jest.fn(), jest.fn()]; + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: middleware + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + middleware.forEach((m) => expect(m).toBeCalledTimes(1)); + } + }); + }); + + it('runs primary chain middleware then handler', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(() => + expect(handler).toBeCalledTimes(0) + ) as Middleware; + const handler = jest.fn(() => expect(middleware).toBeCalledTimes(1)); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(handler, { + descriptor: '/fake', + use: [middleware] + }), + test: async ({ fetch }) => { + await fetch(); + expect(middleware).toBeCalledTimes(1); + expect(handler).toBeCalledTimes(1); + } + }); + }); + + it('runs handler even if no middleware used', async () => { + expect.hasAssertions(); + + const handler = jest.fn(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(handler, { descriptor: '/fake', use: [] }), + test: async ({ fetch }) => { + await fetch(); + expect(handler).toBeCalledTimes(1); + } + }); + }); + + it('skips running handler if not a function', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + // @ts-expect-error: bad handler + pagesHandler: withMiddleware(true, { + descriptor: '/fake', + use: [(_, res) => res.status(200).end()] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + } + }); + }); + + it('populates runtime.endpoint with endpoint metadata if available', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake/:path', + use: [ + (_, res, context) => + res.status(200).send({ endpoint: context.runtime.endpoint }) + ] + }), + test: async ({ fetch }) => { + await expect((await fetch()).json()).resolves.toStrictEqual({ + endpoint: { + descriptor: '/fake/:path' + } + }); + } + }); + }); + + it('skips running handler if primary chain was aborted', async () => { + expect.hasAssertions(); + + const handler = jest.fn(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(handler, { + descriptor: '/fake', + use: [(_, __, context) => context.runtime.done()] + }), + test: async ({ fetch }) => { + await fetch(); + expect(handler).toBeCalledTimes(0); + } + }); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(handler, { + descriptor: '/fake', + use: [() => toss(new Error('bad'))] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ message: 'bad' }); + + expect(handler).toBeCalledTimes(0); + }); + }); + + it('sends 501 if handler is undefined', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { descriptor: '/fake', use: [] }), + test: async ({ fetch }) => expect((await fetch()).status).toBe(501) + }); + }); + + it('sends 501 if res.end not called by the time handler completes', async () => { + expect.hasAssertions(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(async () => undefined, { + descriptor: '/fake', + use: [] + }), + test: async ({ fetch }) => expect((await fetch()).status).toBe(501) + }); + }); + + it('only populates runtime.error for error handling middleware (and not primary)', async () => { + expect.hasAssertions(); + + const error = new Error('bad stuff happened'); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [ + (_, __, context) => expect(context.runtime.error).toBeUndefined(), + (_, __, context) => expect(context.runtime.error).toBeUndefined(), + () => toss(error) + ], + useOnError: [ + (_, __, context) => expect(context.runtime.error).toBe(error), + (_, __, context) => expect(context.runtime.error).toBe(error) + ] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).toReject(); + }); + }); + + it('runs one middleware in error handling chain on error in primary chain', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [() => toss(new Error('error'))], + useOnError: [middleware, (_, res) => res.end()] + }), + test: async ({ fetch }) => { + await fetch(); + expect(middleware).toBeCalledTimes(1); + } + }); + }); + }); + + it('runs multiple middleware in error handling chain on error in primary chain', async () => { + expect.hasAssertions(); + + const middleware = [jest.fn(), jest.fn(), ((_, res) => res.end()) as Middleware]; + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [() => toss(new Error('error'))], + useOnError: middleware + }), + test: async ({ fetch }) => { + await fetch(); + middleware.slice(0, -1).forEach((m) => expect(m).toBeCalledTimes(1)); + } + }); + }); + }); + + it('runs one middleware in error handling chain on error in handler', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(() => toss(new Error('error')), { + descriptor: '/fake', + use: [], + useOnError: [middleware, (_, res) => res.end()] + }), + test: async ({ fetch }) => { + await fetch(); + expect(middleware).toBeCalledTimes(1); + } + }); + }); + }); + + it('runs multiple middleware in error handling chain on error in handler', async () => { + expect.hasAssertions(); + + const middleware = [jest.fn(), jest.fn(), ((_, res) => res.end()) as Middleware]; + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(() => toss(new Error('error')), { + descriptor: '/fake', + use: [], + useOnError: middleware + }), + test: async ({ fetch }) => { + await fetch(); + middleware.slice(0, -1).forEach((m) => expect(m).toBeCalledTimes(1)); + } + }); + }); + }); + + it('skips remaining middleware if chain is aborted and aborts chain if runtime.done called', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + + await withMockedOutput(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [(_, __, context) => context.runtime.done(), middleware, middleware], + useOnError: [ + (_, __, context) => context.runtime.done(), + middleware, + middleware + ] + }), + test: async ({ fetch }) => { + await fetch(); + expect(middleware).toBeCalledTimes(0); + } + }); + + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(noopHandler, { + descriptor: '/fake', + use: [() => toss(new Error('bad')), middleware, middleware], + useOnError: [() => toss(new Error('bad')), middleware, middleware] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).toReject(); + + expect(middleware).toBeCalledTimes(0); + }); + }); + + it('throws on error in error handling chain', async () => { + expect.hasAssertions(); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [() => toss(new Error('bad'))], + useOnError: [() => toss(new Error('worse'))] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ message: 'worse' }); + }); + }); + + it('throws on error in primary chain if no error handling middleware available', async () => { + expect.hasAssertions(); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [() => toss(new Error('bad'))], + useOnError: [] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ message: 'bad' }); + }); + }); + + it('throws if res.end not called by the time error handling chain completes', async () => { + expect.hasAssertions(); + + await withMockedOutput(async () => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [() => toss(new Error('bad'))], + useOnError: [() => undefined] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ message: 'bad' }); + }); + }); + + it('makes runtime control functions noops if chain completes', async () => { + expect.hasAssertions(); + + const nextWarning = expect.stringContaining( + 'already finished executing; calling runtime.next() at this point is a noop' + ); + + const doneWarning = expect.stringContaining( + 'already finished executing; calling runtime.done() at this point is a noop' + ); + + let next: () => Promise, done: () => void; + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware( + async () => { + expect(stdErrSpy).not.toBeCalledWith(nextWarning); + expect(stdErrSpy).not.toBeCalledWith(doneWarning); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + done(); + expect(stdErrSpy).toBeCalledWith(doneWarning); + + throw new Error('badness'); + }, + { + options: { callDoneOnEnd: false }, + descriptor: '/fake', + use: [ + (_req, _res, { runtime }) => { + next = runtime.next; + done = runtime.done; + } + ], + useOnError: [ + (_req, res, { runtime }) => { + expect(runtime.error).toMatchObject({ message: 'badness' }); + + next = runtime.next; + done = runtime.done; + res.end(); + } + ] + } + ), + test: async ({ fetch }) => { + await fetch(); + + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + done(); + expect(stdErrSpy).toBeCalledWith(doneWarning); + } + }); + }); + }); + }); + + it('makes runtime control functions noops if chain aborts', async () => { + expect.hasAssertions(); + + const nextWarning = expect.stringContaining( + 'aborted; calling runtime.next() at this point is a noop' + ); + + const doneWarning = expect.stringContaining( + 'already aborted; calling runtime.done() at this point is a noop' + ); + + let next: () => Promise, done: () => void; + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await expect( + testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + (_req, _res, { runtime }) => { + next = runtime.next; + done = runtime.done; + throw new Error('aborted'); + } + ], + useOnError: [ + async (_req, _res, { runtime }) => { + expect(stdErrSpy).not.toBeCalledWith(nextWarning); + expect(stdErrSpy).not.toBeCalledWith(doneWarning); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + done(); + expect(stdErrSpy).toBeCalledWith(doneWarning); + + next = runtime.next; + done = runtime.done; + + throw new Error('aborted again'); + } + ] + }), + test: async ({ fetch }) => void (await fetch()) + }) + ).rejects.toMatchObject({ message: 'aborted again' }); + + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + done(); + expect(stdErrSpy).toBeCalledWith(doneWarning); + }); + }); + }); + + it('can pull entire chain (and then some) manually using runtime.next', async () => { + expect.hasAssertions(); + + const nextWarning = expect.stringContaining( + 'already finished executing; calling runtime.next() at this point is a noop' + ); + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + async (_req, res, { runtime: { next } }) => { + await next(); + expect(stdErrSpy).not.toBeCalledWith(nextWarning); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + res.status(200).end(); + } + ] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + } + }); + }); + }); + }); + + it('can pull entire chain manually using runtime.next with warning if called multiple times', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + const nextWarning = expect.stringContaining( + 'already finished executing; calling runtime.next() at this point is a noop' + ); + + await withMockedOutput(async ({ stdErrSpy }) => { + await withDebugEnabled(async () => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + async (_req, _res, { runtime: { next } }) => { + await next(); + expect(stdErrSpy).not.toBeCalledWith(nextWarning); + + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).toBeCalledWith(nextWarning); + + throw new Error('not good bad bad'); + }, + middleware, + middleware + ], + useOnError: [ + async (_req, _res, { runtime: { next, error } }) => { + expect(middleware).toBeCalledTimes(2); + expect(error).toMatchObject({ message: 'not good bad bad' }); + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).not.toBeCalledWith(nextWarning); + + stdErrSpy.mockClear(); + + await next(); + expect(stdErrSpy).toBeCalledWith( + expect.stringContaining( + 'aborted; calling runtime.next() at this point is a noop' + ) + ); + }, + middleware, + middleware, + (_, res) => { + expect(middleware).toBeCalledTimes(4); + res.status(200).end(); + } + ] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(200); + } + }); + }); + }); + }); + + it('skips non-function middleware in chain', async () => { + expect.hasAssertions(); + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + // @ts-expect-error: bad middleware value + 'bad', + // @ts-expect-error: bad middleware value + null, + // @ts-expect-error: bad middleware value + {}, + (_, res) => res.status(403).end() + ] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(403); + expect(stdErrSpy).toBeCalledWith( + expect.stringContaining( + 'skipping execution of non-function item in chain' + ) + ); + } + }); + }); + }); + }); + + it('calls runtime.done on res.end only if options.callDoneOnEnd is true', async () => { + expect.hasAssertions(); + + const middleware = jest.fn(); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [(_, res) => res.status(404).end(), middleware], + options: { callDoneOnEnd: false } + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(404); + expect(middleware).toBeCalledTimes(1); + } + }); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [(_, res) => res.status(403).end(), middleware], + options: { callDoneOnEnd: true } + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(403); + expect(middleware).toBeCalledTimes(1); + } + }); + }); + + it('calls runtime.done on res.end only if chain was not aborted', async () => { + expect.hasAssertions(); + + const skippedMessage = expect.stringContaining('skipped calling runtime.done'); + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + async (_, res, { runtime: { done } }) => { + done(); + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + res.status(404).end(); + expect(stdErrSpy).toBeCalledWith(skippedMessage); + } + ] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(404); + } + }); + + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware(undefined, { + descriptor: '/fake', + use: [ + async () => { + throw new Error('contrived'); + } + ], + useOnError: [ + async (_, res, { runtime: { done, error } }) => { + expect(error).toMatchObject({ message: 'contrived' }); + + done(); + + stdErrSpy.mockClear(); + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + res.status(404).end(); + expect(stdErrSpy).toBeCalledWith(skippedMessage); + } + ] + }), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(404); + } + }); + }); + }); + }); + + it('calls runtime.done on res.end only if chain has not already completed', async () => { + expect.hasAssertions(); + + const skippedMessage = expect.stringContaining('skipped calling runtime.done'); + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware( + async (_, res) => { + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + res.status(404).end(); + expect(stdErrSpy).toBeCalledWith(skippedMessage); + }, + { + descriptor: '/fake', + use: [] + } + ), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(404); + } + }); + }); + }); + }); + + it('does not call runtime.done on res.end if response was already sent', async () => { + expect.hasAssertions(); + + const skippedMessage = expect.stringContaining('skipped calling runtime.done'); + + await withDebugEnabled(async () => { + await withMockedOutput(async ({ stdErrSpy }) => { + await testApiHandler({ + rejectOnHandlerError: true, + pagesHandler: withMiddleware( + async (_, res) => { + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + res.status(404).end(); + expect(stdErrSpy).toBeCalledWith(skippedMessage); + stdErrSpy.mockClear(); + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + res.status(404).end(); + expect(stdErrSpy).not.toBeCalledWith(skippedMessage); + }, + { + descriptor: '/fake', + use: [] + } + ), + test: async ({ fetch }) => { + expect((await fetch()).status).toBe(404); + } + }); + }); + }); + }); + + it('supports type generics', async () => { + expect.assertions(0); + + type myMiddlewareOptions = { customOption: boolean }; + + const myMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext + ) => { + res.status(200).send(customOption); + }; + + const myPartialMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext> + ) => { + res.status(200).send(customOption); + }; + + withMiddleware(undefined, { + // @ts-expect-error: MiddlewareContext !== MiddlewareContext + use: [myMiddleware] + }); + + withMiddleware(undefined, { + descriptor: '/fake', + use: [myMiddleware] + // TODO: improve TypeScript skills to enforce required options here + }); + + withMiddleware(undefined, { + use: [myMiddleware], + // @ts-expect-error: missing required property: customOption + options: {} + }); + + withMiddleware(undefined, { + use: [myMiddleware], + // @ts-expect-error: bad type for required property: customOption + options: { customOption: 5 } + }); + + withMiddleware(undefined, { + use: [ + myMiddleware, + (_, __, { options: { anotherOpt } }) => { + void anotherOpt; + } + ], + // @ts-expect-error: missing required property: anotherOpt + options: { customOption: true } + }); + + withMiddleware(undefined, { + descriptor: '/fake', + use: [myPartialMiddleware] + }); + + withMiddleware>(undefined, { + descriptor: '/fake', + use: [myPartialMiddleware], + options: {} + }); + }); +}); + +describe('::middlewareFactory', () => { + it('returns a pre-configured withMiddleware instance', async () => { + expect.hasAssertions(); + + type myMiddlewareOptions = { customOption: boolean }; + + const myMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext + ) => { + res.status(200).send({ customOption }); + }; + + const customOption = true; + + const pagesHandler = middlewareFactory({ + use: [myMiddleware], + options: { customOption } + })(undefined, { + descriptor: '/fake' + }); + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + await expect((await fetch()).json()).resolves.toStrictEqual({ customOption }); + } + }); + }); + + it('handles appending and prepending to middleware chains', async () => { + expect.hasAssertions(); + + type myMiddlewareOptions = { customOption: boolean }; + + const myMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext + ) => { + res.status(200).send({ customOption }); + }; + + const customOption = true; + + await testApiHandler({ + pagesHandler: middlewareFactory({ + use: [myMiddleware], + options: { customOption } + })(undefined, { + descriptor: '/fake', + prependUse: [(_, res) => res.status(201).send({ a: 1 })] + }), + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(201); + await expect(res.json()).resolves.toStrictEqual({ a: 1 }); + } + }); + + await testApiHandler({ + pagesHandler: middlewareFactory({ + use: [(_, res) => void res.status(202)] + })(undefined, { + descriptor: '/fake', + appendUse: [(_, res) => res.send({ b: 1 })] + }), + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(202); + await expect(res.json()).resolves.toStrictEqual({ b: 1 }); + } + }); + + await testApiHandler({ + pagesHandler: middlewareFactory({ + use: [myMiddleware], + options: { customOption } + })(undefined, { + descriptor: '/fake', + prependUse: [() => toss(new DummyError('bad bad not good'))], + prependUseOnError: [(_, res) => void res.status(203)], + appendUseOnError: [(_, res) => res.send({ c: 1 })] + }), + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(203); + await expect(res.json()).resolves.toStrictEqual({ c: 1 }); + } + }); + }); + + it('supports type generics', async () => { + expect.assertions(0); + + type myMiddlewareOptions = { customOption: boolean }; + + const myMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext + ) => { + res.status(200).send(customOption); + }; + + const myPartialMiddleware = ( + _: NextApiRequest, + res: NextApiResponse, + { options: { customOption } }: MiddlewareContext> + ) => { + res.status(200).send(customOption); + }; + + middlewareFactory({ + // @ts-expect-error: MiddlewareContext !== MiddlewareContext + use: [myMiddleware] + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [myMiddleware] + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [myMiddleware], + // @ts-expect-error: missing required property: customOption + options: {} + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [myMiddleware], + // @ts-expect-error: bad type for required property: customOption + options: { customOption: 5 } + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [ + myMiddleware, + (_, __, { options: { anotherOpt } }) => { + void anotherOpt; + } + ], + // @ts-expect-error: missing required property: anotherOpt + options: { customOption: true } + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [myPartialMiddleware] + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory>({ + use: [myPartialMiddleware], + options: {} + })(undefined, { + descriptor: '/fake' + }); + + middlewareFactory({ + use: [myPartialMiddleware] + })(undefined, { + descriptor: '/fake', + // @ts-expect-error: MiddlewareContext !== MiddlewareContext + appendUse: [myMiddleware] + }); + + middlewareFactory({ + use: [myPartialMiddleware] + })(undefined, { + descriptor: '/fake', + appendUse: [myPartialMiddleware], + appendUseOnError: [myPartialMiddleware] + }); + + middlewareFactory({ + use: [myPartialMiddleware] + })(undefined, { + descriptor: '/fake', + prependUse: [myMiddleware], + prependUseOnError: [myMiddleware] + }); + + middlewareFactory({ + use: [myPartialMiddleware] + })(undefined, { + descriptor: '/fake', + // @ts-expect-error: bad type for required property: customOption + options: { customOption: 5 } + }); + }); +}); diff --git a/lib/next-api-respond/index.ts b/lib/next-api-respond/index.ts new file mode 100644 index 0000000..1a0fb65 --- /dev/null +++ b/lib/next-api-respond/index.ts @@ -0,0 +1,206 @@ +import type { NextApiResponse } from 'next'; +import type { HttpStatusCode, JsonSuccess, JsonError } from '@xunnamius/types'; + +/** + * Sends a generic HTTP response with the given `statusCode` and optional + * `responseJson` body (defaults to `{}`). This is the "base" function called by + * all other response functions. + */ +export function sendGenericHttpResponse( + res: NextApiResponse, + statusCode: HttpStatusCode, + responseJson?: Record +) { + res + .setHeader('content-type', 'application/json') + .status(statusCode) + .send(responseJson || {}); +} + +/** + * Sends a generic "success" response and `responseJson` body, optionally with + * additional properties. This function is called by all 2xx response functions. + */ +export function sendHttpSuccessResponse( + res: NextApiResponse, + statusCode: HttpStatusCode, + responseJson?: Record +) { + const json: JsonSuccess = { success: true, ...responseJson }; + sendGenericHttpResponse(res, statusCode, json); + return json; +} + +/** + * Sends a generic "error" response and `responseJson` body, optionally with + * additional properties. This function is called by all non-2xx response + * functions. + */ +export function sendHttpErrorResponse( + res: NextApiResponse, + statusCode: HttpStatusCode, + responseJson: Record & { error: string } +) { + const json: JsonError = { success: false, ...responseJson }; + sendGenericHttpResponse(res, statusCode, json); + return json; +} + +/** + * Sends an HTTP 200 "ok" response with optional `responseJson` data. + */ +export function sendHttpOk( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpSuccessResponse(res, 200, responseJson); +} + +/** + * Sends an HTTP 400 "client error" response with optional `responseJson` data. + */ +export function sendHttpBadRequest( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 400, { + error: 'request was malformed or otherwise bad', + ...responseJson + }); +} + +/** + * Sends an HTTP 401 "unauthenticated" response with optional `responseJson` + * data. + */ +export function sendHttpUnauthenticated( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 401, { + error: 'client is not authenticated', + ...responseJson + }); +} + +/** + * Sends an HTTP 403 "forbidden" ("unauthorized") response with optional + * `responseJson` data. + */ +export function sendHttpUnauthorized( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 403, { + error: 'client is not authorized to access this resource', + ...responseJson + }); +} + +/** + * Sends an HTTP 404 "not found" response with optional `responseJson` data. + */ +export function sendHttpNotFound( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 404, { + error: 'resource was not found', + ...responseJson + }); +} + +/** + * Sends an HTTP 405 "bad method" response with optional `responseJson` data. + */ +export function sendHttpBadMethod( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 405, { + error: 'bad method', + ...responseJson + }); +} + +/** + * Sends an HTTP 413 "too big" response with optional `responseJson` data. + */ +export function sendHttpTooLarge( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 413, { + error: 'request body is too large', + ...responseJson + }); +} + +/** + * Sends an HTTP 415 "unsupported media type" response with optional + * `responseJson` data. + */ +export function sendHttpBadContentType( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 415, { + error: 'request payload is in an unsupported format', + ...responseJson + }); +} + +/** + * Sends an HTTP 429 "too many requests" response with optional `responseJson` + * data. + */ +export function sendHttpRateLimited( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 429, { + error: 'client is rate limited', + ...responseJson + }); +} + +/** + * Sends a generic HTTP 500 "error" response with `error` property and optional + * `responseJson` data. + */ +export function sendHttpError( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 500, { + error: '🀯 something unexpected happened on our end 🀯', + ...responseJson + }); +} + +/** + * Sends an HTTP 501 "not implemented" response with optional `responseJson` + * data. + */ +export function sendNotImplemented( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 501, { + error: 'this endpoint has not yet been implemented', + ...responseJson + }); +} + +/** + * Sends an HTTP 555 "contrived" response with optional `responseJson` data. + */ +export function sendHttpContrivedError( + res: NextApiResponse, + responseJson?: Record +) { + sendHttpErrorResponse(res, 555, { + error: '(note: do not report this contrived error)', + ...responseJson + }); +} diff --git a/lib/next-api-respond/package.json b/lib/next-api-respond/package.json new file mode 100644 index 0000000..0b40e8e --- /dev/null +++ b/lib/next-api-respond/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-api-respond" +} diff --git a/lib/next-api-respond/unit.test.ts b/lib/next-api-respond/unit.test.ts new file mode 100644 index 0000000..758a5f4 --- /dev/null +++ b/lib/next-api-respond/unit.test.ts @@ -0,0 +1,533 @@ +import { + sendGenericHttpResponse, + sendHttpBadContentType, + sendHttpBadMethod, + sendHttpBadRequest, + sendHttpContrivedError, + sendHttpError, + sendHttpErrorResponse, + sendHttpNotFound, + sendHttpOk, + sendHttpRateLimited, + sendHttpSuccessResponse, + sendHttpTooLarge, + sendHttpUnauthenticated, + sendHttpUnauthorized, + sendNotImplemented +} from 'multiverse/next-api-respond'; +import { testApiHandler } from 'next-test-api-route-handler'; + +describe('::sendGenericHttpResponse', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendGenericHttpResponse(res, 201); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(201); + await expect(res.json()).resolves.toStrictEqual({}); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendGenericHttpResponse(res, 201, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(201); + await expect(res.json()).resolves.toStrictEqual({ json: 'data' }); + } + }); + }); + + it('sends application/json header', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendGenericHttpResponse(res, 200); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toStartWith('application/json'); + } + }); + }); +}); + +describe('::sendHttpBadMethod', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadMethod(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(405); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'bad method' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadMethod(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(405); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'bad method', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpBadRequest', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadRequest(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(400); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request was malformed or otherwise bad' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadRequest(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(400); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request was malformed or otherwise bad', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpContrivedError', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpContrivedError(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(555); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: '(note: do not report this contrived error)' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpContrivedError(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(555); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: '(note: do not report this contrived error)', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpError', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpError(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(500); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: '🀯 something unexpected happened on our end 🀯' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpError(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(500); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: '🀯 something unexpected happened on our end 🀯', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpOk', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpOk(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + await expect(res.json()).resolves.toStrictEqual({ + success: true + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpOk(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + await expect(res.json()).resolves.toStrictEqual({ + success: true, + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpErrorResponse', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpErrorResponse(res, 400, { json: 'data', error: 'error' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(400); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + json: 'data', + error: 'error' + }); + } + }); + }); +}); + +describe('::sendHttpNotFound', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpNotFound(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(404); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'resource was not found' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpNotFound(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(404); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'resource was not found', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpRateLimited', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpRateLimited(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(429); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is rate limited' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpRateLimited(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(429); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is rate limited', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpSuccessResponse', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpSuccessResponse(res, 202); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(202); + await expect(res.json()).resolves.toStrictEqual({ + success: true + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpSuccessResponse(res, 202, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(202); + await expect(res.json()).resolves.toStrictEqual({ + success: true, + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpTooLarge', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpTooLarge(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(413); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request body is too large' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpTooLarge(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(413); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request body is too large', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpBadContentType', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadContentType(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(415); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request payload is in an unsupported format' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpBadContentType(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(415); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'request payload is in an unsupported format', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpUnauthenticated', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpUnauthenticated(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(401); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is not authenticated' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpUnauthenticated(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(401); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is not authenticated', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendHttpUnauthorized', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpUnauthorized(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(403); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is not authorized to access this resource' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendHttpUnauthorized(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(403); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'client is not authorized to access this resource', + json: 'data' + }); + } + }); + }); +}); + +describe('::sendNotImplemented', () => { + it('sends appropriate response given arguments', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendNotImplemented(res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(501); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'this endpoint has not yet been implemented' + }); + } + }); + + await testApiHandler({ + pagesHandler: (_, res) => { + sendNotImplemented(res, { json: 'data' }); + }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(501); + await expect(res.json()).resolves.toStrictEqual({ + success: false, + error: 'this endpoint has not yet been implemented', + json: 'data' + }); + } + }); + }); +}); diff --git a/lib/next-api-util/index.ts b/lib/next-api-util/index.ts new file mode 100644 index 0000000..187a3f8 --- /dev/null +++ b/lib/next-api-util/index.ts @@ -0,0 +1,13 @@ +import { ErrorMessage, ValidationError } from 'universe/error'; +import type { JsonValue } from 'type-fest'; + +export function validateAndParseJson( + input: string | null | undefined, + property?: string +): T { + try { + return JSON.parse(input || ''); + } catch { + throw new ValidationError(ErrorMessage.InvalidJSON(property)); + } +} diff --git a/lib/next-api-util/package.json b/lib/next-api-util/package.json new file mode 100644 index 0000000..6c075d1 --- /dev/null +++ b/lib/next-api-util/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-api-util" +} diff --git a/lib/next-auth/authenticate.ts b/lib/next-auth/authenticate.ts new file mode 100644 index 0000000..2aa85e1 --- /dev/null +++ b/lib/next-auth/authenticate.ts @@ -0,0 +1,55 @@ +import { isNativeError } from 'node:util/types'; + +import { debugFactory } from 'multiverse/debug-extended'; +import { getTokenByDerivation } from './token'; + +const debug = debugFactory('next-auth:authenticate'); + +/** + * An array of supported authentication schemes. + */ +// ! Must be lowercase alphanumeric (enforced by unit tests) +export const validAuthenticationSchemes = ['bearer'] as const; + +/** + * A supported authentication scheme. + */ +export type AuthenticationScheme = (typeof validAuthenticationSchemes)[number]; + +/** + * Authenticates a client via their Authorization header using the well-known + * "auth" MongoDB collection. Does not throw on invalid/missing header string. + * + * Despite the unfortunate name of the "Authorization" header, this function is + * only used for authentication, not authorization. + */ +export async function authenticateHeader({ + header, + allowedSchemes +}: { + /** + * Contents of the HTTP Authorization header. + */ + header: string | undefined; + /** + * Accepted authentication schemes. By default, all schemes are accepted. + */ + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; +}): Promise<{ authenticated: boolean; error?: string }> { + try { + await getTokenByDerivation({ from: header, allowedSchemes }); + } catch (error) { + debug.error(`authentication failure: ${error}`); + + if ( + isNativeError(error) && + !['NotAuthenticatedError', 'InvalidSecretError'].includes(error.name) + ) { + throw error; + } + + return { authenticated: false }; + } + + return { authenticated: true }; +} diff --git a/lib/next-auth/authorize.ts b/lib/next-auth/authorize.ts new file mode 100644 index 0000000..dfccfe9 --- /dev/null +++ b/lib/next-auth/authorize.ts @@ -0,0 +1,100 @@ +import { isNativeError } from 'node:util/types'; +import { InvalidAppConfigurationError, NotAuthorizedError } from 'named-app-errors'; + +import { debugFactory } from 'multiverse/debug-extended'; +import { getTokenByDerivation } from './token'; + +const debug = debugFactory('next-auth:authenticate'); + +/** + * An array of supported well-known authorization (Authorization header) + * constraints. + */ +export const validAuthorizationConstraints = [ + /** + * This constraint ensures that only "auth" entries that have the + * `globalAdmin` field set to `true` are successfully authenticated. + */ + 'isGlobalAdmin' +] as const; + +/** + * A supported authorization constraint. + */ +export type AuthorizationConstraint = (typeof validAuthorizationConstraints)[number]; + +/** + * Authorizes a client via their Authorization header using the well-known + * "auth" MongoDB collection. Does not throw on invalid/missing header string. + */ +export async function authorizeHeader({ + header, + constraints +}: { + /** + * Contents of the HTTP Authorization header. + */ + header: string | undefined; + /** + * Constraints a client must satisfy to be considered authorized. + */ + constraints?: AuthorizationConstraint | AuthorizationConstraint[]; +}): Promise<{ authorized: boolean; error?: string }> { + try { + const { attributes } = await getTokenByDerivation({ from: header }); + + if ( + typeof constraints !== 'string' && + (!Array.isArray(constraints) || !constraints.length) + ) { + debug.warn('header authorization was vacuous (no constraints)'); + } else { + const constraintsArray = [constraints].flat(); + const finalConstraints = Array.from(new Set(constraintsArray)); + + if (finalConstraints.length !== constraintsArray.length) { + throw new InvalidAppConfigurationError( + 'encountered duplicate authorization constraints' + ); + } else { + await Promise.all( + finalConstraints.map(async (constraint) => { + debug(`evaluating authorization constraint "${constraint}"`); + + const failAuthorization = (constraint: AuthorizationConstraint) => { + debug.warn(`authorization failed on constraint: ${constraint}`); + throw new NotAuthorizedError( + `failed to satisfy authorization constraint "${constraint}"` + ); + }; + + if (constraint === 'isGlobalAdmin') { + if (!attributes.isGlobalAdmin) { + failAuthorization('isGlobalAdmin'); + } + } /*else if(constraint === '...') { + ... + }*/ else { + throw new InvalidAppConfigurationError( + `encountered unknown or unhandled authorization constraint "${constraint}"` + ); + } + }) + ); + } + } + } catch (error) { + debug.error(`authorization failure: ${error}`); + + if ( + isNativeError(error) && + !['NotAuthorizedError', 'InvalidSecretError'].includes(error.name) + ) { + throw error; + } + + return { authorized: false }; + } + + return { authorized: true }; +} diff --git a/lib/next-auth/constants.ts b/lib/next-auth/constants.ts new file mode 100644 index 0000000..d630289 --- /dev/null +++ b/lib/next-auth/constants.ts @@ -0,0 +1,43 @@ +// * Hard-coded constants +export function getConfig() { + return { + /** + * Used as the MongoDb query resultset limit. The API will never return + * more JSON objects than this number. + * + * If this number is not positive, behavior is undefined. + */ + resultsPerPage: 100 + }; +} + +// * Well-known tokens + +/** + * This string is guaranteed never to appear in data generated during tests or + * in production. Hence, this string can be used to represent a `null` or + * non-existent token. This string cannot be used for authenticated HTTP access + * to the API. + */ +export const NULL_BEARER_TOKEN = '00000000-0000-0000-0000-000000000000'; + +/** + * This string allows authenticated API access only when running in a test + * environment (i.e. `NODE_ENV=test`). This string cannot be used for + * authenticated HTTP access to the API in production. + */ +export const DUMMY_BEARER_TOKEN = '12349b61-83a7-4036-b060-213784b491'; + +/** + * This string is guaranteed to be rate limited when running in a test + * environment (i.e. `NODE_ENV=test`). This string cannot be used for + * authenticated HTTP access to the API in production. + */ +export const BANNED_BEARER_TOKEN = 'banned-h54e-6rt7-gctfh-hrftdygct0'; + +/** + * This string can be used to authenticate with local and _non-web-facing_ test + * and preview deployments as a global administrator. This string cannot be used + * for authenticated HTTP access to the API in production. + */ +export const DEV_BEARER_TOKEN = 'dev-xunn-dev-294a-536h-9751-rydmj'; diff --git a/lib/next-auth/db.ts b/lib/next-auth/db.ts new file mode 100644 index 0000000..e34fc87 --- /dev/null +++ b/lib/next-auth/db.ts @@ -0,0 +1,103 @@ +import { getDb } from 'multiverse/mongo-schema'; + +import { + isTokenAttributes, + type BearerToken, + type Token, + type TokenAttributes +} from './token'; + +import type { WithId } from 'mongodb'; +import type { Merge, Exact } from 'type-fest'; + +/** + * The base shape of an entry in the well-known "auth" collection. Consists of a + * token, its scheme, and its attributes. + * + * **More complex entry types must extend from or intersect with this base + * type.** + */ +export type InternalAuthEntry = WithId< + { + /** + * Metadata indicating if an entry has been soft-deleted or not. If `deleted` is `true`, this entry will be ignored by the other CRUD functions. + */ + deleted: boolean; + /** + * Metadata attributes associated with this "auth" entry. + */ + attributes: TokenAttributes; + } & Token +>; + +/** + * The base shape of a new entry in the well-known "auth" collection. More + * complex entry types may or may not extend from or intersect with this type. + * + * Each API has the latitude to generate a token using whichever available + * scheme is most convenient. Hence, the only external data necessary to create + * a new auth entry is `attributes`. + */ +export type NewAuthEntry = Pick; + +/** + * The public base shape derived from an entry in the well-known "auth" + * collection. + */ +export type PublicAuthEntry = Pick< + InternalAuthEntry, + 'attributes' | 'scheme' | 'token' +> & { + /** + * A string representation of the ObjectId associated with this entry. + */ + auth_id: string; +}; + +/** + * The shape of a bearer token entry in the well-known "auth" collection. + */ +export type InternalAuthBearerEntry = Merge; + +/** + * A MongoDB cursor projection that transforms an internal auth entry (or + * "token") into a public auth entry. + */ +export const publicAuthEntryProjection = { + _id: false, + auth_id: { $toString: '$_id' }, + attributes: true, + scheme: true, + token: true +}; + +/** + * Return the well-known "auth" collection after calling {@link getDb} on the + * `'root'` database. + */ +export async function getAuthDb() { + return (await getDb({ name: 'root' })).collection('auth'); +} + +/** + * Transform an internal entry from the well-known "auth" MongoDB collection + * into one that is safe for consumption. + */ +export function toPublicAuthEntry(entry: InternalAuthEntry): PublicAuthEntry { + const { + _id, + deleted: _, + ...publicEntry + } = { ...entry, auth_id: entry._id.toString() }; + + return publicEntry /*satisfies Exact*/; +} + +/** + * Type guard that returns `true` if `obj` satisfies the {@link NewAuthEntry} + * interface. + */ +export function isNewAuthEntry(obj: unknown): obj is NewAuthEntry { + const entry = obj as NewAuthEntry; + return isTokenAttributes(entry?.attributes); +} diff --git a/lib/next-auth/index.ts b/lib/next-auth/index.ts new file mode 100644 index 0000000..d4c0c55 --- /dev/null +++ b/lib/next-auth/index.ts @@ -0,0 +1,5 @@ +export * from './authenticate'; +export * from './authorize'; +export * from './constants'; +export * from './db'; +export * from './token'; diff --git a/lib/next-auth/package.json b/lib/next-auth/package.json new file mode 100644 index 0000000..508d875 --- /dev/null +++ b/lib/next-auth/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-auth" +} diff --git a/lib/next-auth/test/unit-authenticate.test.ts b/lib/next-auth/test/unit-authenticate.test.ts new file mode 100644 index 0000000..2dd4afc --- /dev/null +++ b/lib/next-auth/test/unit-authenticate.test.ts @@ -0,0 +1,105 @@ +import { useMockDateNow } from 'multiverse/mongo-common'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + authenticateHeader, + validAuthenticationSchemes, + type AuthenticationScheme +} from 'multiverse/next-auth/authenticate'; + +import { + BANNED_BEARER_TOKEN, + DEV_BEARER_TOKEN, + DUMMY_BEARER_TOKEN, + NULL_BEARER_TOKEN +} from 'multiverse/next-auth/constants'; + +import { deleteTokenById, getTokenByDerivation } from 'multiverse/next-auth/token'; + +setupMemoryServerOverride(); +useMockDateNow(); + +test('ensure validAuthenticationSchemes contains only lowercase alphanumeric strings', () => { + expect.hasAssertions(); + const isLowercaseAlphanumeric = /^[\da-z]+$/; + + expect( + validAuthenticationSchemes.every( + (scheme) => typeof scheme === 'string' && isLowercaseAlphanumeric.test(scheme) + ) + ).toBeTrue(); +}); + +describe('::authenticateHeader', () => { + it('returns an authenticated response if bearer token exists in database', async () => { + expect.hasAssertions(); + + await expect( + authenticateHeader({ + header: `bearer ${DUMMY_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + authenticateHeader({ + header: `BEARER ${DEV_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: true }); + }); + + // ? Rejecting banned tokens is handled at a different layer than validation + it('returns an authenticated response even if bearer token is banned', async () => { + expect.hasAssertions(); + + await expect( + authenticateHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: true }); + }); + + it('returns a not-authenticated response if bearer token does not exist in database', async () => { + expect.hasAssertions(); + + await expect( + authenticateHeader({ + header: `bearer ${NULL_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: false }); + }); + + it('returns a not-authenticated response if using a disallowed scheme', async () => { + expect.hasAssertions(); + + await expect( + authenticateHeader({ + header: 'bearer 123', + allowedSchemes: ['none' as unknown as AuthenticationScheme] + }) + ).resolves.toStrictEqual({ authenticated: false }); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + authenticateHeader({ + header: `bearer ${DUMMY_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + deleteTokenById({ + auth_id: ( + await getTokenByDerivation({ from: `bearer ${DUMMY_BEARER_TOKEN}` }) + ).auth_id + }) + ).resolves.toBe(1); + + await expect( + authenticateHeader({ + header: `bearer ${DUMMY_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authenticated: false }); + }); +}); diff --git a/lib/next-auth/test/unit-authorize.test.ts b/lib/next-auth/test/unit-authorize.test.ts new file mode 100644 index 0000000..932cadb --- /dev/null +++ b/lib/next-auth/test/unit-authorize.test.ts @@ -0,0 +1,156 @@ +import { useMockDateNow } from 'multiverse/mongo-common'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + authorizeHeader, + type AuthorizationConstraint +} from 'multiverse/next-auth/authorize'; + +import { + BANNED_BEARER_TOKEN, + DEV_BEARER_TOKEN, + DUMMY_BEARER_TOKEN, + NULL_BEARER_TOKEN +} from 'multiverse/next-auth/constants'; + +import { deleteTokenById, getTokenByDerivation } from 'multiverse/next-auth/token'; + +setupMemoryServerOverride(); +useMockDateNow(); + +describe('::authorizeHeader', () => { + it('returns a vacuously authorized response if bearer token exists in database', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${DUMMY_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authorized: true }); + + await expect( + authorizeHeader({ + header: `BEARER ${DEV_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authorized: true }); + }); + + // ? Rejecting banned tokens is handled at a different layer than authorization + it('returns a vacuously authorized response even if bearer token is banned', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authorized: true }); + }); + + it('returns a vacuously authorized response if passed no constraints', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}`, + constraints: [] + }) + ).resolves.toStrictEqual({ authorized: true }); + }); + + it('returns an authorized response if authorization succeeds', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${DEV_BEARER_TOKEN}`, + constraints: ['isGlobalAdmin'] + }) + ).resolves.toStrictEqual({ authorized: true }); + }); + + it('returns a not-authorized response if bearer token does not exist in database', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${NULL_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ authorized: false }); + }); + + it('returns a not-authorized response only if the isGlobalAdmin constraint fails', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ authorized: false }); + + await expect( + authorizeHeader({ + header: `bearer ${DEV_BEARER_TOKEN}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ + authorized: true + }); + }); + + it('rejects if duplicate constraints provided', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}`, + constraints: ['isGlobalAdmin', 'isGlobalAdmin'] + }) + ).rejects.toMatchObject({ + message: expect.stringContaining( + 'encountered duplicate authorization constraints' + ) + }); + }); + + it('rejects if a non-existent constraint is provided', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${BANNED_BEARER_TOKEN}`, + constraints: ['fake-constraint' as AuthorizationConstraint] + }) + ).rejects.toMatchObject({ + message: expect.stringContaining( + 'encountered unknown or unhandled authorization constraint "fake-constraint"' + ) + }); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + authorizeHeader({ + header: `bearer ${DEV_BEARER_TOKEN}`, + constraints: ['isGlobalAdmin'] + }) + ).resolves.toStrictEqual({ authorized: true }); + + await expect( + deleteTokenById({ + auth_id: (await getTokenByDerivation({ from: `bearer ${DEV_BEARER_TOKEN}` })) + .auth_id + }) + ).resolves.toBe(1); + + await expect( + authorizeHeader({ + header: `bearer ${DEV_BEARER_TOKEN}` + }) + ).resolves.toStrictEqual({ + authorized: false + }); + }); +}); diff --git a/lib/next-auth/test/unit-db.test.ts b/lib/next-auth/test/unit-db.test.ts new file mode 100644 index 0000000..2e28f5e --- /dev/null +++ b/lib/next-auth/test/unit-db.test.ts @@ -0,0 +1,37 @@ +import { isNewAuthEntry } from 'multiverse/next-auth/db'; + +describe('::isNewAuthEntry', () => { + it('returns true only if passed a NewAuthEntry', async () => { + expect.hasAssertions(); + + expect(isNewAuthEntry(undefined)).toBeFalse(); + expect(isNewAuthEntry(null)).toBeFalse(); + expect(isNewAuthEntry(1)).toBeFalse(); + expect(isNewAuthEntry('1')).toBeFalse(); + expect(isNewAuthEntry({})).toBeFalse(); + expect(isNewAuthEntry({ attributes: undefined })).toBeFalse(); + expect(isNewAuthEntry({ attributes: null })).toBeFalse(); + expect(isNewAuthEntry({ attributes: { owner: true } })).toBeFalse(); + expect(isNewAuthEntry({ attributes: { owner: null } })).toBeFalse(); + + expect( + isNewAuthEntry({ attributes: { owner: 'owner', isGlobalAdmin: 1 } }) + ).toBeFalse(); + + expect( + isNewAuthEntry({ attributes: { owner: 'owner', isGlobalAdmin: 'true' } }) + ).toBeFalse(); + + expect( + isNewAuthEntry({ + attributes: { owner: 'owner', isGlobalAdmin: false, extra: 'prop' } + }) + ).toBeFalse(); + + expect( + isNewAuthEntry({ attributes: { owner: 'owner', isGlobalAdmin: false } }) + ).toBeTrue(); + + expect(isNewAuthEntry({ attributes: { owner: 'owner' } })).toBeTrue(); + }); +}); diff --git a/lib/next-auth/test/unit-index.test.ts b/lib/next-auth/test/unit-index.test.ts new file mode 100644 index 0000000..bccab35 --- /dev/null +++ b/lib/next-auth/test/unit-index.test.ts @@ -0,0 +1,187 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import { ObjectId } from 'mongodb'; +import { randomUUID } from 'node:crypto'; + +import { useMockDateNow } from 'multiverse/mongo-common'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import * as NextAuthTokenSpyTarget from 'multiverse/next-auth/token'; + +import { + DUMMY_BEARER_TOKEN, + authenticateHeader, + authorizeHeader, + createToken, + deriveSchemeAndToken, + getAuthDb, + validAuthenticationSchemes, + type AuthenticationScheme, + type InternalAuthEntry, + type Token, + type TokenAttributes +} from 'multiverse/next-auth'; + +setupMemoryServerOverride(); +useMockDateNow(); + +jest.mock('node:crypto', () => { + const crypto = jest.requireActual('node:crypto'); + return { ...crypto, randomUUID: jest.fn() }; +}); + +const mockRandomUUID = asMockedFunction(randomUUID); +const _validAuthenticationSchemes = validAuthenticationSchemes.slice(); +const mutableAuthenticationSchemes = + validAuthenticationSchemes as unknown as string[]; + +beforeEach(() => { + mockRandomUUID.mockReturnValue(DUMMY_BEARER_TOKEN); +}); + +afterEach(() => { + mutableAuthenticationSchemes.splice( + 0, + mutableAuthenticationSchemes.length, + ..._validAuthenticationSchemes + ); +}); + +test('ensure multiple different auth entries of various schemes can coexist', async () => { + expect.hasAssertions(); + + mockRandomUUID.mockImplementation(jest.requireActual('node:crypto').randomUUID); + + const uuid = randomUUID(); + const authDb = await getAuthDb(); + + mutableAuthenticationSchemes.push('new-scheme-1', 'new-scheme-2'); + + const newEntryRed: InternalAuthEntry = { + _id: new ObjectId(), + deleted: false, + attributes: { + owner: 'owner-red', + isGlobalAdmin: false, + createdAt: Date.now() + } as TokenAttributes, + scheme: 'new-scheme-1' as AuthenticationScheme, + token: { id1: uuid.slice(0, 32), id2: uuid.slice(32) } + }; + + const newEntryBlue: InternalAuthEntry = { + _id: new ObjectId(), + deleted: false, + attributes: { owner: 'owner-blue', isGlobalAdmin: true }, + scheme: 'new-scheme-2' as AuthenticationScheme, + token: { + uuid, + salt: uuid.slice(0, 3), + granter: { key: `${uuid.slice(0, 3)}-${uuid}` } + } + }; + + const actual_deriveSchemeAndToken = deriveSchemeAndToken; + + jest + .spyOn(NextAuthTokenSpyTarget, 'deriveSchemeAndToken') + .mockImplementation(async function ({ + authString, + authData + }: { + authString?: string; + authData?: Token; + }): Promise { + let returnValue: Token | undefined; + + if ( + authString?.startsWith('new-scheme-1') || + authData?.scheme?.startsWith('new-scheme-1') + ) { + returnValue = { + scheme: 'new-scheme-1' as AuthenticationScheme, + token: { id1: uuid.slice(0, 32), id2: uuid.slice(32) } + }; + } else if ( + authString?.startsWith('new-scheme-2') || + authData?.scheme?.startsWith('new-scheme-2') + ) { + returnValue = { + scheme: 'new-scheme-2' as AuthenticationScheme, + token: { + uuid, + salt: uuid.slice(0, 3), + granter: { key: `${uuid.slice(0, 3)}-${uuid}` } + } + }; + } else { + // eslint-disable-next-line prefer-rest-params + returnValue = await actual_deriveSchemeAndToken(arguments[0]); + } + + return returnValue; + } as typeof deriveSchemeAndToken); + + jest.spyOn(NextAuthTokenSpyTarget, 'isTokenAttributes').mockReturnValue(true); + + const newEntry1 = await createToken({ + data: { attributes: { owner: 'owner-1' } } + }); + + const newEntry2 = await createToken({ + data: { attributes: { owner: 'owner-2', isGlobalAdmin: true } } + }); + + // * Pseudo-createToken calls + await authDb.insertOne(newEntryRed); + await authDb.insertOne(newEntryBlue); + + await expect( + authenticateHeader({ header: `${newEntry1.scheme} ${newEntry1.token.bearer}` }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + authenticateHeader({ header: `${newEntry2.scheme} ${newEntry2.token.bearer}` }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + authenticateHeader({ header: `${newEntryRed.scheme} ${newEntryRed.token.id1}` }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + authenticateHeader({ + header: `${newEntryBlue.scheme} ${newEntryBlue.token.uuid}` + }) + ).resolves.toStrictEqual({ authenticated: true }); + + await expect( + authenticateHeader({ header: `${newEntry1.scheme} ${newEntryBlue.token.uuid}` }) + ).resolves.toStrictEqual({ authenticated: false }); + + await expect( + authorizeHeader({ + header: `${newEntry1.scheme} ${newEntry1.token.bearer}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ authorized: false }); + + await expect( + authorizeHeader({ + header: `${newEntry2.scheme} ${newEntry2.token.bearer}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ authorized: true }); + + await expect( + authorizeHeader({ + header: `${newEntryRed.scheme} ${newEntryRed.token.id1}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ authorized: false }); + + await expect( + authorizeHeader({ + header: `${newEntryBlue.scheme} ${newEntryBlue.token.uuid}`, + constraints: 'isGlobalAdmin' + }) + ).resolves.toStrictEqual({ authorized: true }); +}); diff --git a/lib/next-auth/test/unit-token.test.ts b/lib/next-auth/test/unit-token.test.ts new file mode 100644 index 0000000..f1dd17d --- /dev/null +++ b/lib/next-auth/test/unit-token.test.ts @@ -0,0 +1,1720 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; +import { ObjectId } from 'mongodb'; +import { randomUUID } from 'node:crypto'; + +import { expectExceptionsWithMatchingErrors } from 'multiverse/jest-expect-matching-errors'; +import { objectIdPseudoSortPredicate } from 'multiverse/jest-mongo-object-id-pseudo-sort'; +import { dummyRootData, useMockDateNow } from 'multiverse/mongo-common'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { ErrorMessage } from 'universe/error'; + +import * as NextAuthConstants from 'multiverse/next-auth/constants'; + +import { + BANNED_BEARER_TOKEN, + DEV_BEARER_TOKEN, + DUMMY_BEARER_TOKEN, + NULL_BEARER_TOKEN, + createToken, + deleteTokenById, + deleteTokensByAttribute, + deriveSchemeAndToken, + getAuthDb, + getTokenByDerivation, + getTokenById, + getTokensByAttribute, + isAllowedScheme, + isTokenAttributes, + isTokenAttributesFilter, + toPublicAuthEntry, + updateTokenAttributesById, + updateTokensAttributesByAttribute, + validAuthenticationSchemes, + type AuthenticationScheme, + type InternalAuthEntry, + type PublicAuthEntry, + type TokenAttribute, + type TokenAttributes +} from 'multiverse/next-auth'; + +setupMemoryServerOverride(); +useMockDateNow(); + +jest.mock('node:crypto', () => { + const crypto = jest.requireActual('node:crypto'); + return { ...crypto, randomUUID: jest.fn() }; +}); + +async function countAuthDbTokens() { + return (await getAuthDb()).countDocuments({ deleted: false }); +} + +const mockRandomUUID = asMockedFunction(randomUUID); +const _validAuthenticationSchemes = validAuthenticationSchemes.slice(); +const mutableAuthenticationSchemes = + validAuthenticationSchemes as unknown as string[]; + +beforeEach(() => { + mockRandomUUID.mockReturnValue(DUMMY_BEARER_TOKEN); +}); + +afterEach(() => { + mutableAuthenticationSchemes.splice( + 0, + mutableAuthenticationSchemes.length, + ..._validAuthenticationSchemes + ); +}); + +test("ensure validTokenAttributes forms a bijection on TokenAttributes's fields", () => { + expect.hasAssertions(); + + // ? This is a TypeScript-only "test" where type checking will fail if + // ? `TokenAttributes` does not match `validTokenAttributes`. While this won't + // ? fail when run via jest, this will fail the pre-commit hook. + const x: keyof TokenAttributes = '' as TokenAttribute; + const y: TokenAttribute = '' as keyof TokenAttributes; + expect(x).toBe(y); +}); + +describe('::createToken', () => { + it('creates an auth entry and returns the new token', async () => { + expect.hasAssertions(); + + const crypto = jest.requireActual('node:crypto'); + const newToken1 = crypto.randomUUID(); + const newToken2 = crypto.randomUUID(); + + mockRandomUUID.mockReturnValueOnce(newToken1); + mockRandomUUID.mockReturnValueOnce(newToken2); + + const authDb = await getAuthDb(); + + await expect( + authDb.countDocuments({ 'attributes.owner': 'new-owner' }) + ).resolves.toBe(0); + + await expect( + createToken({ data: { attributes: { owner: 'new-owner' } } }) + ).resolves.toStrictEqual({ + auth_id: expect.any(String), + attributes: { owner: 'new-owner' }, + scheme: 'bearer', + token: { bearer: newToken1 } + }); + + await expect( + authDb.countDocuments({ + attributes: { owner: 'new-owner' }, + scheme: 'bearer', + token: { bearer: newToken1 } + }) + ).resolves.toBe(1); + + await expect( + createToken({ + data: { attributes: { owner: 'new-owner', isGlobalAdmin: true } } + }) + ).resolves.toStrictEqual({ + auth_id: expect.any(String), + attributes: { owner: 'new-owner', isGlobalAdmin: true }, + scheme: 'bearer', + token: { bearer: newToken2 } + }); + + await expect( + authDb.countDocuments({ + attributes: { owner: 'new-owner', isGlobalAdmin: true }, + scheme: 'bearer', + token: { bearer: newToken2 } + }) + ).resolves.toBe(1); + + await expect( + authDb.countDocuments({ 'attributes.owner': 'new-owner' }) + ).resolves.toBe(2); + }); + + it('rejects if a duplicate token is accidentally generated', async () => { + expect.hasAssertions(); + + await expect( + createToken({ data: { attributes: { owner: 'new-owner' } } }) + ).rejects.toMatchObject({ + message: expect.stringContaining('token collision') + }); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + const errors: [params: Parameters[0], error: string][] = [ + [ + {} as unknown as Parameters[0], + ErrorMessage.InvalidSecret('token data') + ], + [{ data: { attributes: undefined } }, ErrorMessage.InvalidSecret('token data')], + [ + { data: { attributes: null as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: false as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: true as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: {} as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { attributes: { isGlobalAdmin: null } as unknown as TokenAttributes } + }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: { isGlobalAdmin: 1 } as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { attributes: { isGlobalAdmin: true } as unknown as TokenAttributes } + }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: { name: 'owner' } as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { data: { attributes: { owner: null } as unknown as TokenAttributes } }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { + attributes: { + owner: 'name', + isGlobalAdmin: 1 + } as unknown as TokenAttributes + } + }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { + attributes: { + owner: 'name', + isGlobalAdmin: null + } as unknown as TokenAttributes + } + }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { + attributes: { + owner: 'name', + isGlobalAdmin: 'true' + } as unknown as TokenAttributes + } + }, + ErrorMessage.InvalidSecret('token data') + ], + [ + { + data: { + attributes: { + owner: 'name', + extra: 1 + } as unknown as TokenAttributes + } + }, + ErrorMessage.InvalidSecret('token data') + ] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => createToken(params)); + }); +}); + +describe('::deleteTokenById', () => { + it('deletes an auth entry by auth_id ans returns deleted count', async () => { + expect.hasAssertions(); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length); + + await expect( + deleteTokenById({ auth_id: dummyRootData.auth[1]._id }) + ).resolves.toBe(1); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length - 1); + + await expect( + deleteTokenById({ auth_id: dummyRootData.auth[0]._id }) + ).resolves.toBe(1); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length - 2); + }); + + it('returns 0 deleted count if auth_id not found', async () => { + expect.hasAssertions(); + + await expect(deleteTokenById({ auth_id: new ObjectId() })).resolves.toBe(0); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + const errors: [params: Parameters[0], error: string][] = [ + [ + {} as unknown as Parameters[0], + ErrorMessage.InvalidObjectId('undefined') + ], + [{ auth_id: undefined }, ErrorMessage.InvalidObjectId('undefined')], + [{ auth_id: null as unknown as ObjectId }, ErrorMessage.InvalidObjectId(null)], + [ + { auth_id: false as unknown as ObjectId }, + ErrorMessage.InvalidObjectId(false) + ], + [{ auth_id: true as unknown as ObjectId }, ErrorMessage.InvalidObjectId(true)], + [{ auth_id: {} as unknown as ObjectId }, ErrorMessage.InvalidObjectId({})], + [{ auth_id: '' }, ErrorMessage.InvalidObjectId('')], + [{ auth_id: 'xyz' }, ErrorMessage.InvalidObjectId('xyz')], + [{ auth_id: 5 as unknown as ObjectId }, ErrorMessage.InvalidObjectId(5)] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + deleteTokenById(params) + ); + }); + + it('ignores already-deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + deleteTokenById({ auth_id: dummyRootData.auth[1]._id }) + ).resolves.toBe(1); + + await expect( + deleteTokenById({ auth_id: dummyRootData.auth[1]._id }) + ).resolves.toBe(0); + }); +}); + +describe('::deleteTokensByAttribute', () => { + it('deletes an auth entry', async () => { + expect.hasAssertions(); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length); + await expect( + deleteTokensByAttribute({ filter: dummyRootData.auth[1].attributes }) + ).resolves.toBe(1); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length - 1); + + await expect( + deleteTokensByAttribute({ + filter: dummyRootData.auth[0].attributes + }) + ).resolves.toBe(1); + + await expect(countAuthDbTokens()).resolves.toBe(dummyRootData.auth.length - 2); + }); + + it('deletes multiple matching auth entries', async () => { + expect.hasAssertions(); + + await expect( + deleteTokensByAttribute({ + filter: { owner: dummyRootData.auth.map((entry) => entry.attributes.owner) } + }) + ).resolves.toBe(dummyRootData.auth.length); + + await expect(countAuthDbTokens()).resolves.toBe(0); + }); + + it('returns 0 deleted count if auth_id not found', async () => { + expect.hasAssertions(); + + await expect( + deleteTokensByAttribute({ filter: { owner: 'does-not-exist' } }) + ).resolves.toBe(0); + }); + + it('rejects if attempting to delete all tokens (empty filter)', async () => { + expect.hasAssertions(); + + await expect(deleteTokensByAttribute({ filter: {} })).rejects.toMatchObject({ + message: ErrorMessage.InvalidSecret('filter') + }); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + type Params = Parameters[0]; + const errors: [params: Params, error: string][] = [ + [{} as unknown as Params, ErrorMessage.InvalidSecret('filter')], + [{ filter: undefined }, ErrorMessage.InvalidSecret('filter')], + [{ filter: null }, ErrorMessage.InvalidSecret('filter')], + [{ filter: false }, ErrorMessage.InvalidSecret('filter')], + [{ filter: true }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: { owner: '' } }, ErrorMessage.InvalidSecret('filter')], + [{ filter: { owner: 5 } }, ErrorMessage.InvalidSecret('filter')], + [{ filter: { isGlobalAdmin: null } }, ErrorMessage.InvalidSecret('filter')], + [{ filter: { fake: 'fake' } }, ErrorMessage.InvalidSecret('filter')] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + deleteTokensByAttribute(params) + ); + }); + + it('ignores already-deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + deleteTokensByAttribute({ filter: dummyRootData.auth[1].attributes }) + ).resolves.toBe(1); + + await expect( + deleteTokensByAttribute({ filter: dummyRootData.auth[1].attributes }) + ).resolves.toBe(0); + }); +}); + +describe('::deriveSchemeAndToken', () => { + it('handles schemes case-insensitively', async () => { + expect.hasAssertions(); + + const expected1 = await deriveSchemeAndToken({ authString: 'bearer 123' }); + + await expect( + deriveSchemeAndToken({ authString: 'bEaReR 123' }) + ).resolves.toStrictEqual(expected1); + + await expect( + deriveSchemeAndToken({ authString: 'BeaRer 123' }) + ).resolves.toStrictEqual(expected1); + + await expect( + deriveSchemeAndToken({ authString: 'BEARER 123' }) + ).resolves.toStrictEqual(expected1); + + const expected2 = await deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: '123' } } + }); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: '123' } } + }) + ).resolves.toStrictEqual(expected2); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: '123' } } + }) + ).resolves.toStrictEqual(expected2); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: '123' } } + }) + ).resolves.toStrictEqual(expected2); + }); + + it('handles bearer scheme with token', async () => { + expect.hasAssertions(); + + await expect( + deriveSchemeAndToken({ authString: 'bearer abc-123' }) + ).resolves.toStrictEqual({ + scheme: 'bearer', + token: { bearer: 'abc-123' } + }); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: 'abc-123' } } + }) + ).resolves.toStrictEqual({ + scheme: 'bearer', + token: { bearer: 'abc-123' } + }); + }); + + it('handles allowedSchemes as an AuthenticationScheme or an array of AuthSchemes', async () => { + expect.hasAssertions(); + + await expect( + deriveSchemeAndToken({ authString: 'bearer abc-123', allowedSchemes: 'bearer' }) + ).resolves.toStrictEqual({ + scheme: 'bearer', + token: { bearer: 'abc-123' } + }); + + await expect( + deriveSchemeAndToken({ + authData: { + scheme: 'bearer', + token: { bearer: 'abc-123' } + }, + allowedSchemes: ['bearer'] + }) + ).resolves.toStrictEqual({ + scheme: 'bearer', + token: { bearer: 'abc-123' } + }); + + // ? Unlike with authorizeHeader and its constraints, duplicate AuthSchemes + // ? are not a big deal here and so are not checked against. + await expect( + deriveSchemeAndToken({ + authData: { + scheme: 'bearer', + token: { bearer: 'abc-123' } + }, + allowedSchemes: ['bearer', 'bearer'] + }) + ).resolves.toStrictEqual({ + scheme: 'bearer', + token: { bearer: 'abc-123' } + }); + }); + + it('rejects bearer scheme with multipart token', async () => { + expect.hasAssertions(); + + await expect( + deriveSchemeAndToken({ + authString: 'bearer abc-123,\ndef-234,ghi-345,\n\njkl-456,mno-567' + }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('token syntax')); + + await expect( + deriveSchemeAndToken({ + authData: { + scheme: 'bearer', + token: { bearer: ['abc-123', 'def-234', 'ghi-345', 'jkl-456', 'mno-567'] } + } + }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('token syntax')); + }); + + it('rejects on missing and null data', async () => { + expect.hasAssertions(); + + await expect(deriveSchemeAndToken({ authString: '' })).rejects.toThrow( + ErrorMessage.InvalidSecret('auth string') + ); + + await expect(deriveSchemeAndToken({ authString: undefined })).rejects.toThrow( + ErrorMessage.InvalidSecret('invocation') + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await expect(deriveSchemeAndToken({ authString: null as any })).rejects.toThrow( + ErrorMessage.InvalidSecret('auth string') + ); + + await expect( + deriveSchemeAndToken({ authData: { scheme: 'bearer', token: { bearer: '' } } }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('token syntax')); + + await expect( + deriveSchemeAndToken({ authData: { scheme: 'bearer', token: { bearer: '' } } }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('token syntax')); + + await expect( + deriveSchemeAndToken({ authData: { scheme: '', token: { bearer: 'abc-123' } } }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deriveSchemeAndToken({ authData: { something: 'else' } as any }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + + await expect(deriveSchemeAndToken({ authData: undefined })).rejects.toThrow( + ErrorMessage.InvalidSecret('invocation') + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await expect(deriveSchemeAndToken({ authData: null as any })).rejects.toThrow( + ErrorMessage.InvalidSecret('auth data') + ); + }); + + it('rejects on badly formatted headers', async () => { + expect.hasAssertions(); + + await expect(deriveSchemeAndToken({ authString: 'bearer' })).rejects.toThrow( + ErrorMessage.InvalidSecret('auth string') + ); + + await expect( + deriveSchemeAndToken({ authString: 'bearer-bearer' }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('auth string')); + }); + + it('rejects on unknown schemes', async () => { + expect.hasAssertions(); + + await expect( + deriveSchemeAndToken({ authString: 'unknown xyz', allowedSchemes: 'bearer' }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'unknown', token: { bearer: 'xyz' } }, + allowedSchemes: 'bearer' + }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + }); + + it('rejects if using a disallowed scheme', async () => { + expect.hasAssertions(); + + await expect( + deriveSchemeAndToken({ + authString: 'bearer 123', + allowedSchemes: ['none' as unknown as AuthenticationScheme] + }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + + await expect( + deriveSchemeAndToken({ + authData: { scheme: 'bearer', token: { bearer: '123' } }, + allowedSchemes: 'none' as unknown as AuthenticationScheme + }) + ).rejects.toThrow(ErrorMessage.InvalidSecret('scheme (disallowed or unknown)')); + }); + + it('rejects if handler for scheme is mistakenly unimplemented', async () => { + expect.hasAssertions(); + + mutableAuthenticationSchemes.push('none'); + + await expect(deriveSchemeAndToken({ authString: 'none 123' })).rejects.toThrow( + 'auth string handler for scheme "none" is not implemented' + ); + + await expect( + deriveSchemeAndToken({ authData: { scheme: 'none', token: { bearer: '123' } } }) + ).rejects.toThrow('auth data handler for scheme "none" is not implemented'); + }); +}); + +describe('::getTokenById', () => { + it('returns tokens if bearer token exists in database', async () => { + expect.hasAssertions(); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[1]._id + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[1]._id + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[0]._id + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[0])); + }); + + // ? Rejecting banned tokens is handled at a different layer than validation + it('returns tokens even if bearer token is banned', async () => { + expect.hasAssertions(); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[2]._id + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[2])); + }); + + it('throws if bearer token does not exist in database', async () => { + expect.hasAssertions(); + + await expect( + getTokenById({ + auth_id: new ObjectId() + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('was not found') + }); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[1]._id + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await ( + await getAuthDb() + ).updateOne({ _id: dummyRootData.auth[1]._id }, { $set: { deleted: true } }); + + await expect( + getTokenById({ + auth_id: dummyRootData.auth[1]._id + }) + ).rejects.toMatchObject({ message: expect.stringContaining('was not found') }); + }); +}); + +describe('::getTokenByDerivation', () => { + it('returns token if bearer token exists in database', async () => { + expect.hasAssertions(); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DUMMY_BEARER_TOKEN } } + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DEV_BEARER_TOKEN } } + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[0])); + }); + + // ? Rejecting banned tokens is handled at a different layer than validation + it('returns token even if bearer token is banned', async () => { + expect.hasAssertions(); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: BANNED_BEARER_TOKEN } } + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[2])); + }); + + it('throws if bearer token does not exist in database', async () => { + expect.hasAssertions(); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: NULL_BEARER_TOKEN } } + }) + ).rejects.toMatchObject({ + message: expect.stringContaining('scheme and token combination') + }); + }); + + it('respects allowedSchemes parameter', async () => { + expect.hasAssertions(); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DUMMY_BEARER_TOKEN } } + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DUMMY_BEARER_TOKEN } }, + allowedSchemes: ['none' as AuthenticationScheme] + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidSecret('scheme (disallowed or unknown)') + }); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DUMMY_BEARER_TOKEN } } + }) + ).resolves.toStrictEqual(toPublicAuthEntry(dummyRootData.auth[1])); + + await ( + await getAuthDb() + ).updateOne({ _id: dummyRootData.auth[1]._id }, { $set: { deleted: true } }); + + await expect( + getTokenByDerivation({ + from: { scheme: 'bearer', token: { bearer: DUMMY_BEARER_TOKEN } } + }) + ).rejects.toMatchObject({ message: expect.stringContaining('combination') }); + }); +}); + +describe('::getTokensByAttribute', () => { + it('returns array of all auth entries with matching attributes', async () => { + expect.hasAssertions(); + + const entries = dummyRootData.auth.slice(0, -1); + const owners = entries.map((entry) => entry.attributes.owner); + + const newAuthEntry1: InternalAuthEntry = { + _id: new ObjectId(), + deleted: false, + attributes: { owner: owners[0] }, + scheme: 'bearer', + token: { bearer: '(new) ' + jest.requireActual('node:crypto').randomUUID() } + }; + + const newAuthEntry2: InternalAuthEntry = { + _id: new ObjectId(), + deleted: false, + attributes: { owner: owners[1] }, + scheme: 'bearer', + token: { bearer: '(new) ' + jest.requireActual('node:crypto').randomUUID() } + }; + + await expect( + getTokensByAttribute({ filter: { owner: owners } }) + ).resolves.toStrictEqual(entries.map((entry) => toPublicAuthEntry(entry))); + + await (await getAuthDb()).insertMany([newAuthEntry1, newAuthEntry2]); + + await expect( + getTokensByAttribute({ filter: { owner: owners } }) + ).resolves.toStrictEqual( + [...entries, newAuthEntry1, newAuthEntry2] + .sort((a, b) => objectIdPseudoSortPredicate('ascending')(a, b)) + .map((entry) => toPublicAuthEntry(entry)) + ); + }); + + // ? Rejecting banned tokens is handled at a different layer than validation + it('returns tokens even if bearer token is banned', async () => { + expect.hasAssertions(); + + await expect( + getTokensByAttribute({ filter: dummyRootData.auth[2].attributes }) + ).resolves.toStrictEqual([toPublicAuthEntry(dummyRootData.auth[2])]); + }); + + it('returns empty array if no matching attributes exist', async () => { + expect.hasAssertions(); + + await expect( + getTokensByAttribute({ filter: { owner: ['does-not-exist'] } }) + ).resolves.toStrictEqual([]); + + await expect( + getTokensByAttribute({ + filter: { owner: ['does-not-exist-1', 'does-not-exist-2'] } + }) + ).resolves.toStrictEqual([]); + + await expect( + getTokensByAttribute({ filter: { owner: [] } }) + ).resolves.toStrictEqual([]); + }); + + it('returns all auth entries if empty filter provided', async () => { + expect.hasAssertions(); + + await expect(getTokensByAttribute({ filter: {} })).resolves.toStrictEqual( + dummyRootData.auth.map((entry) => toPublicAuthEntry(entry)) + ); + }); + + it('respects pagination via after_id', async () => { + expect.hasAssertions(); + + await expect(getTokensByAttribute({ filter: {} })).resolves.toStrictEqual( + dummyRootData.auth.map((entry) => toPublicAuthEntry(entry)) + ); + + await expect( + getTokensByAttribute({ + filter: {}, + after_id: dummyRootData.auth[0]._id + }) + ).resolves.toStrictEqual( + dummyRootData.auth.slice(1).map((entry) => toPublicAuthEntry(entry)) + ); + + await expect( + getTokensByAttribute({ + filter: {}, + after_id: dummyRootData.auth.at(-1)?._id + }) + ).resolves.toStrictEqual([]); + }); + + it('respects config.resultsPerPage', async () => { + expect.hasAssertions(); + + await expect(getTokensByAttribute({ filter: {} })).resolves.toStrictEqual( + dummyRootData.auth.map((entry) => toPublicAuthEntry(entry)) + ); + + jest.spyOn(NextAuthConstants, 'getConfig').mockImplementation(() => { + return { + resultsPerPage: 1 + }; + }); + + await expect(getTokensByAttribute({ filter: {} })).resolves.toStrictEqual( + dummyRootData.auth.slice(0, 1).map((entry) => toPublicAuthEntry(entry)) + ); + + jest.spyOn(NextAuthConstants, 'getConfig').mockImplementation(() => { + return { + resultsPerPage: dummyRootData.auth.length - 1 + }; + }); + + await expect(getTokensByAttribute({ filter: {} })).resolves.toStrictEqual( + dummyRootData.auth.slice(0, -1).map((entry) => toPublicAuthEntry(entry)) + ); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + type Params = Parameters[0]; + const errors: [params: Params, error: string][] = [ + [ + { filter: { owner: [false] as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: { owner: [true] as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: { owner: true as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [{ filter: { owner: [''] } }, ErrorMessage.InvalidSecret('filter')], + [ + { filter: { owner: [5] as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: { owner: 5 as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: { owner: [null] as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: { owner: [undefined] as unknown as Params } }, + ErrorMessage.InvalidSecret('filter') + ], + [{ filter: { owners: ['owner-1'] } }, ErrorMessage.InvalidSecret('filter')], + [{ filter: { bad: 'not-good' } }, ErrorMessage.InvalidSecret('filter')] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + getTokensByAttribute(params) + ); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + getTokensByAttribute({ filter: dummyRootData.auth[1].attributes }) + ).resolves.toStrictEqual([toPublicAuthEntry(dummyRootData.auth[1])]); + + await ( + await getAuthDb() + ).updateOne({ _id: dummyRootData.auth[1]._id }, { $set: { deleted: true } }); + + await expect( + getTokensByAttribute({ filter: dummyRootData.auth[1].attributes }) + ).resolves.toStrictEqual([]); + }); +}); + +describe('::isAllowedScheme', () => { + it('returns true only if passed an AuthenticationScheme', async () => { + expect.hasAssertions(); + + expect(isAllowedScheme('bearer')).toBeTrue(); + expect(isAllowedScheme('nope')).toBeFalse(); + + mutableAuthenticationSchemes.push('nope'); + + expect(isAllowedScheme('nope')).toBeTrue(); + }); + + it('returns true only if passed an allowed AuthenticationScheme when using onlyAllowSubset', async () => { + expect.hasAssertions(); + + expect(isAllowedScheme('bearer')).toBeTrue(); + expect(isAllowedScheme('bearer', [])).toBeFalse(); + expect(isAllowedScheme('bearer', ['nope' as AuthenticationScheme])).toBeFalse(); + expect(isAllowedScheme('nope', ['nope' as AuthenticationScheme])).toBeTrue(); + expect(isAllowedScheme('nope', 'nope' as AuthenticationScheme)).toBeTrue(); + expect(isAllowedScheme('nope', 'bearer')).toBeFalse(); + }); +}); + +describe('::isTokenAttributes', () => { + it('returns true only if passed TokenAttributes', async () => { + expect.hasAssertions(); + + expect(isTokenAttributes(undefined)).toBeFalse(); + expect(isTokenAttributes(null)).toBeFalse(); + expect(isTokenAttributes(1)).toBeFalse(); + expect(isTokenAttributes('1')).toBeFalse(); + expect(isTokenAttributes({ owner: true })).toBeFalse(); + expect(isTokenAttributes({ owner: null })).toBeFalse(); + expect(isTokenAttributes({ owner: undefined })).toBeFalse(); + expect(isTokenAttributes({ owner: 'owner', isGlobalAdmin: 1 })).toBeFalse(); + expect(isTokenAttributes({ owner: 'owner', isGlobalAdmin: 'true' })).toBeFalse(); + expect(isTokenAttributes({ owner: 'owner' })).toBeTrue(); + expect(isTokenAttributes({ owner: 'owner', isGlobalAdmin: false })).toBeTrue(); + expect(isTokenAttributes({ isGlobalAdmin: false })).toBeFalse(); + }); + + it('returns true if passed partial TokenAttributes in patch mode', async () => { + expect.hasAssertions(); + + expect(isTokenAttributes(undefined, { partial: true })).toBeFalse(); + expect(isTokenAttributes(null, { partial: true })).toBeFalse(); + expect(isTokenAttributes(1, { partial: true })).toBeFalse(); + expect(isTokenAttributes('1', { partial: true })).toBeFalse(); + expect(isTokenAttributes({}, { partial: true })).toBeTrue(); + expect(isTokenAttributes({ owner: true }, { partial: true })).toBeFalse(); + expect(isTokenAttributes({ owner: null }, { partial: true })).toBeFalse(); + expect(isTokenAttributes({ owner: undefined }, { partial: true })).toBeTrue(); + expect(isTokenAttributes({ owner: '' })).toBeFalse(); + + expect( + isTokenAttributes({ owner: 'owner', isGlobalAdmin: 1 }, { partial: true }) + ).toBeFalse(); + + expect( + isTokenAttributes({ owner: 'owner', isGlobalAdmin: 'true' }, { partial: true }) + ).toBeFalse(); + + expect(isTokenAttributes({ owner: 'owner' }, { partial: true })).toBeTrue(); + + expect( + isTokenAttributes({ owner: 'owner', isGlobalAdmin: false }, { partial: true }) + ).toBeTrue(); + + expect(isTokenAttributes({ isGlobalAdmin: false }, { partial: true })).toBeTrue(); + }); + + it('returns false if passed a superset of TokenAttributes', async () => { + expect.hasAssertions(); + + expect(isTokenAttributes({ owner: 'owner', extra: 'prop' })).toBeFalse(); + + expect( + isTokenAttributes({ owner: 'owner', extra: 'prop' }, { partial: true }) + ).toBeFalse(); + }); + + it('returns false if passed an empty object', async () => { + expect.hasAssertions(); + expect(isTokenAttributes({})).toBeFalse(); + }); + + it('returns false if passed an empty string owner', async () => { + expect.hasAssertions(); + + expect(isTokenAttributes({ owner: '' })).toBeFalse(); + expect(isTokenAttributes({ owner: [''] })).toBeFalse(); + expect(isTokenAttributes({ owner: ['owner-1', ''] })).toBeFalse(); + }); +}); + +describe('::isTokenAttributesFilter', () => { + it('returns true only if passed TokenAttributesFilter', async () => { + expect.hasAssertions(); + + expect(isTokenAttributesFilter(undefined)).toBeFalse(); + expect(isTokenAttributesFilter(null)).toBeFalse(); + expect(isTokenAttributesFilter(1)).toBeFalse(); + expect(isTokenAttributesFilter('1')).toBeFalse(); + expect(isTokenAttributesFilter({ owner: true })).toBeFalse(); + expect(isTokenAttributesFilter({ owner: null })).toBeFalse(); + expect(isTokenAttributesFilter({ owner: undefined })).toBeFalse(); + expect(isTokenAttributesFilter({ owner: 'owner', isGlobalAdmin: 1 })).toBeFalse(); + + expect( + isTokenAttributesFilter({ owner: 'owner', isGlobalAdmin: 'true' }) + ).toBeFalse(); + + expect(isTokenAttributesFilter({ owner: 'owner' })).toBeTrue(); + + expect( + isTokenAttributesFilter({ owner: 'owner', isGlobalAdmin: false }) + ).toBeTrue(); + + expect(isTokenAttributesFilter({ isGlobalAdmin: false })).toBeTrue(); + + expect( + isTokenAttributesFilter({ owner: ['owner'], isGlobalAdmin: false }) + ).toBeTrue(); + + expect( + isTokenAttributesFilter({ owner: ['owner-1', 'owner-2'], isGlobalAdmin: false }) + ).toBeTrue(); + }); + + it('returns false if passed a superset of TokenAttributesFilter', async () => { + expect.hasAssertions(); + expect(isTokenAttributesFilter({ owner: ['owner'], extra: 'prop' })).toBeFalse(); + }); + + it('returns true if passed an empty object', async () => { + expect.hasAssertions(); + expect(isTokenAttributesFilter({})).toBeTrue(); + }); + + it('returns false if passed an empty string owner', async () => { + expect.hasAssertions(); + + expect(isTokenAttributesFilter({ owner: '' })).toBeFalse(); + expect(isTokenAttributesFilter({ owner: [''] })).toBeFalse(); + expect(isTokenAttributesFilter({ owner: ['owner-1', ''] })).toBeFalse(); + }); +}); + +describe('::updateTokenAttributesById', () => { + it('updates (patches) an existing auth entry', async () => { + expect.hasAssertions(); + + const authDb = await getAuthDb(); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: { isGlobalAdmin: false } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[0]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[0], + attributes: { ...dummyRootData.auth[0].attributes, isGlobalAdmin: false } + }); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[1]._id, + update: { owner: 'name' } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[1]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[1], + attributes: { ...dummyRootData.auth[1].attributes, owner: 'name' } + }); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: { owner: 'name', isGlobalAdmin: true } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[0]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[0], + attributes: { + ...dummyRootData.auth[0].attributes, + owner: 'name', + isGlobalAdmin: true + } + }); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[1]._id, + update: { isGlobalAdmin: true } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[1]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[1], + attributes: { + ...dummyRootData.auth[1].attributes, + owner: 'name', + isGlobalAdmin: true + } + }); + }); + + it('allows empty update (no-op)', async () => { + expect.hasAssertions(); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: {} + }) + ).resolves.toBe(0); + + await expect((await getAuthDb()).find().toArray()).resolves.toStrictEqual( + dummyRootData.auth + ); + }); + + it('returns 0 when demonstrating idempotency', async () => { + expect.hasAssertions(); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: dummyRootData.auth[0].attributes + }) + ).resolves.toBe(0); + }); + + it('returns 0 updated count if auth_id not found', async () => { + expect.hasAssertions(); + + await expect( + updateTokenAttributesById({ + auth_id: new ObjectId(), + update: { isGlobalAdmin: false } + }) + ).resolves.toBe(0); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + const auth_id = new ObjectId(); + + const errors: [ + params: Parameters[0], + error: string + ][] = [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [{} as any, ErrorMessage.InvalidSecret('update')], + [{ auth_id: undefined, update: {} }, ErrorMessage.InvalidObjectId('undefined')], + [{ auth_id: null, update: {} }, ErrorMessage.InvalidObjectId(null)], + [{ auth_id: false, update: {} }, ErrorMessage.InvalidObjectId(false)], + [{ auth_id: true, update: {} }, ErrorMessage.InvalidObjectId(true)], + [{ auth_id: {}, update: {} }, ErrorMessage.InvalidObjectId({})], + [{ auth_id: 'xyz123', update: {} }, ErrorMessage.InvalidObjectId('xyz123')], + + [{ auth_id, update: undefined }, ErrorMessage.InvalidSecret('update')], + [{ auth_id, update: null }, ErrorMessage.InvalidSecret('update')], + [{ auth_id, update: false }, ErrorMessage.InvalidSecret('update')], + [{ auth_id, update: true }, ErrorMessage.InvalidSecret('update')], + [{ auth_id, update: { owner: '' } }, ErrorMessage.InvalidSecret('update')], + [ + { auth_id, update: { isGlobalAdmin: 5 } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { auth_id, update: { owners: ['prop'] } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { auth_id, update: { owner: ['array-form-is-only-for-filter'] } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + auth_id, + update: { isGlobalAdmin: null } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + auth_id, + update: { isGlobalAdmin: 1 } + }, + ErrorMessage.InvalidSecret('update') + ], + [{ auth_id, update: { name: 'owner' } }, ErrorMessage.InvalidSecret('update')], + [{ auth_id, update: { owner: null } }, ErrorMessage.InvalidSecret('update')], + [ + { + auth_id, + update: { + owner: 'name', + isGlobalAdmin: 1 + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + auth_id, + update: { + owner: 'name', + isGlobalAdmin: null + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + auth_id, + update: { + owner: 'name', + isGlobalAdmin: 'true' + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + auth_id, + update: { + owner: 'name', + extra: 1 + } + }, + ErrorMessage.InvalidSecret('update') + ] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + updateTokenAttributesById(params) + ); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: { owner: 'xyz123' } + }) + ).resolves.toBe(1); + + await (await getAuthDb()).updateMany({}, { $set: { deleted: true } }); + + await expect( + updateTokenAttributesById({ + auth_id: dummyRootData.auth[0]._id, + update: { owner: 'abc987' } + }) + ).resolves.toBe(0); + }); +}); + +describe('::updateTokensAttributesByAttribute', () => { + it('updates (patches) an existing auth entry', async () => { + expect.hasAssertions(); + + const authDb = await getAuthDb(); + + await expect( + updateTokensAttributesByAttribute({ + filter: dummyRootData.auth[0].attributes, + update: { isGlobalAdmin: false } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[0]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[0], + attributes: { ...dummyRootData.auth[0].attributes, isGlobalAdmin: false } + }); + + await expect( + updateTokensAttributesByAttribute({ + filter: dummyRootData.auth[1].attributes, + update: { owner: 'name' } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[1]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[1], + attributes: { ...dummyRootData.auth[1].attributes, owner: 'name' } + }); + + await expect( + updateTokensAttributesByAttribute({ + filter: { ...dummyRootData.auth[0].attributes, isGlobalAdmin: false }, + update: { owner: 'name', isGlobalAdmin: true } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[0]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[0], + attributes: { + ...dummyRootData.auth[0].attributes, + owner: 'name', + isGlobalAdmin: true + } + }); + + await expect( + updateTokensAttributesByAttribute({ + filter: { ...dummyRootData.auth[1].attributes, owner: 'name' }, + update: { isGlobalAdmin: true } + }) + ).resolves.toBe(1); + + await expect( + authDb.findOne({ _id: dummyRootData.auth[1]._id }) + ).resolves.toStrictEqual({ + ...dummyRootData.auth[1], + attributes: { + ...dummyRootData.auth[1].attributes, + owner: 'name', + isGlobalAdmin: true + } + }); + }); + + it('updates (patches) multiple existing auth entries', async () => { + expect.hasAssertions(); + + const authDb = await getAuthDb(); + + await expect( + updateTokensAttributesByAttribute({ + filter: { + owner: dummyRootData.auth + .slice(0, -1) + .map((entry) => entry.attributes.owner) + }, + update: { owner: 'xyz123' } + }) + ).resolves.toBe(dummyRootData.auth.length - 1); + + await expect(authDb.find().toArray()).resolves.toStrictEqual( + dummyRootData.auth + .slice(0, -1) + .map((entry) => { + return { ...entry, attributes: { ...entry.attributes, owner: 'xyz123' } }; + }) + .concat(dummyRootData.auth.at(-1)!) + ); + + await expect( + updateTokensAttributesByAttribute({ + filter: { owner: 'xyz123' }, + update: { isGlobalAdmin: true } + }) + ).resolves.toBe( + // ? An extra -1 because one already has isGlobalAdmin === true + dummyRootData.auth.length - 2 + ); + + await expect(authDb.find().toArray()).resolves.toStrictEqual( + dummyRootData.auth + .slice(0, -1) + .map((entry) => { + return { + ...entry, + attributes: { + ...entry.attributes, + owner: 'xyz123', + isGlobalAdmin: true + } as TokenAttributes + }; + }) + .concat(dummyRootData.auth.at(-1)!) + ); + + await expect( + updateTokensAttributesByAttribute({ + filter: {}, + update: { isGlobalAdmin: false } + }) + ).resolves.toBe(dummyRootData.auth.length); + + await expect(authDb.find().toArray()).resolves.toStrictEqual( + dummyRootData.auth + .slice(0, -1) + .map((entry) => { + return { + ...entry, + attributes: { + ...entry.attributes, + owner: 'xyz123', + isGlobalAdmin: false + } as TokenAttributes + }; + }) + .concat({ + ...dummyRootData.auth.at(-1)!, + attributes: { + ...dummyRootData.auth.at(-1)!.attributes, + isGlobalAdmin: false + } + }) + ); + }); + + it('allows empty update (no-op)', async () => { + expect.hasAssertions(); + + await expect( + updateTokensAttributesByAttribute({ + filter: dummyRootData.auth[0].attributes, + update: {} + }) + ).resolves.toBe(0); + + await expect((await getAuthDb()).find().toArray()).resolves.toStrictEqual( + dummyRootData.auth + ); + }); + + it('allows empty filter (updates all entries)', async () => { + expect.hasAssertions(); + + await expect( + updateTokensAttributesByAttribute({ + filter: {}, + update: { owner: 'xyz123' } + }) + ).resolves.toBe(dummyRootData.auth.length); + + await expect((await getAuthDb()).find().toArray()).resolves.toStrictEqual( + dummyRootData.auth.map((entry) => { + return { ...entry, attributes: { ...entry.attributes, owner: 'xyz123' } }; + }) + ); + }); + + it('returns 0 updated count when demonstrating idempotency', async () => { + expect.hasAssertions(); + + await expect( + updateTokensAttributesByAttribute({ + filter: dummyRootData.auth[0].attributes, + update: dummyRootData.auth[0].attributes + }) + ).resolves.toBe(0); + }); + + it('returns 0 updated count if the auth entry is not found', async () => { + expect.hasAssertions(); + + await expect( + updateTokensAttributesByAttribute({ + filter: { owner: 'does-not-exist' }, + update: { isGlobalAdmin: false } + }) + ).resolves.toBe(0); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + + const errors: [ + params: Parameters[0], + error: string + ][] = [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [{} as any, ErrorMessage.InvalidSecret('filter')], + [{ filter: undefined, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: undefined }, ErrorMessage.InvalidSecret('update')], + [{ filter: null, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: null }, ErrorMessage.InvalidSecret('update')], + [{ filter: false, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: false }, ErrorMessage.InvalidSecret('update')], + [{ filter: true, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: true }, ErrorMessage.InvalidSecret('update')], + [{ filter: { owner: '' }, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: { owner: '' } }, ErrorMessage.InvalidSecret('update')], + [ + { filter: { isGlobalAdmin: 5 }, update: {} }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: {}, update: { isGlobalAdmin: 5 } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { filter: { owners: ['prop'] }, update: {} }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: {}, update: { owners: ['prop'] } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { filter: {}, update: { owner: ['array-form-is-only-for-filter'] } }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + filter: { isGlobalAdmin: null }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { isGlobalAdmin: null } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + filter: { isGlobalAdmin: 1 }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { isGlobalAdmin: 1 } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { filter: { name: 'owner' }, update: {} }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { filter: {}, update: { name: 'owner' } }, + ErrorMessage.InvalidSecret('update') + ], + [{ filter: { owner: null }, update: {} }, ErrorMessage.InvalidSecret('filter')], + [{ filter: {}, update: { owner: null } }, ErrorMessage.InvalidSecret('update')], + [ + { + filter: { + owner: 'name', + isGlobalAdmin: 1 + }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { + owner: 'name', + isGlobalAdmin: 1 + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + filter: { + owner: 'name', + isGlobalAdmin: null + }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { + owner: 'name', + isGlobalAdmin: null + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + filter: { + owner: 'name', + isGlobalAdmin: 'true' + }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { + owner: 'name', + isGlobalAdmin: 'true' + } + }, + ErrorMessage.InvalidSecret('update') + ], + [ + { + filter: { + owner: 'name', + extra: 1 + }, + update: {} + }, + ErrorMessage.InvalidSecret('filter') + ], + [ + { + filter: {}, + update: { + owner: 'name', + extra: 1 + } + }, + ErrorMessage.InvalidSecret('update') + ] + ]; + + await expectExceptionsWithMatchingErrors(errors, (params) => + updateTokensAttributesByAttribute(params) + ); + }); + + it('ignores deleted auth entries', async () => { + expect.hasAssertions(); + + await expect( + updateTokensAttributesByAttribute({ + filter: dummyRootData.auth[0].attributes, + update: { owner: 'xyz123' } + }) + ).resolves.toBe(1); + + await (await getAuthDb()).updateMany({}, { $set: { deleted: true } }); + + await expect( + updateTokensAttributesByAttribute({ + filter: { ...dummyRootData.auth[0].attributes, owner: 'xyz123' }, + update: { owner: 'abc987' } + }) + ).resolves.toBe(0); + + await expect( + updateTokensAttributesByAttribute({ + filter: {}, + update: { owner: 'abc987' } + }) + ).resolves.toBe(0); + }); +}); diff --git a/lib/next-auth/token.ts b/lib/next-auth/token.ts new file mode 100644 index 0000000..0a6653d --- /dev/null +++ b/lib/next-auth/token.ts @@ -0,0 +1,665 @@ +import { randomUUID as generateUUID } from 'node:crypto'; +import { MongoServerError, ObjectId } from 'mongodb'; + +import { + AppValidationError, + InvalidSecretError, + ItemNotFoundError +} from 'named-app-errors'; + +import { getEnv } from 'multiverse/next-env'; +import { debugFactory } from 'multiverse/debug-extended'; +import { itemToObjectId } from 'multiverse/mongo-item'; + +import { + validAuthenticationSchemes, + type AuthenticationScheme +} from './authenticate'; + +import { + type InternalAuthBearerEntry, + type NewAuthEntry, + type PublicAuthEntry, + getAuthDb, + isNewAuthEntry, + publicAuthEntryProjection, + toPublicAuthEntry +} from './db'; + +// ? Used by a comment eslint-disable-next-line +// @typescript-eslint/no-unused-vars +import { getConfig } from './constants'; + +import type { JsonObject, JsonValue } from 'type-fest'; +import type { LiteralUnknownUnion } from 'types/global'; + +const debug = debugFactory('next-auth:token'); + +/** + * The shape of the actual token and scheme data contained within an entry in + * the well-known "auth" collection. + */ +export type Token = { + /** + * The authentication scheme this token supports. + */ + scheme: AuthenticationScheme; + /** + * The actual token. + */ + token: JsonObject; +}; + +/** + * The potential token/scheme of one or more entries in the well-known "auth" + * collection. + */ +export type TokenFilter = Partial<{ + /** + * The authentication scheme of the target token(s). + */ + scheme: string; + /** + * The target token(s). + */ + token: Record; +}>; + +/** + * An array of allowed "auth" full token entry attributes. Each array element + * must correspond to a field in the {@link TokenAttributes} type and + * vice-versa. + */ +export const validTokenAttributes = ['owner', 'isGlobalAdmin'] as const; + +/** + * A supported "auth" full token entry attribute (i.e. a field/property name as + * a string) associated with a specific token and scheme. + */ +export type TokenAttribute = (typeof validTokenAttributes)[number]; + +/** + * The shape of the attributes corresponding to a full token entry in the + * well-known "auth" collection. Each property must correspond to an array + * element in the {@link validTokenAttributes} array and vice-versa. + */ +// ! `owner` must be the only required property. All others must be optional. +export type TokenAttributes = { + /** + * A string (or stringified `ObjectId`) representing the owner of the token. + */ + owner: string; + /** + * If `true`, the token grants access to potentially dangerous abilities via + * the well-known "/sys" API endpoint. + * + * @default undefined + */ + isGlobalAdmin?: boolean; +}; + +/** + * The shape of a filter used to search through the well-known "auth" + * collection. + */ +export type TokenAttributesFilter = Partial<{ + /** + * As a string, this represents the target _owner_ of the target token. As an + * array, this represents the target _owners_ of the target tokens, any of + * which could be returned. + */ + owner: string | string[]; + /** + * The target global administrator status of the target token(s). + */ + isGlobalAdmin: boolean; +}>; + +/** + * The shape of a bearer token object. + */ +export type BearerToken = { + /** + * The authentication scheme this token supports. + */ + scheme: 'bearer'; + /** + * The bearer token. + */ + token: { + bearer: string; + }; +}; + +/** + * Transforms `filter`, the token attributes filter, into a MongoDb update + * filter with equivalent meaning. + */ +function tokenAttributesFilterToMongoFilter(filter: TokenAttributesFilter) { + return { + // TODO: Queries on owner are covered by the index. Maybe others + // TODO: should too? + ...(filter.owner !== undefined + ? { 'attributes.owner': { $in: [filter.owner].flat() } } + : {}), + ...(filter.isGlobalAdmin !== undefined + ? { 'attributes.isGlobalAdmin': filter.isGlobalAdmin } + : {}), + deleted: false + }; +} + +/** + * Transforms `update`, a patch to update {@link TokenAttributes} in the + * MongoDb "auth" collection, into a valid MongoDb update expression. + */ +function tokenAttributesUpdateToMongoUpdate(update: TokenAttributes) { + return { + $set: { + ...(update.owner !== undefined ? { 'attributes.owner': update.owner } : {}), + ...(update.isGlobalAdmin !== undefined + ? { 'attributes.isGlobalAdmin': update.isGlobalAdmin } + : {}) + } + }; +} + +/** + * Type guard that returns `true` if `obj` satisfies the + * {@link AuthenticationScheme} interface. Additional constraints may be + * enforced such that `obj` is among a _subset_ of allowable schemes via the + * `onlyAllowSubset` parameter. + */ +export function isAllowedScheme( + obj: unknown, + onlyAllowSubset?: AuthenticationScheme | AuthenticationScheme[] +): obj is AuthenticationScheme { + return !![onlyAllowSubset || validAuthenticationSchemes] + .flat() + .includes(obj as AuthenticationScheme); +} + +/** + * Type guard that returns `true` if `obj` satisfies the {@link TokenAttributes} + * interface. + */ +export function isTokenAttributes( + obj: unknown, + { partial = false } = {} +): obj is TokenAttributes { + const attribute = obj as TokenAttributes; + let returnValue = false; + + if (!!attribute && typeof attribute === 'object') { + const isValidOwner = !!attribute.owner && typeof attribute.owner === 'string'; + + const isValidGlobalAdmin = + attribute.isGlobalAdmin === undefined || + typeof attribute.isGlobalAdmin === 'boolean'; + + const allKeysAreValid = Object.keys(attribute).every((key) => + validTokenAttributes.includes(key as TokenAttribute) + ); + + if (allKeysAreValid) { + returnValue = true; + + // eslint-disable-next-line unicorn/prefer-ternary + if (partial) { + returnValue &&= attribute.owner === undefined || isValidOwner; + } else { + returnValue &&= isValidOwner; + } + + returnValue &&= isValidGlobalAdmin; + } + } + + return returnValue; +} + +/** + * Type guard that returns `true` if `obj` satisfies the + * {@link TokenAttributesFilter} interface. + */ +export function isTokenAttributesFilter(obj: unknown): obj is TokenAttributesFilter { + if (!obj || typeof obj !== 'object') { + return false; + } + + if ('owner' in obj) { + const ownerIsNotString = typeof obj.owner !== 'string'; + const ownerIsNotProperArray = + !Array.isArray(obj.owner) || + !obj.owner.every((owner) => isTokenAttributes({ owner })); + + if (ownerIsNotProperArray && (ownerIsNotString || obj.owner === '')) { + return false; + } + } + + if ('isGlobalAdmin' in obj && typeof obj.isGlobalAdmin !== 'boolean') { + return false; + } + + const allKeysAreValid = Object.keys(obj).every((key) => + validTokenAttributes.includes(key as TokenAttribute) + ); + + if (!allKeysAreValid) { + return false; + } + + return true; +} + +/** + * Derives a token and scheme from an authentication string (such as an + * Authorization header). **Does not check the database for token existence**. + * Throws on invalid/missing authentication string. + * + * Throws {@link InvalidSecretError} if invalid/missing data is provided. + */ +export async function deriveSchemeAndToken({ + authString, + allowedSchemes +}: { + /** + * The authentication string used to derive a token and scheme. + */ + authString?: string | undefined; + /** + * Accepted authentication schemes. By default, all schemes are accepted. + */ + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; +}): Promise; +/** + * Returns the token and scheme passed via `authData` if the token and scheme + * are valid. **Does not check the database for token existence**. Throws on + * invalid/missing token/scheme. + */ +export async function deriveSchemeAndToken({ + authData, + allowedSchemes +}: { + /** + * The data that will be verified and returned as-is. + */ + authData?: TokenFilter; + /** + * Accepted authentication schemes. By default, all schemes are accepted. + */ + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; +}): Promise; +export async function deriveSchemeAndToken({ + authString, + authData, + allowedSchemes +}: { + /** + * The authentication string used to derive a token and scheme. + */ + authString?: string | undefined; + /** + * The parameters used to derive a token and scheme. + */ + authData?: TokenFilter; + /** + * Accepted authentication schemes. By default, all schemes are accepted. + */ + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; +}): Promise { + if (authString !== undefined) { + if ( + !authString || + typeof authString !== 'string' || + !/^\S+ \S/.test(authString) || + authString.length > getEnv().AUTH_HEADER_MAX_LENGTH + ) { + throw new InvalidSecretError('auth string'); + } + + let scheme: AuthenticationScheme; + const [rawScheme, ...rawCredentials] = authString.split(/\s/gi); + const maybeScheme = rawScheme.toLowerCase(); + + debug(`deriving token of scheme "${maybeScheme}" from auth string`); + + if (isAllowedScheme(maybeScheme, allowedSchemes)) { + scheme = maybeScheme; + } else { + throw new InvalidSecretError('scheme (disallowed or unknown)'); + } + + const credentials = rawCredentials.flatMap((c) => c.split(',')).filter(Boolean); + + if (scheme === 'bearer') { + if (credentials.length === 1) { + return { scheme, token: { bearer: credentials[0] } }; + } else { + throw new InvalidSecretError('token syntax'); + } + } /*else if(scheme === '...') { + ... + }*/ else { + throw new AppValidationError( + `auth string handler for scheme "${scheme}" is not implemented` + ); + } + } else if (authData !== undefined) { + if (!authData || typeof authData !== 'object') { + throw new InvalidSecretError('auth data'); + } + + let scheme: AuthenticationScheme; + const maybeScheme = authData.scheme?.toLowerCase(); + + debug(`deriving token of scheme "${maybeScheme}" from auth data`); + + if (isAllowedScheme(maybeScheme, allowedSchemes)) { + scheme = maybeScheme; + } else { + throw new InvalidSecretError('scheme (disallowed or unknown)'); + } + + if (scheme === 'bearer') { + if ( + authData.token && + typeof authData.token === 'object' && + Object.keys(authData.token).length === 1 && + authData.token.bearer && + typeof authData.token.bearer === 'string' + ) { + return { scheme, token: { bearer: authData.token.bearer } }; + } else { + throw new InvalidSecretError('token syntax'); + } + } /*else if(scheme === '...') { + ... + }*/ else { + throw new AppValidationError( + `auth data handler for scheme "${scheme}" is not implemented` + ); + } + } else { + throw new InvalidSecretError('invocation'); + } +} + +/** + * Generates a new full token entry in the well-known "auth" MongoDB collection, + * including the provided attribute and scheme metadata. Throws on invalid entry + * data. + * + * The current version of this function uses the `bearer` scheme to create v4 + * UUID "bearer tokens". This _implementation detail_ may change at any time. + */ +export async function createToken({ + data +}: { + /** + * Data used to generate a new "auth" entry. + */ + data: LiteralUnknownUnion; +}): Promise { + if (isNewAuthEntry(data)) { + const newToken: InternalAuthBearerEntry = { + _id: new ObjectId(), + deleted: false, + attributes: data.attributes, + scheme: 'bearer', + // ! Due to how MongoDB works, it is EXTREMELY important that new entries' + // ! token object properties are ALWAYS in an consistent, expected order. + // ! This only matters when entry.token has more than one property. + token: { bearer: generateUUID() } + }; + + try { + await (await getAuthDb()).insertOne({ ...newToken }); + } catch (error) { + throw error instanceof MongoServerError && error.code === 11_000 + ? new AppValidationError('token collision') + : error; + } + + return toPublicAuthEntry(newToken); + } else { + throw new InvalidSecretError('token data'); + } +} + +/** + * Returns the full token entry (`token`, `scheme`, and `attributes`) + * corresponding to the given `_id` (`auth_id`) in the well-known "auth" MongoDB + * collection. + * + * Throws if invalid/missing data is provided. + */ +export async function getTokenById({ + auth_id +}: { + /** + * The {@link ObjectId} of the token in the well-known "auth" MongoDb + * collection. Throws if `auth_id` cannot be coerced into an {@link ObjectId}. + */ + auth_id: string | ObjectId | undefined; +}): Promise { + const fullToken = await ( + await getAuthDb() + ).findOne( + { _id: itemToObjectId(auth_id), deleted: false }, + { projection: publicAuthEntryProjection } + ); + + if (fullToken === null) { + throw new ItemNotFoundError(auth_id, 'full token entry'); + } + + return fullToken; +} + +/** + * Returns the full token entry (`token`, `scheme`, and `attributes`) in the + * well-known "auth" MongoDB collection that matches the given token and scheme + * provided via `from`. + * + * Throws {@link InvalidSecretError} if invalid/missing data is provided. + */ +export async function getTokenByDerivation({ + from, + allowedSchemes +}: { + /** + * If `from` is an object, it will be passed as `authData` to + * {@link deriveSchemeAndToken}. Otherwise, if `from` is a string, it will be + * passed as `authString` to {@link deriveSchemeAndToken}. + */ + from: Token | string | undefined; + /** + * Accepted authentication schemes. By default, all schemes are accepted. + */ + allowedSchemes?: AuthenticationScheme | AuthenticationScheme[]; +}): Promise { + const { scheme, token } = + typeof from === 'string' + ? await deriveSchemeAndToken({ authString: from, allowedSchemes }) + : await deriveSchemeAndToken({ authData: from, allowedSchemes }); + + const fullToken = await ( + await getAuthDb() + ).findOne( + // ? To hit the index, order matters + { scheme, token, deleted: false }, + { projection: publicAuthEntryProjection } + ); + + if (fullToken === null) { + throw new InvalidSecretError('authentication scheme and token combination'); + } + + return fullToken; +} + +/** + * Returns at most `resultsPerPage` (from {@link getConfig}) full token entries + * (`token`, `scheme`, and `attributes`) with matching attributes in the + * well-known "auth" MongoDB collection. + */ +export async function getTokensByAttribute({ + filter, + after_id +}: { + /** + * The token attributes used to filter and return tokens. + */ + filter: LiteralUnknownUnion; + /** + * Only tokens with an `auth_id` after (less than) `after_id` will be returned. + */ + after_id?: string | ObjectId | undefined; +}): Promise { + if (isTokenAttributesFilter(filter)) { + const returnAll = Object.keys(filter).length === 0; + + return (await getAuthDb()) + .find( + // eslint-disable-next-line unicorn/no-array-callback-reference + Object.assign( + { deleted: false }, + after_id ? { _id: { $gt: itemToObjectId(after_id) } } : {}, + returnAll ? {} : tokenAttributesFilterToMongoFilter(filter) + ), + // eslint-disable-next-line unicorn/no-array-method-this-argument + { + projection: publicAuthEntryProjection, + sort: { _id: 1 }, + limit: getConfig().resultsPerPage + } + ) + .toArray(); + } else { + throw new InvalidSecretError('filter'); + } +} + +/** + * Updates a token's attributes by matching the provided data against the + * well-known "auth" MongoDB collection. Throws on invalid/missing target or + * entry data. + * + * **Note that the new `attributes` object will _patch_, not replace, the old + * object.** + */ +export async function updateTokenAttributesById({ + auth_id, + update +}: { + /** + * The {@link ObjectId} of the token in the well-known "auth" MongoDb + * collection. Throws if `auth_id` cannot be coerced into an {@link ObjectId}. + */ + auth_id: string | ObjectId | undefined; + /** + * The object used to patch the token's attributes. + */ + update: LiteralUnknownUnion; +}): Promise { + if (isTokenAttributes(update, { partial: true })) { + const { modifiedCount } = await ( + await getAuthDb() + ).updateOne( + { _id: itemToObjectId(auth_id), deleted: false }, + tokenAttributesUpdateToMongoUpdate(update) + ); + + return modifiedCount; + } else { + throw new InvalidSecretError('update'); + } +} + +/** + * Updates all tokens with matching attributes in the well-known "auth" MongoDB + * collection. + */ +export async function updateTokensAttributesByAttribute({ + filter, + update +}: { + /** + * The token attributes used to filter and update tokens. + */ + filter: LiteralUnknownUnion; + /** + * The object used to patch the tokens' attributes. + */ + update: LiteralUnknownUnion; +}): Promise { + if (isTokenAttributesFilter(filter)) { + if (isTokenAttributes(update, { partial: true })) { + if (Object.keys(update).length) { + const { modifiedCount } = await ( + await getAuthDb() + ).updateMany( + tokenAttributesFilterToMongoFilter(filter), + tokenAttributesUpdateToMongoUpdate(update) + ); + + return modifiedCount; + } else { + return 0; + } + } else { + throw new InvalidSecretError('update'); + } + } else { + throw new InvalidSecretError('filter'); + } +} + +/** + * Deletes a full token entry by its `auth_id` from the well-known "auth" + * MongoDB collection. + * + * Deleted tokens remain in the system but in a deactivated state. They cannot + * be reactivated or otherwise interacted with after they are + * deleted/deactivated. + */ +export async function deleteTokenById({ + auth_id +}: { + auth_id: string | ObjectId | undefined; +}): Promise { + const { modifiedCount } = await ( + await getAuthDb() + ).updateOne({ _id: itemToObjectId(auth_id) }, { $set: { deleted: true } }); + + return modifiedCount; +} + +/** + * Deletes all full token entries with matching attributes in the well-known + * "auth" MongoDB collection. Throws if an attempt is made to delete entries + * with an empty filter. + * + * Deleted tokens remain in the system but in a deactivated state. They cannot + * be reactivated or otherwise interacted with after they are + * deleted/deactivated. + */ +export async function deleteTokensByAttribute({ + filter +}: { + filter: LiteralUnknownUnion; +}): Promise { + if (isTokenAttributesFilter(filter) && Object.keys(filter).length) { + const { modifiedCount } = await ( + await getAuthDb() + ).updateMany(tokenAttributesFilterToMongoFilter(filter), { + $set: { deleted: true } + }); + + return modifiedCount; + } + + throw new InvalidSecretError('filter'); +} diff --git a/lib/next-contrived/index.ts b/lib/next-contrived/index.ts new file mode 100644 index 0000000..c8e4bd4 --- /dev/null +++ b/lib/next-contrived/index.ts @@ -0,0 +1,33 @@ +import { getEnv } from 'multiverse/next-env'; +import { debugFactory } from 'multiverse/debug-extended'; +import { getDb } from 'multiverse/mongo-schema'; + +const debug = debugFactory('next-contrived:isDueForContrivedError'); + +/** + * Returns `true` if a request should be rejected with a pseudo-error. + * + * Note that this is a per-serverless-function request counter and not global + * across all Vercel virtual machines. + */ +export async function isDueForContrivedError() { + const { REQUESTS_PER_CONTRIVED_ERROR: reqPerError } = getEnv(); + + if (reqPerError) { + const x = (await getDb({ name: 'root' })).collection('request-log'); + const count = await x.estimatedDocumentCount(); + + debug(`${count}%${reqPerError} = ${count % reqPerError}`); + + if (count % reqPerError === 0) { + debug('determined request is due for contrived error'); + return true; + } + } else { + debug( + `skipped contrived error check (cause: REQUESTS_PER_CONTRIVED_ERROR=${reqPerError})` + ); + } + + return false; +} diff --git a/lib/next-contrived/package.json b/lib/next-contrived/package.json new file mode 100644 index 0000000..7d1b5b9 --- /dev/null +++ b/lib/next-contrived/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-contrived" +} diff --git a/lib/next-contrived/unit.test.ts b/lib/next-contrived/unit.test.ts new file mode 100644 index 0000000..10c8f69 --- /dev/null +++ b/lib/next-contrived/unit.test.ts @@ -0,0 +1,139 @@ +import { dummyRootData, useMockDateNow } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { isDueForContrivedError } from 'multiverse/next-contrived'; +import { mockEnvFactory } from 'testverse/setup'; + +setupMemoryServerOverride(); +useMockDateNow(); + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); +const { _id, ...entry } = dummyRootData['request-log'][0]; + +beforeEach(async () => { + await (await getDb({ name: 'root' })).collection('request-log').deleteMany({}); +}); + +describe('::isDueForContrivedError', () => { + it('returns true every 1st call', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '1' } + ); + }); + + it('returns true every 2nd call', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '2' } + ); + }); + + it('returns true every 3rd call', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '3' } + ); + }); + + it('returns true every 4th call', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '4' } + ); + }); + + it('returns true every 5th call', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeTrue(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeTrue(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '5' } + ); + }); + + it('middleware disabled when REQUESTS_PER_CONTRIVED_ERROR=0', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('request-log'); + + await withMockedEnv( + async () => { + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + await db.insertOne({ ...entry }); + await expect(isDueForContrivedError()).resolves.toBeFalse(); + }, + { REQUESTS_PER_CONTRIVED_ERROR: '0' } + ); + }); +}); diff --git a/lib/next-env/index.ts b/lib/next-env/index.ts new file mode 100644 index 0000000..9d533d1 --- /dev/null +++ b/lib/next-env/index.ts @@ -0,0 +1,184 @@ +import { parse as parseAsBytes } from 'bytes'; +import { isServer } from 'is-server-side'; +import { InvalidAppEnvironmentError } from 'named-app-errors'; +import { toss } from 'toss-expression'; +import { validHttpMethods } from '@xunnamius/types'; +import { debugFactory } from 'multiverse/debug-extended'; + +import type { ValidHttpMethod } from '@xunnamius/types'; +import type { Primitive } from 'type-fest'; + +const debug = debugFactory('next-env:env'); + +// * NOTE: next-env does not invoke dotenv or load any .env files for you, +// * you'll have to do that manually. For Next.js apps, this is the desired +// * behavior since environment variables are defined as secrets. Further note +// * that Webpack and Jest configurations are setup to load .env files for you. + +/** + * This method takes an environment variable value (string), removes illegal + * characters, and then splits the string by its commas, returning the resulting + * array with all nullish members filtered out. + */ +export const envToArray = (envVal: string) => { + return envVal + .replaceAll(/[^\w*,-^~]+/g, '') + .split(',') + .filter(Boolean); +}; + +export type Environment = Record; + +type OverrideEnvExpect = 'force-check' | 'force-no-check' | undefined; + +/** + * Returns an object representing the current runtime environment. + */ +export function getEnv(customizedEnv?: T) { + debug( + `environment definitions (resolved as NODE_ENV) listed in order of precedence:` + ); + debug(`APP_ENV: ${process.env.APP_ENV ?? '(undefined)'}`); + debug(`NODE_ENV: ${process.env.NODE_ENV ?? '(undefined)'}`); + debug(`BABEL_ENV: ${process.env.BABEL_ENV ?? '(undefined)'}`); + + const env = { + OVERRIDE_EXPECT_ENV: + process.env.OVERRIDE_EXPECT_ENV === 'force-check' || + process.env.OVERRIDE_EXPECT_ENV === 'force-no-check' || + process.env.OVERRIDE_EXPECT_ENV === undefined + ? (process.env.OVERRIDE_EXPECT_ENV as OverrideEnvExpect) + : toss( + new InvalidAppEnvironmentError( + 'OVERRIDE_EXPECT_ENV must have value "force-check", "force-no-check", or undefined' + ) + ), + NODE_ENV: + process.env.APP_ENV || + process.env.NODE_ENV || + process.env.BABEL_ENV || + 'unknown', + MONGODB_URI: process.env.MONGODB_URI || '', + MONGODB_MS_PORT: !!process.env.MONGODB_MS_PORT + ? Number(process.env.MONGODB_MS_PORT) + : null, + DISABLED_API_VERSIONS: !!process.env.DISABLED_API_VERSIONS + ? envToArray(process.env.DISABLED_API_VERSIONS.toLowerCase()) + : [], + RESULTS_PER_PAGE: Number(process.env.RESULTS_PER_PAGE) || 100, + IGNORE_RATE_LIMITS: + !!process.env.IGNORE_RATE_LIMITS && process.env.IGNORE_RATE_LIMITS !== 'false', + LOCKOUT_ALL_CLIENTS: + !!process.env.LOCKOUT_ALL_CLIENTS && + process.env.LOCKOUT_ALL_CLIENTS !== 'false', + DISALLOWED_METHODS: !!process.env.DISALLOWED_METHODS + ? envToArray(process.env.DISALLOWED_METHODS.toUpperCase()) + : [], + MAX_CONTENT_LENGTH_BYTES: + parseAsBytes(process.env.MAX_CONTENT_LENGTH_BYTES ?? '-Infinity') || 102_400, + AUTH_HEADER_MAX_LENGTH: Number(process.env.AUTH_HEADER_MAX_LENGTH) || 500, + DEBUG: process.env.DEBUG ?? null, + DEBUG_INSPECTING: !!process.env.VSCODE_INSPECTOR_OPTIONS, + REQUESTS_PER_CONTRIVED_ERROR: + Number(process.env.REQUESTS_PER_CONTRIVED_ERROR) || 0, + + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: !!process.env + .BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS + ? Number(process.env.BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS) + : null, + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: !!process.env + .BAN_HAMMER_MAX_REQUESTS_PER_WINDOW + ? Number(process.env.BAN_HAMMER_MAX_REQUESTS_PER_WINDOW) + : null, + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: !!process.env + .BAN_HAMMER_RESOLUTION_WINDOW_SECONDS + ? Number(process.env.BAN_HAMMER_RESOLUTION_WINDOW_SECONDS) + : null, + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: !!process.env + .BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES + ? Number(process.env.BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES) + : null, + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: !!process.env + .BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER + ? Number(process.env.BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER) + : null, + + PRUNE_DATA_MAX_LOGS_BYTES: + parseAsBytes(process.env.PRUNE_DATA_MAX_LOGS_BYTES ?? '-Infinity') || null, + PRUNE_DATA_MAX_BANNED_BYTES: + parseAsBytes(process.env.PRUNE_DATA_MAX_BANNED_BYTES ?? '-Infinity') || null, + + ...customizedEnv + }; + + debug('resolved env vars:'); + debug(env); + + // TODO: when the following logic is retired, consider renaming this package + // TODO: to `@xunnamius/env` or something similar since it's not next-specific + + // TODO: when in production, perhaps these checks should only be run once? + // TODO: Maybe this entire module should be cached? How does that work with + // TODO: downstream getEnv decorators (like `universe/env`)? + + // TODO: retire all of the following logic when expect-env is created. Also, + // TODO: expect-env should have the ability to skip runs on certain NODE_ENV + // TODO: unless OVERRIDE_EXPECT_ENV is properly defined. + /* istanbul ignore next */ + if ( + (env.NODE_ENV !== 'test' && env.OVERRIDE_EXPECT_ENV !== 'force-no-check') || + env.OVERRIDE_EXPECT_ENV === 'force-check' + ) { + const errors = []; + const envIsGtZero = (name: keyof typeof env) => { + if ( + typeof env[name] !== 'number' || + Number.isNaN(env[name] as number) || + (env[name] as number) < 0 + ) { + errors.push( + `bad ${name}, saw "${env[name]}" (expected a non-negative number)` + ); + } + }; + + if (env.NODE_ENV === 'unknown') + errors.push(`bad NODE_ENV, saw "${env.NODE_ENV}"`); + + // TODO: expect-env should cover this use-case (server-only) as well. + if (isServer()) { + if (env.MONGODB_URI === '') + errors.push(`bad MONGODB_URI, saw "${env.MONGODB_URI}"`); + + ( + [ + 'RESULTS_PER_PAGE', + 'MAX_CONTENT_LENGTH_BYTES', + 'AUTH_HEADER_MAX_LENGTH' + ] as (keyof typeof env)[] + ).forEach((name) => envIsGtZero(name)); + + env.DISALLOWED_METHODS.forEach((method) => { + if (!validHttpMethods.includes(method as ValidHttpMethod)) { + errors.push( + `unknown method "${method}", must be one of: ${validHttpMethods.join( + ', ' + )}` + ); + } + }); + + if (env.MONGODB_MS_PORT && env.MONGODB_MS_PORT <= 1024) { + errors.push(`optional environment variable MONGODB_MS_PORT must be > 1024`); + } + } + + if (errors.length) { + throw new InvalidAppEnvironmentError( + `bad variables:\n - ${errors.join('\n - ')}` + ); + } + } + + return env as typeof env & T; +} diff --git a/lib/next-env/package.json b/lib/next-env/package.json new file mode 100644 index 0000000..cf3f12a --- /dev/null +++ b/lib/next-env/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-env" +} diff --git a/lib/next-env/unit.test.ts b/lib/next-env/unit.test.ts new file mode 100644 index 0000000..278a37d --- /dev/null +++ b/lib/next-env/unit.test.ts @@ -0,0 +1,178 @@ +import { getEnv } from 'multiverse/next-env'; +import { withMockedEnv } from 'testverse/setup'; + +describe('::getEnv', () => { + it('returns object with respect to process.env', async () => { + expect.hasAssertions(); + + await withMockedEnv( + () => { + expect(getEnv()).toStrictEqual({ + OVERRIDE_EXPECT_ENV: undefined, + NODE_ENV: 'known', + MONGODB_URI: 'uri', + MONGODB_MS_PORT: null, + DISABLED_API_VERSIONS: [], + RESULTS_PER_PAGE: 5, + IGNORE_RATE_LIMITS: false, + LOCKOUT_ALL_CLIENTS: false, + DISALLOWED_METHODS: [], + MAX_CONTENT_LENGTH_BYTES: 1024, + AUTH_HEADER_MAX_LENGTH: 500, + DEBUG: null, + DEBUG_INSPECTING: false, + REQUESTS_PER_CONTRIVED_ERROR: 0, + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: null, + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: null, + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: null, + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: null, + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: null, + PRUNE_DATA_MAX_LOGS_BYTES: null, + PRUNE_DATA_MAX_BANNED_BYTES: null + }); + }, + { + BABEL_ENV: 'known', + MONGODB_URI: 'uri', + RESULTS_PER_PAGE: '5', + MAX_CONTENT_LENGTH_BYTES: '1KB' + } + ); + + // TODO: retire this test and/or merge it into expect-env + await withMockedEnv(() => { + expect(() => getEnv()).toThrow(`bad variables: + - bad NODE_ENV, saw "unknown" + - bad MONGODB_URI, saw ""`); + }, {}); + }); + + // TODO: retire the next two checks and fold them into expect-env instead + + it('does not run expect-env if NODE_ENV is not "test"', async () => { + expect.hasAssertions(); + + await withMockedEnv( + () => { + expect(getEnv()).toStrictEqual({ + OVERRIDE_EXPECT_ENV: undefined, + NODE_ENV: 'test', + MONGODB_URI: 'uri', + MONGODB_MS_PORT: 1234, + DISABLED_API_VERSIONS: ['one', '2', 'three'], + RESULTS_PER_PAGE: 5, + IGNORE_RATE_LIMITS: false, + LOCKOUT_ALL_CLIENTS: true, + DISALLOWED_METHODS: ['FAKE'], + MAX_CONTENT_LENGTH_BYTES: 1024, + AUTH_HEADER_MAX_LENGTH: 50, + DEBUG: 'false', + DEBUG_INSPECTING: true, + REQUESTS_PER_CONTRIVED_ERROR: 5, + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: 10, + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: 15, + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: 20, + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: 25, + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: 30, + PRUNE_DATA_MAX_LOGS_BYTES: 35, + PRUNE_DATA_MAX_BANNED_BYTES: 1024 + }); + }, + { + NODE_ENV: 'test', + MONGODB_URI: 'uri', + MONGODB_MS_PORT: '1234', + DISABLED_API_VERSIONS: 'one, 2, three', + RESULTS_PER_PAGE: '5', + IGNORE_RATE_LIMITS: 'false', + LOCKOUT_ALL_CLIENTS: 'true', + DISALLOWED_METHODS: 'FAKE', + MAX_CONTENT_LENGTH_BYTES: '1KB', + AUTH_HEADER_MAX_LENGTH: '50', + DEBUG: 'false', + VSCODE_INSPECTOR_OPTIONS: 'inspector', + REQUESTS_PER_CONTRIVED_ERROR: '5', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '10', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '15', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '20', + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '25', + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '30', + PRUNE_DATA_MAX_LOGS_BYTES: '35', + PRUNE_DATA_MAX_BANNED_BYTES: '1kb' + } + ); + }); + + it('respects OVERRIDE_EXPECT_ENV', async () => { + expect.hasAssertions(); + + await withMockedEnv( + () => { + expect(() => getEnv()).toThrow(/bad/); + }, + { + OVERRIDE_EXPECT_ENV: 'force-check', + NODE_ENV: 'test', + MONGODB_URI: 'uri', + MONGODB_MS_PORT: '1234', + DISABLED_API_VERSIONS: 'one, 2, three', + RESULTS_PER_PAGE: '5', + IGNORE_RATE_LIMITS: 'false', + LOCKOUT_ALL_CLIENTS: 'true', + DISALLOWED_METHODS: 'FAKE', + MAX_CONTENT_LENGTH_BYTES: '1KB', + AUTH_HEADER_MAX_LENGTH: '50', + DEBUG: 'false', + VSCODE_INSPECTOR_OPTIONS: 'inspector', + REQUESTS_PER_CONTRIVED_ERROR: '5', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '10', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '15', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '20', + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '25', + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '30', + PRUNE_DATA_MAX_LOGS_BYTES: '35b', + PRUNE_DATA_MAX_BANNED_BYTES: '40b' + } + ); + + await withMockedEnv( + () => { + expect(() => getEnv()).not.toThrow(); + }, + { + OVERRIDE_EXPECT_ENV: 'force-no-check', + NODE_ENV: 'test', + MONGODB_URI: 'uri', + MONGODB_MS_PORT: '1234', + DISABLED_API_VERSIONS: 'one, 2, three', + RESULTS_PER_PAGE: '5', + IGNORE_RATE_LIMITS: 'false', + LOCKOUT_ALL_CLIENTS: 'true', + DISALLOWED_METHODS: 'FAKE', + MAX_CONTENT_LENGTH_BYTES: '1KB', + AUTH_HEADER_MAX_LENGTH: '50', + DEBUG: 'false', + VSCODE_INSPECTOR_OPTIONS: 'inspector', + REQUESTS_PER_CONTRIVED_ERROR: '5', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '10', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '15', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '20', + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '25', + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '30', + PRUNE_DATA_MAX_LOGS_BYTES: '35b', + PRUNE_DATA_MAX_BANNED_BYTES: '40b' + } + ); + }); + + it('throws on invalid OVERRIDE_EXPECT_ENV', async () => { + expect.hasAssertions(); + + await withMockedEnv( + () => { + expect(() => getEnv()).toThrow(/must have value "force-check"/); + }, + { OVERRIDE_EXPECT_ENV: '' } + ); + }); +}); diff --git a/lib/next-limit/index.ts b/lib/next-limit/index.ts new file mode 100644 index 0000000..b5f2d0e --- /dev/null +++ b/lib/next-limit/index.ts @@ -0,0 +1,115 @@ +import { getDb } from 'multiverse/mongo-schema'; +import { getEnv } from 'multiverse/next-env'; +import { getClientIp } from 'request-ip'; +import { ValidationError } from 'universe/error'; + +import type { NextApiRequest } from 'next'; +import type { UnixEpochMs } from '@xunnamius/types'; +import type { UpdateResult, WithId, WithoutId } from 'mongodb'; + +/** + * The shape of an entry in the well-known "limited log" collection. + */ +export type InternalLimitedLogEntry = WithId< + | { + until: UnixEpochMs; + ip: string; + header?: never; + } + | { + until: UnixEpochMs; + ip?: never; + header: string; + } +>; + +/** + * The shape of a new entry in the well-known "limited log" collection. + */ +export type NewLimitedLogEntry = WithoutId; + +/** + * Returns an object with two keys: `isLimited` and `retryAfter`. If `isLimited` + * is true, then the request should be rejected. The client should be instructed + * to retry their request after `retryAfter` milliseconds have passed. + */ +export async function clientIsRateLimited(req: NextApiRequest) { + const ip = getClientIp(req); + const header = req.headers.authorization + ?.slice(0, getEnv().AUTH_HEADER_MAX_LENGTH) + .toLowerCase(); + + const limited = await ( + await getDb({ name: 'root' }) + ) + .collection('limited-log') + .find({ + $or: [...(ip ? [{ ip }] : []), ...(header ? [{ header }] : [])], + until: { $gt: Date.now() } // ? Skip the recently unbanned + }) + .sort({ until: -1 }) + .limit(1) + .next(); + + return { + isLimited: !!limited, + retryAfter: Math.max( + 0, + ((limited?.until as number) || Date.now()) - Date.now() + ) as UnixEpochMs + }; +} + +/** + * Removes a rate limit on a client matched against either `ip`, `header`, or + * both. Matching against both removes rate limits that match either criterion. + * + * @returns The number of rate limits removed. + */ +export async function removeRateLimit({ + target +}: { + target: { ip?: string; header?: string } | undefined; +}) { + if (target) { + const { ip, header } = target; + + if (ip !== undefined || header !== undefined) { + if (ip !== undefined && (typeof ip !== 'string' || !ip)) { + throw new ValidationError('ip must be a non-empty string'); + } + + if (header !== undefined && (typeof header !== 'string' || !header)) { + throw new ValidationError('header must be a non-empty string'); + } + + const now = Date.now(); + const result = (await (await getDb({ name: 'root' })) + .collection('limited-log') + .updateMany( + { + $or: [...(ip ? [{ ip }] : []), ...(header ? [{ header }] : [])], + until: { $gt: now } // ? Skip the recently unbanned + }, + { $set: { until: now } } + )) as UpdateResult; + + return result.modifiedCount; + } + } + + throw new ValidationError('must provide either an ip or a header'); +} + +/** + * Retrieve all active rate limits. + */ +export async function getAllRateLimits() { + return (await getDb({ name: 'root' })) + .collection('limited-log') + .find>( + { until: { $gt: Date.now() } }, + { sort: { _id: -1 }, projection: { _id: false } } + ) + .toArray(); +} diff --git a/lib/next-limit/package.json b/lib/next-limit/package.json new file mode 100644 index 0000000..9d0ad6f --- /dev/null +++ b/lib/next-limit/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-limit" +} diff --git a/lib/next-limit/unit.test.ts b/lib/next-limit/unit.test.ts new file mode 100644 index 0000000..bc0e804 --- /dev/null +++ b/lib/next-limit/unit.test.ts @@ -0,0 +1,334 @@ +import { dummyRootData, useMockDateNow } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { BANNED_BEARER_TOKEN } from 'multiverse/next-auth'; +import { + clientIsRateLimited, + getAllRateLimits, + removeRateLimit +} from 'multiverse/next-limit'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import type { InternalLimitedLogEntry } from 'multiverse/next-limit'; +import type { NextApiRequest } from 'next'; + +setupMemoryServerOverride(); +useMockDateNow(); + +describe('::clientIsRateLimited', () => { + it('returns true if ip or header (case-insensitive) are rate limited', async () => { + expect.hasAssertions(); + + const req1 = await clientIsRateLimited({ + headers: { 'x-forwarded-for': '1.2.3.4' }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest); + + const req2 = await clientIsRateLimited({ + headers: { + 'x-forwarded-for': '8.8.8.8', + // ? Should work with different cases too + authorization: `BEARER ${BANNED_BEARER_TOKEN}` + }, + method: 'GET', + url: '/api/route/path2' + } as unknown as NextApiRequest); + + const req3 = await clientIsRateLimited({ + headers: { + 'x-forwarded-for': '1.2.3.4', + authorization: 'bearer fake-header' + }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest); + + const req4 = await clientIsRateLimited({ + headers: { + 'x-forwarded-for': '5.6.7.8' + }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest); + + const req5 = await clientIsRateLimited({ + headers: { + 'x-forwarded-for': '1.2.3.4', + authorization: `bearer ${BANNED_BEARER_TOKEN}` + }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest); + + const req6 = await clientIsRateLimited({ + headers: { + // ? Should work with different cases too + authorization: `bEaReR ${BANNED_BEARER_TOKEN}` + }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest); + + expect(req1.isLimited).toBeTrue(); + expect(req2.isLimited).toBeTrue(); + expect(req3.isLimited).toBeTrue(); + expect(req4.isLimited).toBeTrue(); + expect(req5.isLimited).toBeTrue(); + expect(req6.isLimited).toBeTrue(); + + const minToMs = (minutes: number) => 1000 * 60 * minutes; + expect(req1.retryAfter).toBeWithin(minToMs(15) - 1000, minToMs(15) + 1000); + expect(req2.retryAfter).toBeWithin(minToMs(60) - 1000, minToMs(60) + 1000); + expect(req3.retryAfter).toBeWithin(minToMs(15) - 1000, minToMs(15) + 1000); + expect(req4.retryAfter).toBeWithin(minToMs(15) - 1000, minToMs(15) + 1000); + // ? Should return greater of the two ban times (header time > ip time) + expect(req5.retryAfter).toBeWithin(minToMs(60) - 1000, minToMs(60) + 1000); + expect(req6.retryAfter).toBeWithin(minToMs(60) - 1000, minToMs(60) + 1000); + }); + + it('returns false if both ip and header (if provided) are not rate limited', async () => { + expect.hasAssertions(); + const req1 = { + headers: { 'x-forwarded-for': '1.2.3.5' }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest; + + const req2 = { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: 'bearer fake-header' + }, + method: 'GET', + url: '/api/route/path2' + } as unknown as NextApiRequest; + + await expect(clientIsRateLimited(req1)).resolves.toStrictEqual({ + isLimited: false, + retryAfter: 0 + }); + await expect(clientIsRateLimited(req2)).resolves.toStrictEqual({ + isLimited: false, + retryAfter: 0 + }); + }); + + it('returns false if "until" time has passed', async () => { + expect.hasAssertions(); + + const req = { + headers: { 'x-forwarded-for': '1.2.3.4' }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest; + + await expect(clientIsRateLimited(req)).resolves.toStrictEqual({ + isLimited: true, + retryAfter: expect.any(Number) + }); + + await (await getDb({ name: 'root' })) + .collection('limited-log') + .updateOne({ ip: '1.2.3.4' }, { $set: { until: Date.now() - 10 ** 5 } }); + + await expect(clientIsRateLimited(req)).resolves.toStrictEqual({ + isLimited: false, + retryAfter: 0 + }); + }); +}); + +describe('::removeRateLimit', () => { + it('removes an active rate limit by ip, header', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + + await expect( + db.countDocuments({ + ip: dummyRootData['limited-log'][0].ip, + until: { $gt: Date.now() } + }) + ).resolves.toBe(1); + + await expect( + removeRateLimit({ target: { ip: dummyRootData['limited-log'][0].ip } }) + ).resolves.toBe(1); + + await expect( + db.countDocuments({ + ip: dummyRootData['limited-log'][0].ip, + until: { $gt: Date.now() } + }) + ).resolves.toBe(0); + + await expect( + db.countDocuments({ + header: dummyRootData['limited-log'][2].header, + until: { $gt: Date.now() } + }) + ).resolves.toBe(1); + + await expect( + removeRateLimit({ target: { header: dummyRootData['limited-log'][2].header } }) + ).resolves.toBe(1); + + await expect( + db.countDocuments({ + header: dummyRootData['limited-log'][2].header, + until: { $gt: Date.now() } + }) + ).resolves.toBe(0); + }); + + it('removes an active rate limit by ip or header (simultaneously)', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + + await expect( + db.countDocuments({ + $or: [ + { ip: dummyRootData['limited-log'][1].ip }, + { header: dummyRootData['limited-log'][2].header } + ], + until: { $gt: Date.now() } + }) + ).resolves.toBe(2); + + await expect( + removeRateLimit({ + target: { + ip: dummyRootData['limited-log'][1].ip, + header: dummyRootData['limited-log'][2].header + } + }) + ).resolves.toBe(2); + + await expect( + db.countDocuments({ + $or: [ + { ip: dummyRootData['limited-log'][1].ip }, + { header: dummyRootData['limited-log'][2].header } + ], + until: { $gt: Date.now() } + }) + ).resolves.toBe(0); + }); + + it('only removes active rate limits', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + + await db.updateOne( + { ip: dummyRootData['limited-log'][1].ip }, + { $set: { until: Date.now() } } + ); + + await expect( + removeRateLimit({ + target: { + ip: dummyRootData['limited-log'][1].ip, + header: dummyRootData['limited-log'][2].header + } + }) + ).resolves.toBe(1); + }); + + it('returns 0 if no active rate limit was found', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + + await db.updateOne( + { ip: dummyRootData['limited-log'][1].ip }, + { $set: { until: Date.now() } } + ); + + await expect( + removeRateLimit({ target: { ip: dummyRootData['limited-log'][1].ip } }) + ).resolves.toBe(0); + }); + + it('rejects if passed invalid data', async () => { + expect.hasAssertions(); + await Promise.all([ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(removeRateLimit({} as any)).rejects.toMatchObject({ + message: 'must provide either an ip or a header' + }), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(removeRateLimit({ something: 'else' } as any)).rejects.toMatchObject({ + message: 'must provide either an ip or a header' + }), + + expect(removeRateLimit({ target: undefined })).rejects.toMatchObject({ + message: 'must provide either an ip or a header' + }), + + expect(removeRateLimit({ target: {} })).rejects.toMatchObject({ + message: 'must provide either an ip or a header' + }), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(removeRateLimit({ target: { ip: true } } as any)).rejects.toMatchObject({ + message: 'ip must be a non-empty string' + }), + + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + removeRateLimit({ target: { header: true } as any }) + ).rejects.toMatchObject({ + message: 'header must be a non-empty string' + }), + + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + removeRateLimit({ target: { ip: '', header: true } as any }) + ).rejects.toMatchObject({ + message: 'ip must be a non-empty string' + }), + + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + removeRateLimit({ target: { ip: null, header: '' } as any }) + ).rejects.toMatchObject({ + message: 'ip must be a non-empty string' + }), + + expect( + removeRateLimit({ target: { ip: undefined, header: undefined } }) + ).rejects.toMatchObject({ message: 'must provide either an ip or a header' }), + expect( + removeRateLimit({ target: { ip: '', header: '' } }) + ).rejects.toMatchObject({ + message: 'ip must be a non-empty string' + }) + ]); + }); +}); + +describe('::getAllRateLimits', () => { + it('returns all active rate limits in the system', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + + await expect(getAllRateLimits()).resolves.toIncludeSameMembers( + await db + .find({ until: { $gt: Date.now() } }, { projection: { _id: false } }) + .toArray() + ); + }); + + it('does not crash if database is empty', async () => { + expect.hasAssertions(); + + const db = (await getDb({ name: 'root' })).collection('limited-log'); + await db.deleteMany({}); + + await expect(getAllRateLimits()).resolves.toStrictEqual([]); + }); +}); diff --git a/lib/next-log/index.ts b/lib/next-log/index.ts new file mode 100644 index 0000000..9471e7c --- /dev/null +++ b/lib/next-log/index.ts @@ -0,0 +1,77 @@ +import { getClientIp } from 'request-ip'; + +import { getEnv } from 'multiverse/next-env'; +import { getDb } from 'multiverse/mongo-schema'; + +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { HttpStatusCode, UnixEpochMs } from '@xunnamius/types'; +import type { WithId, WithoutId } from 'mongodb'; + +/** + * The shape of an entry in the well-known "request log" collection. + */ +export type InternalRequestLogEntry = WithId<{ + ip: string | null; + header: string | null; + route: string | null; + endpoint: string | null; + method: string | null; + resStatusCode: HttpStatusCode; + createdAt: UnixEpochMs; + durationMs: number; +}>; + +/** + * The shape of a new entry in the well-known "request log" collection. + */ +export type NewRequestLogEntry = WithoutId; + +/** + * This function adds a request metadata entry to the database. + * + * Note that this async function **does not have to be awaited**. It's fire and + * forget! + * + * @example + * ``` + * doSomeStuff(); + * void addToRequestLog({ req, res, endpoint }); + * doSomeOtherStuff(); + * ``` + */ +export async function addToRequestLog({ + req, + res, + endpoint, + durationMs +}: { + req: NextApiRequest; + res: NextApiResponse; + endpoint: string | null | undefined; + durationMs: number; +}): Promise { + if (!endpoint) { + // eslint-disable-next-line no-console + console.warn( + `${ + req.url ? `API endpoint at ${req.url}` : 'an API endpoint' + } is missing its descriptor metadata` + ); + } + + await (await getDb({ name: 'root' })) + .collection('request-log') + .insertOne({ + ip: getClientIp(req), + header: + req.headers.authorization + ?.slice(0, getEnv().AUTH_HEADER_MAX_LENGTH) + .toLowerCase() || null, + method: req.method?.toUpperCase() || null, + route: req.url || null, + endpoint: endpoint || null, + resStatusCode: res.statusCode as HttpStatusCode, + createdAt: Date.now(), + durationMs + }); +} diff --git a/lib/next-log/package.json b/lib/next-log/package.json new file mode 100644 index 0000000..483df65 --- /dev/null +++ b/lib/next-log/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/next-log" +} diff --git a/lib/next-log/unit.test.ts b/lib/next-log/unit.test.ts new file mode 100644 index 0000000..ef6fc3a --- /dev/null +++ b/lib/next-log/unit.test.ts @@ -0,0 +1,238 @@ +import { useMockDateNow, mockDateNowMs } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { BANNED_BEARER_TOKEN } from 'multiverse/next-auth'; +import { addToRequestLog } from 'multiverse/next-log'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { withMockedOutput } from 'testverse/setup'; + +import type { InternalRequestLogEntry } from 'multiverse/next-log'; +import type { HttpStatusCode } from '@xunnamius/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +setupMemoryServerOverride(); +useMockDateNow(); + +const mockPerfNow = 1234; + +describe('::addToRequestLog', () => { + it('adds request to mongo collection', async () => { + expect.hasAssertions(); + + const req1 = { + headers: { 'x-forwarded-for': '9.9.9.9' }, + method: 'POST', + url: '/api/route/path1' + } as unknown as NextApiRequest; + + const req2 = { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: `bearer ${BANNED_BEARER_TOKEN}` + }, + method: 'GET', + url: '/api/route/path2' + } as unknown as NextApiRequest; + + const res1 = { statusCode: 1111 } as NextApiResponse; + const res2 = { statusCode: 2222 } as NextApiResponse; + + await addToRequestLog({ + req: req1, + res: res1, + endpoint: '/fake', + durationMs: 1234 + }); + + await addToRequestLog({ + req: req2, + res: res2, + endpoint: '/fake', + durationMs: 1234 + }); + + const reqlog = ( + await getDb({ name: 'root' }) + ).collection('request-log'); + + await expect( + reqlog.findOne({ resStatusCode: 1111 as HttpStatusCode }) + ).resolves.toStrictEqual({ + _id: expect.anything(), + ip: '9.9.9.9', + header: null, + route: '/api/route/path1', + endpoint: '/fake', + method: 'POST', + createdAt: mockDateNowMs, + resStatusCode: 1111, + durationMs: mockPerfNow + }); + + await expect( + reqlog.findOne({ resStatusCode: 2222 as HttpStatusCode }) + ).resolves.toStrictEqual({ + _id: expect.anything(), + ip: '8.8.8.8', + header: `bearer ${BANNED_BEARER_TOKEN}`, + route: '/api/route/path2', + endpoint: '/fake', + method: 'GET', + createdAt: mockDateNowMs, + resStatusCode: 2222, + durationMs: mockPerfNow + }); + }); + + it('handles null method and/or url and lowercases schema', async () => { + expect.hasAssertions(); + + const req1 = { + headers: { 'x-forwarded-for': '9.9.9.9' }, + method: null, + url: '/api/route/path1' + } as unknown as NextApiRequest; + + const req2 = { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: `BeArEr ${BANNED_BEARER_TOKEN}` + }, + method: 'GET', + url: null + } as unknown as NextApiRequest; + + const res1 = { statusCode: 1111 } as NextApiResponse; + const res2 = { statusCode: 2222 } as NextApiResponse; + + await addToRequestLog({ + req: req1, + res: res1, + endpoint: '/fake', + durationMs: 1234 + }); + + await addToRequestLog({ + req: req2, + res: res2, + endpoint: '/fake', + durationMs: 1234 + }); + + const reqlog = ( + await getDb({ name: 'root' }) + ).collection('request-log'); + + await expect( + reqlog.findOne({ resStatusCode: 1111 as HttpStatusCode }) + ).resolves.toStrictEqual({ + _id: expect.anything(), + ip: '9.9.9.9', + header: null, + route: '/api/route/path1', + endpoint: '/fake', + method: null, + createdAt: mockDateNowMs, + resStatusCode: 1111, + durationMs: mockPerfNow + }); + + await expect( + reqlog.findOne({ resStatusCode: 2222 as HttpStatusCode }) + ).resolves.toStrictEqual({ + _id: expect.anything(), + ip: '8.8.8.8', + header: `bearer ${BANNED_BEARER_TOKEN}`, + route: null, + endpoint: '/fake', + method: 'GET', + createdAt: mockDateNowMs, + resStatusCode: 2222, + durationMs: mockPerfNow + }); + }); + + it('handles null or undefined endpoint metadata with warnings', async () => { + expect.hasAssertions(); + + const req1 = { + headers: { 'x-forwarded-for': '9.9.9.9' }, + method: 'GET', + url: '/api/route/path1' + } as unknown as NextApiRequest; + + const req2 = { + headers: { 'x-forwarded-for': '8.8.8.8' }, + method: 'GET', + url: null + } as unknown as NextApiRequest; + + const res1 = { statusCode: 1111 } as NextApiResponse; + const res2 = { statusCode: 2222 } as NextApiResponse; + const res3 = { statusCode: 3333 } as NextApiResponse; + + const reqlog = ( + await getDb({ name: 'root' }) + ).collection('request-log'); + + await withMockedOutput(async ({ warnSpy }) => { + await addToRequestLog({ + req: req1, + res: res1, + endpoint: null, + durationMs: 1234 + }); + + expect(warnSpy).toBeCalledWith( + expect.stringContaining(`API endpoint at ${req1.url}`) + ); + + await expect( + reqlog.findOne({ resStatusCode: 1111 as HttpStatusCode }) + ).resolves.toStrictEqual( + expect.objectContaining({ + endpoint: null + }) + ); + + await addToRequestLog({ + req: req2, + res: res2, + endpoint: undefined, + durationMs: 1234 + }); + + expect(warnSpy).toBeCalledWith(expect.stringContaining('an API endpoint')); + + await expect( + reqlog.findOne({ resStatusCode: 2222 as HttpStatusCode }) + ).resolves.toStrictEqual( + expect.objectContaining({ + endpoint: null + }) + ); + + // @ts-expect-error: purposely missing endpoint parameter + await addToRequestLog({ req: req2, res: res3 }); + + expect(warnSpy).toBeCalledTimes(3); + + await expect( + reqlog.findOne({ resStatusCode: 3333 as HttpStatusCode }) + ).resolves.toStrictEqual( + expect.objectContaining({ + endpoint: null + }) + ); + + await addToRequestLog({ + req: req2, + res: res3, + endpoint: '/fake', + durationMs: 1234 + }); + + expect(warnSpy).toBeCalledTimes(3); + }); + }); +}); diff --git a/lib/suppress-experimental-warnings/index.ts b/lib/suppress-experimental-warnings/index.ts new file mode 100644 index 0000000..3f39ae1 --- /dev/null +++ b/lib/suppress-experimental-warnings/index.ts @@ -0,0 +1,39 @@ +import { debugFactory } from 'multiverse/debug-extended'; + +const debug = debugFactory('suppress-warnings:debug'); + +/** + * Prevent Node from emitting specific warnings when running third-party code. + */ +export function suppressWarnings( + /** + * The exact (case-sensitive) names of the warnings that will be suppressed. + * + * @default ['ExperimentalWarning'] + */ + names?: string[] +) { + const ignoredWarningNames = names || ['ExperimentalWarning']; + const warningListeners = process.listeners('warning'); + let alreadyWarned = false; + + if (warningListeners[0]) { + const originalWarningListener = warningListeners[0]; + process.removeAllListeners('warning'); + + process.prependListener('warning', (warning) => { + if (!ignoredWarningNames.includes(warning.name)) { + originalWarningListener(warning); + } else if (!alreadyWarned) { + debug.warn('one or more warnings were suppressed'); + alreadyWarned = true; + } + }); + } + + if (warningListeners.length !== 1) { + debug.warn( + `expected 1 listener on the process "warning" event, but removed ${warningListeners.length}` + ); + } +} diff --git a/lib/suppress-experimental-warnings/package.json b/lib/suppress-experimental-warnings/package.json new file mode 100644 index 0000000..47c207c --- /dev/null +++ b/lib/suppress-experimental-warnings/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/suppress-warnings" +} diff --git a/lib/throttled-fetch/index.ts b/lib/throttled-fetch/index.ts new file mode 100644 index 0000000..4f32c19 --- /dev/null +++ b/lib/throttled-fetch/index.ts @@ -0,0 +1,916 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import fetch, { Headers } from 'node-fetch'; +import autobind from 'auto-bind'; +import { GuruMeditationError, HttpError, makeNamedError } from 'named-app-errors'; +import { toss } from 'toss-expression'; +import { setTimeout as wait } from 'node:timers/promises'; + +import { debugFactory } from 'multiverse/debug-extended'; + +const debug = debugFactory(`throttled-fetch:index`); + +import type { Promisable } from 'type-fest'; +import type { RequestInfo, RequestInit } from 'node-fetch'; + +/** + * An internal representation of the node-fetch function's parameters. + */ +type FetchParams = Parameters; + +/** + * An internal representation of the `addRequestToQueue` and + * `prependRequestToQueue` parameters. + */ +type ExtendedFetchParams = [ + url: RequestInfo, + init?: RequestInit | undefined, + state?: Record +]; + +/** + * An internal function used to eventually resolve the `addRequestToQueue` and + * `prependRequestToQueue` calls with the response data. + */ +type RequestQueueCallback = { + (error: null, returnValue: unknown): void; + (error: Error, returnValue?: undefined): void; +}; + +/** + * A `RequestInit` instance guaranteed to have a non-falsy signal property. + */ +type RequestInitWithSignal = RequestInit & { + signal: NonNullable; +}; + +/** + * The shape of a request-inspecting function. + */ +export type RequestInspector = (params: { + queue: RequestQueue; + requestInfo: RequestInfo; + requestInit: RequestInitWithSignal; + state: Record; +}) => Promisable; + +/** + * The shape of a response-inspecting function. + */ +export type ResponseInspector = (params: { + response: unknown; + queue: RequestQueue; + requestInfo: RequestInfo; + requestInit: RequestInitWithSignal; + state: Record; +}) => Promisable; + +/** + * The shape of a FetchError-inspecting function. + */ +export type FetchErrorInspector = (params: { + error: unknown; + queue: RequestQueue; + requestInfo: RequestInfo; + requestInit: RequestInitWithSignal; + state: Record; +}) => Promisable; + +/** + * Thrown in response to a queue-related error. + */ +export class RequestQueueError extends Error {} +makeNamedError(RequestQueueError, 'RequestQueueError'); + +/** + * Thrown by `addRequestToQueue` when the request was removed from the queue + * without being sent or otherwise processed. + */ +export class RequestQueueClearedError extends RequestQueueError {} +makeNamedError(RequestQueueClearedError, 'RequestQueueClearedError'); + +/** + * The default `RequestInspector` used by each `RequestQueue` instance unless + * otherwise configured. Simply passes through the given fetch parameters. + */ +export const defaultRequestInspector: RequestInspector = () => undefined; + +/** + * The default `ResponseInspector` used by each `RequestQueue` instance unless + * otherwise configured. Simply passes through the `Response` instance. + */ +export const defaultResponseInspector: ResponseInspector = ({ response: res }) => res; + +/** + * The default `FetchErrorInspector` used by each `RequestQueue` instance unless + * otherwise configured. Re-throws the `FetchError` instance. + */ +export const defaultFetchErrorInspector: FetchErrorInspector = ({ error }) => { + throw error; +}; + +/** + * Execute requests present in the request queue with respect to backoff data, + * flow control, rate limits, and other data. + */ +export class RequestQueue { + /** + * If non-zero, no new requests will be made until this many milliseconds have + * transpired. + */ + #delayRequestProcessingByMs = 0; + + /** + * Once this is set to false and requestQueue is empty, + * `queueAbortController.abort()` will be called automatically. + */ + #keepProcessingRequestQueue = true; + + /** + * Determines when queue processing is "soft-paused," which allows the + * processor to avoid wasting cycles scheduling intervals when the request + * queue is empty. + */ + #queueProcessingIsSoftPaused = false; + + /** + * Used to abort the request queue processor. + */ + #timeoutId: NodeJS.Timeout | null = null; + + /** + * Used to immediately end the delaying period. + */ + #terminationAbortController = new AbortController(); + + /** + * A function used to individual requests based on feedback from request data. + */ + #requestInspector: RequestInspector; + + /** + * A function used to alter the behavior of the queue based on feedback from + * response data. + */ + #responseInspector: ResponseInspector; + + /** + * A function used to alter the behavior of the queue when the fetch function + * rejects. + */ + #fetchErrorInspector: FetchErrorInspector; + + /** + * Default request initialization parameters sent along with every request. + */ + #defaultRequestInit: RequestInit = {}; + + /** + * Used to facilitate "waiting" for the queue to stop processing requests. + */ + #queueStoppedPromise: Promise = Promise.resolve(); + + /** + * Used to facilitate "waiting" for the queue to stop processing requests. + */ + /* istanbul ignore next */ + #queueStoppedPromiseResolver: () => void = () => undefined; + + /** + * The maximum number of requests processed in a single interval. + */ + #maxRequestsPerInterval: number; + + /** + * The number of milliseconds each interval lasts. + */ + #intervalPeriodMs: number; + + /** + * A queue of requests waiting to be processed. The response JSON data will be + * returned via the `resolve` function. + */ + #requestQueue: [ + fetchParams: FetchParams, + callback: RequestQueueCallback, + state: Record + ][] = []; + + /** + * A counter used only in debug and stats output. + */ + #debugIntervalCounter = 0; + + /** + * A counter used only in debug and stats output. + */ + #debugAddRequestCounter = 0; + + /** + * A counter used only in debug and stats output. + */ + #debugSentRequestCounter = 0; + + /** + * Create, configure, and return a new RequestQueue instance. All instance + * methods are auto-bound. + */ + constructor({ + maxRequestsPerInterval, + intervalPeriodMs, + requestInspector, + responseInspector, + fetchErrorInspector, + autoStart = false + }: { + /** + * A maximum of `maxRequestsPerInterval` requests will be processed every + * `>=intervalPeriodMs` milliseconds. + */ + maxRequestsPerInterval: number; + /** + * A maximum of `maxRequestsPerInterval` requests will be processed every + * `>=intervalPeriodMs` milliseconds. + */ + intervalPeriodMs: number; + /** + * A function used to alter the behavior of individual requests based on + * available parameters. This function must do one of the following before + * terminating: + * + * - Mutate `addRequestToQueue`'s params before letting it continue. + * - BYO fetch library and return a promise that resolves how you want. + * - Call `addRequestToQueue` again and return it (beware infinite loops). + * - Await a `setTimeout` promise to delay the request before continuing. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * Delaying a request using `requestInspector` will have no effect on the + * processing of other requests or the period of intervals, making it ideal + * for more complex (e.g. isolated, per-endpoint) throttling requirements. + * + * If this function returns `undefined` or a promise that resolves to + * `undefined`, an internal `fetch()` will be made using the request params + * passed to (and potentially mutated by) this function. The fetch result + * will be passed to `responseInspector`. Otherwise, the resolved defined + * value of this function will be passed to `responseInspector` directly (no + * additional internal `fetch()` happens). + * + * If this function throws, the corresponding `addRequestToQueue` call will + * reject and `responseInspector` will not be called. + * + * @default defaultRequestInspector (see export) + */ + requestInspector?: RequestInspector; + /** + * A function used to reshape response data before returning it through the + * resolved `addRequestToQueue` promise. This function must do one of the + * following before terminating: + * + * - Return a JSON representation of the response, e.g. `response.json()`. + * - Interpret and/or transform the response data and return any value. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * The return value of this function will eventually be used as the resolved + * value of the promise returned by the corresponding `addRequestToQueue` + * call that triggered it. Similarly, if this function throws, the + * corresponding `addRequestToQueue` call will reject. + * + * @default defaultResponseInspector (see export) + */ + responseInspector?: ResponseInspector; + /** + * A function used to take some action after the node-fetch `fetch` function + * rejects due to failure. Like `requestInspector`, this function must do + * one of the following before terminating: + * + * - Return a promise that resolves how you want. + * - Call `addRequestToQueue` again and return it (beware infinite loops). + * - Await a `setTimeout` promise to delay the request before continuing. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * Delaying a request using `fetchErrorInspector` will have no effect on the + * processing of other requests or the period of intervals, making it ideal + * to retry failed fetch requests. + * + * The resolved value of this function will always be passed to + * `responseInspector` directly (no additional internal `fetch()` happens) + * unless an error is thrown, in which case the `addRequestToQueue` return + * value will reject. + * + * @default defaultFetchErrorInspector (see export) + */ + fetchErrorInspector?: FetchErrorInspector; + /** + * If `true`, `beginProcessingRequestQueue` and + * `gracefullyStopProcessingRequestQueue` will be called immediately after + * the new instance is created. This allows you to start adding requests to + * the queue immediately without worrying about managing the scheduling + * runtime. + * + * Note that using `await` in the same context as the queue instance, and + * before adding all of your desired requests, could cause the queue to stop + * processing earlier than you might expect. If this is happening, you'll + * have to call `beginProcessingRequestQueue` and + * `gracefullyStopProcessingRequestQueue` manually instead of using + * `autoStart`. + * + * @default false + */ + autoStart?: boolean; + }) { + this.#maxRequestsPerInterval = maxRequestsPerInterval; + this.#intervalPeriodMs = intervalPeriodMs; + this.#requestInspector = requestInspector ?? defaultRequestInspector; + this.#responseInspector = responseInspector ?? defaultResponseInspector; + this.#fetchErrorInspector = fetchErrorInspector ?? defaultFetchErrorInspector; + + // ? Note that this only auto-binds public methods + autobind(this); + + if (autoStart) { + this.beginProcessingRequestQueue(); + this.gracefullyStopProcessingRequestQueue(); + } + } + + /** + * Returns `true` if the request queue is currently being processed or `false` + * otherwise. + */ + get isProcessingRequestQueue() { + return !!this.#timeoutId; + } + + /** + * If non-zero, no new requests will be made until this many milliseconds have + * transpired. This value is relative to when `delayRequestProcessingByMs` was + * last called, so querying this property isn't useful without that additional + * context. + */ + get requestProcessingDelayMs() { + return this.#delayRequestProcessingByMs; + } + + /** + * A function used to alter the behavior of individual requests based on + * available parameters. This function must do one of the following before + * terminating: + * + * - Mutate `addRequestToQueue`'s params before letting it continue. + * - BYO fetch library and return a promise that resolves how you want. + * - Call `addRequestToQueue` again and return it (beware infinite loops). + * - Await a `setTimeout` promise to delay the request before continuing. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * Delaying a request using `requestInspector` will have no effect on the + * processing of other requests or the period of intervals, making it ideal + * for more complex (e.g. isolated, per-endpoint) throttling requirements. + * + * If this function returns `undefined` or a promise that resolves to + * `undefined`, an internal `fetch()` will be made using the request params + * passed to (and potentially mutated by) this function. The fetch result will + * be passed to `responseInspector`. Otherwise, the resolved defined value of + * this function will be passed to `responseInspector` directly (no internal + * `fetch()` happens). + * + * If this function throws, the corresponding `addRequestToQueue` call will + * reject and `responseInspector` will not be called. + * + * @default defaultRequestInspector (see export) + */ + get requestInspector() { + return this.#requestInspector; + } + + set requestInspector(inspector) { + debug('set new requestInspector'); + this.#requestInspector = inspector; + } + + /** + * A function used to reshape response data before returning it through the + * resolved `addRequestToQueue` promise. This function must do one of the + * following before terminating: + * + * - Return a JSON representation of the response, e.g. `response.json()`. + * - Interpret and/or transform the response data and return any value. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * The return value of this function will eventually be used as the resolved + * value of the promise returned by the corresponding `addRequestToQueue` + * call that triggered it. Similarly, if this function throws, the + * corresponding `addRequestToQueue` call will reject. + * + * @default defaultResponseInspector (see export) + */ + get responseInspector() { + return this.#responseInspector; + } + + set responseInspector(inspector) { + debug('set new responseInspector'); + this.#responseInspector = inspector; + } + + /** + * A function used to take some action after the node-fetch `fetch` function + * rejects due to failure. Like `requestInspector`, this function must do one + * of the following before terminating: + * + * - Return a promise that resolves how you want. + * - Call `addRequestToQueue` again and return it (beware infinite loops). + * - Await a `setTimeout` promise to delay the request before continuing. + * - Throw an error causing the `addRequestToQueue` method to reject. + * + * Delaying a request using `fetchErrorInspector` will have no effect on the + * processing of other requests or the period of intervals, making it ideal to + * retry failed fetch requests. + * + * The resolved value of this function will always be passed to + * `responseInspector` directly (no additional internal `fetch()` happens) + * unless an error is thrown, in which case the `addRequestToQueue` return + * value will reject. + * + * @default defaultFetchErrorInspector (see export) + */ + get fetchErrorInspector() { + return this.#fetchErrorInspector; + } + + set fetchErrorInspector(inspector) { + debug('set new fetchErrorInspector'); + this.#fetchErrorInspector = inspector; + } + + /** + * Default request initialization parameters sent along with every request. + */ + get defaultRequestInit() { + return this.#defaultRequestInit; + } + + set defaultRequestInit(init) { + debug('set defaultRequestInit: %O => %O', this.#defaultRequestInit, init); + this.#defaultRequestInit = init; + } + + /** + * A maximum of `maxRequestsPerInterval` requests will be processed every + * `>=intervalPeriodMs` milliseconds. + */ + get maxRequestsPerInterval() { + return this.#maxRequestsPerInterval; + } + + set maxRequestsPerInterval(count) { + debug(`set maxRequestsPerInterval: ${this.#maxRequestsPerInterval} => ${count}`); + this.#maxRequestsPerInterval = count; + } + + /** + * A maximum of `maxRequestsPerInterval` requests will be processed every + * `>=intervalPeriodMs` milliseconds. + */ + get intervalPeriodMs() { + return this.#intervalPeriodMs; + } + + set intervalPeriodMs(period) { + debug(`set intervalPeriodMs: ${this.#intervalPeriodMs} => ${period}`); + this.#intervalPeriodMs = period; + } + + #scheduleNextInterval() { + this.#timeoutId = setTimeout( + this.#processRequestQueue.bind(this), + this.intervalPeriodMs + ); + + if (this.#queueProcessingIsSoftPaused) { + debug('queue processing unpaused'); + this.#queueProcessingIsSoftPaused = false; + } + + debug(`scheduled next interval in ${this.intervalPeriodMs}ms`); + } + + #finishGracefulStop() { + this.#timeoutId = null; + this.#queueProcessingIsSoftPaused = false; + + debug('queue processing stopped gracefully'); + + this.#queueStoppedPromiseResolver(); + } + + /** + * An internal function to execute requests present in the request queue with + * respect to constraints available in the response data. + */ + async #processRequestQueue() { + const subDebug = debug.extend(`interval#${++this.#debugIntervalCounter}`); + subDebug('entered queue processing interval function'); + + if (this.#requestQueue.length) { + let previousMaxRequestsPerInterval = this.maxRequestsPerInterval; + const terminationAbortController = this.#terminationAbortController; + + subDebug( + `processing at most ${previousMaxRequestsPerInterval} requests in this interval` + ); + + let count = 0; + + for (; count < this.maxRequestsPerInterval; ++count) { + const reqDebug = subDebug.extend(`request#${count + 1}`); + + if ( + !terminationAbortController.signal.aborted && + this.#delayRequestProcessingByMs + ) { + reqDebug('detected non-zero delayRequestProcessingByMs, processing paused'); + reqDebug(`resuming in ${this.#delayRequestProcessingByMs}ms...`); + + const previousDelayRequestProcessingByMs = this.#delayRequestProcessingByMs; + this.#delayRequestProcessingByMs = 0; + + // ? If we need to delay, pause processing request queue immediately + // eslint-disable-next-line no-await-in-loop + await wait(previousDelayRequestProcessingByMs, undefined, { + signal: terminationAbortController.signal + }); + + reqDebug(`processing resumed`); + + if (this.#delayRequestProcessingByMs !== 0) { + subDebug.warn( + `resetting delayRequestProcessingByMs after resuming queue processing` + ); + this.#delayRequestProcessingByMs = 0; + } + } + + if (previousMaxRequestsPerInterval !== this.maxRequestsPerInterval) { + subDebug.warn( + `maxRequestsPerInterval changed while processing request #${ + count + 1 + }: ${previousMaxRequestsPerInterval} => ${this.maxRequestsPerInterval}` + ); + previousMaxRequestsPerInterval = this.maxRequestsPerInterval; + } + + if (terminationAbortController.signal.aborted) { + // ? Immediately terminate request queue processing + reqDebug(`detected abort signal: queue processing will terminate`); + break; + } + + const enqueuedRequest = this.#requestQueue.shift(); + + if (!enqueuedRequest) { + reqDebug(`early end of queue: nothing left to process in this interval`); + break; + } + + const [fetchParams, callback, state] = enqueuedRequest; + + // ? Guarantees fetchParams[1] is non-nullish + fetchParams[1] = { + ...this.#defaultRequestInit, + ...fetchParams[1], + headers: new Headers( + Object.entries({ + // ? Ensure default headers can be overwritten + ...new Headers(this.#defaultRequestInit?.headers).raw(), + ...new Headers(fetchParams[1]?.headers).raw() + }).map(([k, v]) => [k, v.join(',')]) + ), + signal: terminationAbortController.signal + } as RequestInitWithSignal; + + const isFinal = count + 1 >= this.maxRequestsPerInterval; + + reqDebug(`preparing to fetch: ${fetchParams[0]}`); + + void (async () => { + try { + reqDebug('triggering request inspector'); + + const inspectorParams = { + queue: this, + requestInfo: fetchParams[0], + requestInit: fetchParams[1] as RequestInitWithSignal, + state + }; + + const requestInspectorResult = + await this.requestInspector(inspectorParams); + + let res: unknown; + + if (requestInspectorResult === undefined) { + try { + reqDebug(`internal fetch: ${inspectorParams.requestInfo}`); + this.#debugSentRequestCounter++; + + res = await fetch( + inspectorParams.requestInfo, + inspectorParams.requestInit + ); + + reqDebug('response received'); + } catch (error) { + reqDebug.error( + `triggering error inspector for in-flight request error: ${error}` + ); + + try { + res = await this.fetchErrorInspector({ + error: error, + queue: this, + requestInfo: inspectorParams.requestInfo, + requestInit: inspectorParams.requestInit, + state: inspectorParams.state + }); + } catch (error) { + reqDebug.error( + `unhandled error during in-flight request: ${error}` + ); + callback( + error instanceof Error + ? error + : /* istanbul ignore next */ + new HttpError(res as Response, String(error)) + ); + return; + } + } + } else { + reqDebug( + 'skipping internal fetch (due to defined requestInspector result)' + ); + res = requestInspectorResult; + } + + if (isFinal) { + reqDebug('this is the final request to be processed in this interval'); + } + + try { + reqDebug('triggering response inspector'); + + callback( + null, + await this.responseInspector({ + response: res, + queue: this, + requestInfo: inspectorParams.requestInfo, + requestInit: inspectorParams.requestInit, + state: inspectorParams.state + }) + ); + + reqDebug('finished processing request'); + } catch (error) { + reqDebug.error(`unhandled error during response inspection: ${error}`); + callback(error instanceof Error ? error : new HttpError(String(error))); + } + } catch (error) { + reqDebug.error(`unhandled error during request inspection: ${error}`); + callback(error instanceof Error ? error : new HttpError(String(error))); + } + })(); + } + + subDebug( + `processing complete: ${count} request${count > 1 ? 's' : ''} in-flight` + ); + + this.#scheduleNextInterval(); + } else if (this.#keepProcessingRequestQueue) { + subDebug('queue empty: nothing to process in this interval'); + + this.#queueProcessingIsSoftPaused = true; + + subDebug( + 'queue processing soft-paused (next interval will be scheduled on new request)' + ); + } else { + subDebug( + 'queue empty and graceful stop requested: queue processing will be terminated' + ); + + this.#finishGracefulStop(); + } + } + + /** + * Append a request to the request queue. This function returns a promise that + * will resolve with the request's response data as determined by + * `responseInspector`. + */ + addRequestToQueue(...params: ExtendedFetchParams): Promise { + debug('adding (appending) new request to queue'); + this.#debugAddRequestCounter++; + + if (this.#queueProcessingIsSoftPaused && this.#keepProcessingRequestQueue) { + this.#scheduleNextInterval(); + } + + return new Promise((resolve, reject) => { + this.#requestQueue.push([ + [params[0], params[1]], + (error, returnValue) => { + error ? reject(error) : resolve(returnValue as TT); + }, + params[2] || {} + ]); + }); + } + + /** + * Exactly the same as `addRequestToQueue` in every way, except the request is + * _prepended_ rather than appended to the queue. + */ + prependRequestToQueue(...params: ExtendedFetchParams): Promise { + debug('adding (prepending) new request to queue'); + this.#debugAddRequestCounter++; + + if (this.#queueProcessingIsSoftPaused && this.#keepProcessingRequestQueue) { + this.#scheduleNextInterval(); + } + + return new Promise((resolve, reject) => { + this.#requestQueue.unshift([ + [params[0], params[1]], + (error, returnValue) => { + error ? reject(error) : resolve(returnValue as TT); + }, + params[2] || {} + ]); + }); + } + + /** + * Calling this function will cause the request processor to wait `delay` + * milliseconds before sending any subsequent requests. Requests that have + * already been sent will resolve without delay. + * + * After the delay period transpires, the internal delay value will be reset + * to `0` _regardless of calls to `delayRequestProcessingByMs` in the + * interim_. Hence, due to the asynchronous nature of request processing, + * calling `delayRequestProcessingByMs` asynchronously (e.g. via + * `requestInspector` or `responseInspector`) **does not guarantee that the + * new value will be respected.** + * + * To implement backoff or other complex throttling functionality, consider + * instead using per-request delays manually (e.g. via `setTimeout`) at the + * `requestInspector` level. + */ + delayRequestProcessingByMs(delay: number) { + if (delay < 0) { + throw new RequestQueueError( + `delayRequestProcessingByMs must be a non-negative integer, saw ${delay} instead` + ); + } + + debug( + `set delayRequestProcessingByMs: ${this.#delayRequestProcessingByMs} => ${delay}` + ); + this.#delayRequestProcessingByMs = delay; + } + + /** + * Begin asynchronously processing the request queue. If the queue is already + * being processed, calling this function again will throw. + */ + beginProcessingRequestQueue() { + if (this.isProcessingRequestQueue) { + debug.error( + 'attempted to call beginProcessingRequestQueue while already processing request queue' + ); + throw new RequestQueueError('already processing request queue'); + } else { + debug(`beginning queue processing (${this.intervalPeriodMs}ms intervals)`); + + this.#keepProcessingRequestQueue = true; + this.#queueProcessingIsSoftPaused = false; + + this.#terminationAbortController = new AbortController(); + + this.#queueStoppedPromise = new Promise((resolve) => { + this.#queueStoppedPromiseResolver = resolve; + }); + + this.#scheduleNextInterval(); + } + } + + /** + * Signal to the queue processor that, once the queue is empty, request + * processing is to stop. This means no further requests will be dequeued or + * executed. + * + * Requests can still be added to the queue after request processing + * eventually stops (via `addRequestToQueue`), but they will not be dequeued + * and executed until `beginProcessingRequestQueue` is called again. + * + * This function will throw if called when the queue is not being processed. + */ + gracefullyStopProcessingRequestQueue() { + if (!this.isProcessingRequestQueue) { + debug.error( + 'attempted to call gracefullyStopProcessingRequestQueue when processing already stopped' + ); + throw new RequestQueueError('request queue processing already stopped'); + } + + debug('graceful stop signal received: processing will stop when queue is empty'); + + this.#keepProcessingRequestQueue = false; + + if (this.#queueProcessingIsSoftPaused) { + this.#finishGracefulStop(); + } + } + + /** + * Signal to the queue processor to stop all request processing immediately, + * regardless of if the queue is empty or not. After calling this method, no + * new or queued requests will be processed, though the queue is not cleared. + * Requests can still be added to the queue (via `addRequestToQueue`) but they + * will not be processed until `beginProcessingRequestQueue` is called again. + * + * If a request is in-flight when this method is called, the request will be + * aborted and the corresponding promise rejected with an `AbortError`. The + * aborted request must be re-added to the queue manually as it will not be + * retried automatically. + * + * This function will throw if called when the queue is not being processed. + */ + immediatelyStopProcessingRequestQueue() { + if (!this.isProcessingRequestQueue) { + debug.error( + 'attempted to call immediatelyStopProcessingRequestQueue when processing already stopped' + ); + throw new RequestQueueError('request queue processing already stopped'); + } + + debug('immediately halting queue processing and aborting in-flight requests'); + + this.#keepProcessingRequestQueue = false; + this.#queueProcessingIsSoftPaused = false; + + clearTimeout(this.#timeoutId as NodeJS.Timeout); + this.#timeoutId = null; + + this.#terminationAbortController.abort(); + + debug('queue processing halted abruptly and in-flight requests aborted'); + + this.#queueStoppedPromiseResolver(); + } + + /** + * Remove all pending and unprocessed requests from the request queue, + * rejecting their corresponding promises with a `RequestQueueClearedError`. + * In-flight requests will still complete unless + * `immediatelyStopProcessingRequestQueue` has been called. + */ + clearRequestQueue() { + const count = this.#requestQueue.length; + debug(`clearing request queue (${count} requests will reject)`); + + for (let index = 0; index < count; ++index) { + const [, callback] = + this.#requestQueue.shift() || + /* istanbul ignore next */ + toss(new GuruMeditationError('queue length invariant violated')); + callback(new RequestQueueClearedError()); + } + + debug('request queue cleared'); + } + + /** + * Returns a promise that resolves after queue processing stops. Before + calling this function, you should ensure that + `gracefullyStopProcessingRequestQueue` or + `immediatelyStopProcessingRequestQueue` have already been or will + eventually be called or this promise will never settle. + */ + waitForQueueProcessingToStop() { + debug('caller is waiting for queue processing to terminate...'); + return this.#queueStoppedPromise; + } + + /** + * Returns various statistics about the queue runtime. + */ + getStats() { + return { + intervals: this.#debugIntervalCounter, + requestsEnqueued: this.#debugAddRequestCounter, + internalRequestsSent: this.#debugSentRequestCounter + }; + } +} diff --git a/lib/throttled-fetch/package.json b/lib/throttled-fetch/package.json new file mode 100644 index 0000000..a9ae21e --- /dev/null +++ b/lib/throttled-fetch/package.json @@ -0,0 +1,3 @@ +{ + "name": "@xunnamius/throttled-fetch" +} diff --git a/lib/throttled-fetch/unit.test.ts b/lib/throttled-fetch/unit.test.ts new file mode 100644 index 0000000..4ff69f6 --- /dev/null +++ b/lib/throttled-fetch/unit.test.ts @@ -0,0 +1,1564 @@ +/* eslint-disable jest/unbound-method */ +/* eslint-disable jest/prefer-lowercase-title */ +import { asMockedFunction } from '@xunnamius/jest-types'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { DummyError, TrialError } from 'named-app-errors'; +import { Headers, Response } from 'node-fetch'; +import { setTimeout } from 'node:timers/promises'; + +import { + defaultRequestInspector, + defaultResponseInspector, + RequestQueue, + RequestQueueClearedError, + RequestQueueError +} from 'multiverse/throttled-fetch'; + +jest.mock('node:timers/promises'); + +const useFakeTimers = () => + jest.useFakeTimers({ + timerLimit: 10, + doNotFake: [ + 'Date', + 'hrtime', + 'nextTick', + 'performance', + 'queueMicrotask', + 'requestAnimationFrame', + 'cancelAnimationFrame', + 'requestIdleCallback', + 'cancelIdleCallback', + 'setImmediate', + 'clearImmediate', + 'setInterval', + 'clearInterval' + ] + }); + +const mockSetTimeout = asMockedFunction(setTimeout); + +const server = setupServer( + rest.all('*', async (req, res, context) => { + const { method, headers, params } = req; + const body = await req.text(); + + return res( + context.status( + body.startsWith('status=') + ? Number.parseInt(body.split('status=').at(-1) || '200') + : 200 + ), + context.json({ method, headers: headers.raw(), params, body }) + ); + }) +); + +/** + * Ensure that a promise does not settle before it's supposed to. + * + * @returns a `setThreshold` function and a `sentinel` function (via array) + */ +const promiseSettledSentinel = () => { + let threshold = 0; + + return [ + (newThreshold: number) => (threshold = newThreshold), + { + resolve(count = Number.POSITIVE_INFINITY) { + return (result: T) => { + if (count > threshold) { + throw new TrialError( + `promise #${count} resolved before it was supposed to` + ); + } + return Promise.resolve(result); + }; + }, + + reject(count = Number.POSITIVE_INFINITY) { + return (result: T) => { + if (count > threshold) { + throw new TrialError( + `promise #${count} rejected before it was supposed to` + ); + } + return Promise.reject(result); + }; + } + } + ] as const; +}; + +/** + * Ensure no new intervals are being processed by the given queue. This function + * should be called at the very end of your test when you want to ensure queue + * processing has terminated. **Otherwise, you'll have to deal with an extra + * enqueued request.** + */ +const assertRequestQueueProcessingStopped = (queue: RequestQueue) => { + const mockSetTimeoutCalls = mockSetTimeout.mock.calls.length; + const previousRequestDelayMs = queue.requestProcessingDelayMs; + + // ? First, ensure graceful stop happens + jest.advanceTimersByTime(queue.intervalPeriodMs); + + // ? Now, if we didn't stop, then this request delay will trip mockSetTimeout + queue.delayRequestProcessingByMs(1000); + void queue.addRequestToQueue('https://fake-url'); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + expect(mockSetTimeout).toBeCalledTimes(mockSetTimeoutCalls); + expect(queue.isProcessingRequestQueue).toBeFalse(); + + // ? Reset things back to the way they were + queue.delayRequestProcessingByMs(previousRequestDelayMs); +}; + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); + +beforeEach(() => { + mockSetTimeout.mockReturnValue(Promise.resolve()); + useFakeTimers(); +}); + +afterEach(() => { + server.resetHandlers(); + jest.clearAllTimers(); + jest.useRealTimers(); +}); + +afterAll(() => server.close()); + +describe('RequestQueue::constructor', () => { + it('can be initialized without inspectors', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 30 + }); + + expect(queue.intervalPeriodMs).toBe(1000); + expect(queue.maxRequestsPerInterval).toBe(30); + expect(queue.requestInspector).toBe(defaultRequestInspector); + expect(queue.responseInspector).toBe(defaultResponseInspector); + }); + + it('can be initialized with inspectors', async () => { + expect.hasAssertions(); + + const requestInspector = () => undefined; + const responseInspector = () => undefined; + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 30, + requestInspector, + responseInspector + }); + + expect(queue.intervalPeriodMs).toBe(1000); + expect(queue.maxRequestsPerInterval).toBe(30); + expect(queue.requestInspector).toBe(requestInspector); + expect(queue.responseInspector).toBe(responseInspector); + }); + + it('begins processing queue upon instantiation and gracefully ends automatically if autoStart is true', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + const pRes = queue.addRequestToQueue('https://fake-url'); + jest.advanceTimersByTime(2 * queue.intervalPeriodMs); + await expect(pRes).resolves.toBeInstanceOf(Response); + + assertRequestQueueProcessingStopped(queue); + }); +}); + +describe('RequestQueue::addRequestToQueue', () => { + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { addRequestToQueue } = queue; + const pRes = addRequestToQueue('https://fake-url'); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersToNextTimer(); + await expect(pRes).resolves.toBeInstanceOf(Response); + }); + + it('can override return type information provided at instantiation', async () => { + expect.assertions(0); + + const { addRequestToQueue } = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const pRes = addRequestToQueue<{ a: 1 }>('https://fake-url'); + const res = pRes as unknown as Awaited; + + void res.a; + // @ts-expect-error: if an error occurs, then the type check was successful! + void res.json; + }); +}); + +describe('RequestQueue::prependRequestToQueue', () => { + it('prepends new requests to the queue', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pRes1 = queue + .prependRequestToQueue('https://fake-url-2') + .then(sentinel.resolve(2), sentinel.reject()); + + const pRes2 = queue + .addRequestToQueue('https://fake-url-1') + .then(sentinel.resolve(3), sentinel.reject()); + + const pRes3 = queue + .prependRequestToQueue('https://fake-url-3') + .then(sentinel.resolve(1), sentinel.reject()); + + setThreshold(1); + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pRes3).resolves.toBeInstanceOf(Response); + + setThreshold(2); + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pRes1).resolves.toBeInstanceOf(Response); + + setThreshold(3); + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pRes2).resolves.toBeInstanceOf(Response); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { prependRequestToQueue } = queue; + const pRes = prependRequestToQueue('https://fake-url'); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersToNextTimer(); + await expect(pRes).resolves.toBeInstanceOf(Response); + }); + + it('can override return type information provided at instantiation', async () => { + expect.assertions(0); + + const { prependRequestToQueue } = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const pRes = prependRequestToQueue<{ a: 1 }>('https://fake-url'); + const res = pRes as unknown as Awaited; + + void res.a; + // @ts-expect-error: if an error occurs, then the type check was successful! + void res.json; + }); +}); + +describe('RequestQueue::delayRequestProcessingByMs', () => { + it('causes delaying of entire request processing routine when set', async () => { + expect.hasAssertions(); + + let delayPromiseResolver: ((value?: unknown) => void) | undefined; + + mockSetTimeout.mockImplementation((_, __) => { + return new Promise((resolve) => { + delayPromiseResolver = resolve; + }); + }); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pRes1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(1), sentinel.reject()); + + queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(), sentinel.reject()); + + jest.advanceTimersByTime(10 * queue.intervalPeriodMs); + + expect(delayPromiseResolver).toBeDefined(); + + setThreshold(1); + delayPromiseResolver?.(); + + await expect(pRes1).resolves.toBeInstanceOf(Response); + }); + + it('is reset before a delay transpires', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + void queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + expect(queue.requestProcessingDelayMs).toBe(0); + }); + + it('is reset after a delay transpires, preventing unnecessary slowdowns', async () => { + expect.hasAssertions(); + + let delayPromiseResolver: ((value?: unknown) => void) | undefined; + + mockSetTimeout.mockImplementation((_, __) => { + return new Promise((resolve) => { + delayPromiseResolver = resolve; + }); + }); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + const pRes1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + expect(delayPromiseResolver).toBeDefined(); + + queue.delayRequestProcessingByMs(10_000); + delayPromiseResolver?.(); + + await expect(pRes1).resolves.toBeInstanceOf(Response); + expect(queue.requestProcessingDelayMs).toBe(0); + }); + + it('throws when attempting to set negative delay', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + expect(() => queue.delayRequestProcessingByMs(-1)).toThrow(RequestQueueError); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { delayRequestProcessingByMs } = queue; + expect(() => delayRequestProcessingByMs(-1)).toThrow(RequestQueueError); + }); +}); + +describe('RequestQueue::beginProcessingRequestQueue', () => { + it('processes requests added to the queue', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const requests = [ + queue + .addRequestToQueue('https://fake-url', { method: 'GET' }) + .then(sentinel.resolve(1)), + queue + .addRequestToQueue('https://fake-url', { method: 'POST' }) + .then(sentinel.resolve(2)), + queue + .addRequestToQueue('https://fake-url', { method: 'PUT' }) + .then(sentinel.resolve(3)), + queue + .addRequestToQueue('https://fake-url', { method: 'PATCH' }) + .then(sentinel.resolve(4)), + queue + .addRequestToQueue('https://fake-url', { method: 'DELETE' }) + .then(sentinel.resolve(5)) + ]; + + queue.beginProcessingRequestQueue(); + + setThreshold(1); + jest.advanceTimersByTime(1 * queue.intervalPeriodMs); + + await expect((await requests[0]).json()).resolves.toHaveProperty('method', 'GET'); + + setThreshold(2); + jest.advanceTimersByTime(1 * queue.intervalPeriodMs); + + await expect((await requests[1]).json()).resolves.toHaveProperty( + 'method', + 'POST' + ); + + setThreshold(5); + jest.advanceTimersByTime(3 * queue.intervalPeriodMs); + + await expect((await requests[2]).json()).resolves.toHaveProperty('method', 'PUT'); + await expect((await requests[3]).json()).resolves.toHaveProperty( + 'method', + 'PATCH' + ); + + await expect((await requests[4]).json()).resolves.toHaveProperty( + 'method', + 'DELETE' + ); + }); + + it('respects headers passed to addRequestToQueue', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 3 + }); + + const requests = [ + queue.addRequestToQueue('https://fake-url', { + method: 'GET', + headers: { a: '1' } + }), + queue.addRequestToQueue('https://fake-url', { + method: 'GET', + headers: [['b', '2']] + }), + queue.addRequestToQueue('https://fake-url', { + method: 'GET', + headers: new Headers({ c: '3' }) + }) + ].map((req) => req.then((res) => res.json())); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + const responses = await Promise.all(requests); + + expect(responses[0]).toHaveProperty( + 'headers', + expect.objectContaining({ a: '1' }) + ); + + expect(responses[1]).toHaveProperty( + 'headers', + expect.objectContaining({ b: '2' }) + ); + + expect(responses[2]).toHaveProperty( + 'headers', + expect.objectContaining({ c: '3' }) + ); + }); + + it('keeps processing even if queue is empty', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(3 * queue.intervalPeriodMs); + + const pReq = queue.addRequestToQueue('https://fake-url', { method: 'GET' }); + jest.advanceTimersByTime(3 * queue.intervalPeriodMs); + await expect((await pReq).json()).resolves.toHaveProperty('method', 'GET'); + + const pReq2 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + jest.advanceTimersByTime(3 * queue.intervalPeriodMs); + await expect((await pReq2).json()).resolves.toHaveProperty('method', 'POST'); + }); + + it('passes state from addRequestToQueue all the way through the inspectors', async () => { + expect.hasAssertions(); + + const globalState = { state: 'stateful' }; + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 2, + autoStart: true, + requestInspector: ({ state }) => { + state.stateless = false; + return 'custom-value'; + }, + responseInspector: ({ response: val, state }) => { + state.val = val; + return state; + } + }); + + const pReq1 = queue.addRequestToQueue('https://fake-url', undefined, globalState); + const pReq2 = queue.addRequestToQueue('https://fake-url', undefined, { + localState: true + }); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq1).resolves.toBe(globalState); + await expect(pReq1).resolves.toStrictEqual({ + state: 'stateful', + stateless: false, + val: 'custom-value' + }); + + await expect(pReq2).resolves.not.toBe(globalState); + await expect(pReq2).resolves.toStrictEqual({ + localState: true, + stateless: false, + val: 'custom-value' + }); + }); + + it('throws if called while already processing queue', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + expect(() => queue.beginProcessingRequestQueue()).toThrow(RequestQueueError); + }); + + it('causes addRequestToQueue promise to reject when fetch function rejects', async () => { + expect.hasAssertions(); + + const responseInspector = jest.fn(); + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + responseInspector + }); + + queue.beginProcessingRequestQueue(); + + const pReq = queue.addRequestToQueue('bad-url'); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq).rejects.toBeInstanceOf(Error); + expect(responseInspector).not.toHaveBeenCalled(); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { beginProcessingRequestQueue, intervalPeriodMs } = queue; + const pRes = queue.addRequestToQueue('https://fake-url', { method: 'GET' }); + + beginProcessingRequestQueue(); + jest.advanceTimersByTime(intervalPeriodMs); + await expect(pRes).resolves.toBeInstanceOf(Response); + }); +}); + +describe('RequestQueue::gracefullyStopProcessingRequestQueue', () => { + it('terminates after request queue is exhausted but still allows queue processing to begin again later', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + jest.advanceTimersByTime(3 * queue.intervalPeriodMs); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + + const pReq2 = queue.addRequestToQueue('https://fake-url'); + + queue.gracefullyStopProcessingRequestQueue(); + jest.advanceTimersByTime(1 * queue.intervalPeriodMs); + + await expect(pReq2).resolves.toBeInstanceOf(Response); + + assertRequestQueueProcessingStopped(queue); + + const pReq3 = queue.addRequestToQueue('https://fake-url'); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(2 * queue.intervalPeriodMs); + await expect(pReq3).resolves.toBeInstanceOf(Response); + }); + + it('does not abort in-flight requests', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + queue.gracefullyStopProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + const pReq2 = queue.addRequestToQueue('https://fake-url'); + const pReq3 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(4 * queue.intervalPeriodMs); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + await expect(pReq2).resolves.toBeInstanceOf(Response); + await expect(pReq3).resolves.toBeInstanceOf(Response); + + assertRequestQueueProcessingStopped(queue); + }); + + it('throws if queue processing already stopped', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + expect(() => queue.gracefullyStopProcessingRequestQueue()).toThrow( + RequestQueueError + ); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { gracefullyStopProcessingRequestQueue, intervalPeriodMs } = queue; + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(intervalPeriodMs); + gracefullyStopProcessingRequestQueue(); + + assertRequestQueueProcessingStopped(queue); + }); +}); + +describe('RequestQueue::immediatelyStopProcessingRequestQueue', () => { + it('terminates immediately even if in the middle of a delay timeout', async () => { + expect.hasAssertions(); + + let abortSignal: AbortSignal | undefined; + let delayPromiseResolver: ((value?: unknown) => void) | undefined; + + mockSetTimeout.mockImplementation((_, __, options) => { + abortSignal = options?.signal; + return new Promise((resolve) => { + delayPromiseResolver = resolve; + }); + }); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 2 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + expect(mockSetTimeout).toBeCalledTimes(0); + + const [, sentinel] = promiseSettledSentinel(); + + queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(), sentinel.reject()); + queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(), sentinel.reject()); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + expect(mockSetTimeout).toBeCalledTimes(1); + expect(abortSignal?.aborted).toBeFalse(); + expect(delayPromiseResolver).toBeDefined(); + + queue.immediatelyStopProcessingRequestQueue(); + + expect(abortSignal?.aborted).toBeTrue(); + delayPromiseResolver?.(); + + assertRequestQueueProcessingStopped(queue); + }); + + it('terminates immediately even if queue is not empty and in-flight requests are pending but still allows queue processing to resume later', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pReq1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(1), sentinel.reject(1)); + const pReq2 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(2), sentinel.reject(2)); + const pReq3 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(3), sentinel.reject(3)); + + setThreshold(1); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + queue.immediatelyStopProcessingRequestQueue(); + + await expect(pReq1).rejects.toMatchObject({ name: 'AbortError' }); + + setThreshold(2); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq2).resolves.toBeInstanceOf(Response); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + queue.immediatelyStopProcessingRequestQueue(); + + setThreshold(3); + + await expect(pReq3).rejects.toMatchObject({ name: 'AbortError' }); + assertRequestQueueProcessingStopped(queue); + }); + + it('throws if queue processing already stopped', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + expect(() => queue.immediatelyStopProcessingRequestQueue()).toThrow( + RequestQueueError + ); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { immediatelyStopProcessingRequestQueue, intervalPeriodMs } = queue; + + queue.beginProcessingRequestQueue(); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + const pReq1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(), sentinel.reject(1)); + + jest.advanceTimersByTime(intervalPeriodMs); + immediatelyStopProcessingRequestQueue(); + + setThreshold(1); + + await expect(pReq1).rejects.toMatchObject({ name: 'AbortError' }); + assertRequestQueueProcessingStopped(queue); + }); +}); + +describe('RequestQueue::clearRequestQueue', () => { + it('clears only pending/unprocessed requests from the queue and rejects them', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + const pReq2 = queue.addRequestToQueue('https://fake-url'); + const pReq3 = queue.addRequestToQueue('https://fake-url'); + const pReq4 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + queue.clearRequestQueue(); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + await expect(pReq2).rejects.toBeInstanceOf(RequestQueueClearedError); + await expect(pReq3).rejects.toBeInstanceOf(RequestQueueClearedError); + await expect(pReq4).rejects.toBeInstanceOf(RequestQueueClearedError); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { clearRequestQueue } = queue; + const pReq = queue.addRequestToQueue('https://fake-url'); + + clearRequestQueue(); + + await expect(pReq).rejects.toBeInstanceOf(RequestQueueClearedError); + }); +}); + +describe('RequestQueue::waitForQueueProcessingToStop', () => { + it('resolves only after queue processing is stopped', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + queue.gracefullyStopProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(4 * queue.intervalPeriodMs); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + await expect(queue.waitForQueueProcessingToStop()).resolves.toBeUndefined(); + + assertRequestQueueProcessingStopped(queue); + }); + + it('is auto-bound at instantiation', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const { waitForQueueProcessingToStop, intervalPeriodMs } = queue; + + queue.beginProcessingRequestQueue(); + queue.gracefullyStopProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(2 * intervalPeriodMs); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + await expect(waitForQueueProcessingToStop()).resolves.toBeUndefined(); + + assertRequestQueueProcessingStopped(queue); + }); +}); + +describe('RequestQueue::getStats', () => { + it('returns accurate statistics about the queue runtime', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + queue.gracefullyStopProcessingRequestQueue(); + + void queue.addRequestToQueue('https://fake-url'); + void queue.addRequestToQueue('https://fake-url'); + void queue.addRequestToQueue('https://fake-url'); + void queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(10 * queue.intervalPeriodMs); + + await queue.waitForQueueProcessingToStop(); + void queue.addRequestToQueue('https://fake-url'); + + expect(queue.getStats()).toStrictEqual({ + intervals: 5, + requestsEnqueued: 5, + internalRequestsSent: 4 + }); + }); +}); + +describe('RequestQueue::isProcessingRequestQueue', () => { + it('returns true when request queue is being processed and false otherwise', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + expect(queue.isProcessingRequestQueue).toBeTrue(); + + queue.gracefullyStopProcessingRequestQueue(); + expect(queue.isProcessingRequestQueue).toBeTrue(); + + queue.immediatelyStopProcessingRequestQueue(); + expect(queue.isProcessingRequestQueue).toBeFalse(); + }); +}); + +describe('RequestQueue::requestInspector', () => { + it('causes addRequestToQueue promise to resolve with return value', async () => { + expect.hasAssertions(); + + const globalState = {}; + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true, + requestInspector: ({ queue: q, requestInfo, requestInit, state }) => { + expect(q).toBe(queue); + expect(requestInfo).toBe('https://fake-url'); + expect(requestInit.method).toBe('POST'); + expect(state).toStrictEqual(globalState); + + return { hello: 'world' }; + } + }); + + const pReq1 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).resolves.toStrictEqual({ hello: 'world' }); + }); + + it('causes addRequestToQueue promise to reject when rejected or when Error is thrown', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.requestInspector = async () => { + throw new DummyError('bad'); + }; + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).rejects.toThrow(/bad/); + + queue.requestInspector = () => { + throw new DummyError('worse'); + }; + + const pReq2 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq2).rejects.toThrow(/worse/); + }); + + it('causes addRequestToQueue promise to reject with HttpError if non-Error is thrown/rejected', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true, + requestInspector: async () => { + throw 'goodbye, world!'; + } + }); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq1).rejects.toMatchObject({ + name: 'HttpError', + message: expect.stringContaining('goodbye, world!') + }); + + queue.requestInspector = () => { + throw 'goodbye, cruel world!'; + }; + + const pReq2 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq2).rejects.toMatchObject({ + name: 'HttpError', + message: expect.stringContaining('goodbye, cruel world!') + }); + }); + + it('can leverage state parameter and queue methods', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.requestInspector = async ({ queue: q, state }) => { + if (state.hello === 'world') { + const pReqRedirect = q.addRequestToQueue('https://rake-url', { + method: 'PUT', + body: 'status=403' + }); + + return Promise.resolve().then(() => { + jest.advanceTimersByTime(queue.intervalPeriodMs); + return pReqRedirect; + }); + } + }; + + const pReq1 = queue.addRequestToQueue( + 'https://fake-url', + { method: 'POST' }, + { hello: 'world' } + ); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + const req1 = await pReq1; + + expect(req1.url).toBe('https://rake-url/'); + expect(req1.status).toBe(403); + await expect(req1.json()).resolves.toHaveProperty('method', 'PUT'); + }); + + it('can mutate request parameters', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.requestInspector = async (params) => { + params.requestInfo = 'https://rake-url'; + params.requestInit.method = 'GET'; + params.requestInit.headers = new Headers({ somewhere: 'in a better place' }); + }; + + const pReq1 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + jest.advanceTimersByTime(queue.intervalPeriodMs); + const req1 = await pReq1; + const json = await req1.json(); + + expect(req1.url).toBe('https://rake-url/'); + expect(json.method).toBe('GET'); + expect(json.headers).toMatchObject({ somewhere: 'in a better place' }); + }); + + it('skips internal fetch when passing defined return value', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.requestInspector = () => { + return { short: 'circuit' }; + }; + + const pReq1 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq1).resolves.toStrictEqual({ short: 'circuit' }); + }); +}); + +describe('RequestQueue::responseInspector', () => { + it('causes addRequestToQueue promise to resolve with return value', async () => { + expect.hasAssertions(); + + const globalState = {}; + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true, + responseInspector: ({ + response, + queue: q, + requestInfo, + requestInit, + state + }) => { + expect(response).toBeInstanceOf(Response); + expect(q).toBe(queue); + expect(requestInfo).toBe('https://fake-url'); + expect(requestInit.method).toBe('POST'); + expect(state).toStrictEqual(globalState); + + return { hello: 'world' }; + } + }); + + const pReq1 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).resolves.toStrictEqual({ hello: 'world' }); + }); + + it('causes addRequestToQueue promise to reject when rejected or when Error is thrown', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.responseInspector = async () => { + throw new DummyError('bad'); + }; + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).rejects.toThrow(/bad/); + + queue.responseInspector = () => { + throw new DummyError('worse'); + }; + + const pReq2 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq2).rejects.toThrow(/worse/); + }); + + it('causes addRequestToQueue promise to reject with HttpError if non-Error is thrown/rejected', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true, + responseInspector: async () => { + throw 'goodbye, world!'; + } + }); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq1).rejects.toMatchObject({ + name: 'HttpError', + message: expect.stringContaining('goodbye, world!') + }); + + queue.responseInspector = () => { + throw 'goodbye, cruel world!'; + }; + + const pReq2 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq2).rejects.toMatchObject({ + name: 'HttpError', + message: expect.stringContaining('goodbye, cruel world!') + }); + }); + + it('can leverage state parameter and queue methods', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1, + autoStart: true + }); + + queue.responseInspector = async ({ response, queue: q, state }) => { + const res = response as Response; + + if (res.status === 200) { + const pReqRetry = q.addRequestToQueue( + 'https://fake-url', + { method: 'POST', body: 'status=403' }, + { isARetry: true } + ); + + return Promise.resolve().then(() => { + jest.advanceTimersByTime(queue.intervalPeriodMs); + return pReqRetry; + }); + } + + return `hello, ${state.isARetry ? 'retry' : 'world'}!`; + }; + + const pReq1 = queue.addRequestToQueue('https://fake-url', { method: 'POST' }); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).resolves.toBe('hello, retry!'); + }); +}); + +describe('RequestQueue::defaultRequestInit', () => { + test('default request init parameters are passed along with every request', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.defaultRequestInit = { method: 'POST', body: 'yes' }; + queue.beginProcessingRequestQueue(); + + const pReq1 = queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect((await pReq1).json()).resolves.toStrictEqual( + expect.objectContaining({ method: 'POST', body: 'yes' }) + ); + }); + + test('default headers coexist peacefully with headers passed to addRequestToQueue', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + queue.beginProcessingRequestQueue(); + + queue.defaultRequestInit.headers = { a: '1' }; + const pReq1 = queue.addRequestToQueue('https://fake-url'); + const pReq11 = queue.addRequestToQueue('https://fake-url', { + headers: [ + ['a', 'one'], + ['z', 'Z'] + ] + }); + + jest.advanceTimersByTime(2 * queue.intervalPeriodMs); + + queue.defaultRequestInit.headers = [['b', '2']]; + const pReq2 = queue.addRequestToQueue('https://fake-url'); + const pReq22 = queue.addRequestToQueue('https://fake-url', { + headers: new Headers([ + ['b', 'two'], + ['z', 'Z'] + ]) + }); + + jest.advanceTimersByTime(2 * queue.intervalPeriodMs); + + queue.defaultRequestInit.headers = new Headers({ c: '3' }); + const pReq3 = queue.addRequestToQueue('https://fake-url'); + const pReq33 = queue.addRequestToQueue('https://fake-url', { + headers: { c: 'three', z: 'Z' } + }); + + jest.advanceTimersByTime(2 * queue.intervalPeriodMs); + + await expect((await pReq1).json()).resolves.toStrictEqual( + expect.objectContaining({ headers: expect.objectContaining({ a: '1' }) }) + ); + + await expect((await pReq11).json()).resolves.toStrictEqual( + expect.objectContaining({ + headers: expect.objectContaining({ a: 'one', z: 'Z' }) + }) + ); + + await expect((await pReq2).json()).resolves.toStrictEqual( + expect.objectContaining({ headers: expect.objectContaining({ b: '2' }) }) + ); + + await expect((await pReq22).json()).resolves.toStrictEqual( + expect.objectContaining({ + headers: expect.objectContaining({ b: 'two', z: 'Z' }) + }) + ); + + await expect((await pReq3).json()).resolves.toStrictEqual( + expect.objectContaining({ headers: expect.objectContaining({ c: '3' }) }) + ); + + await expect((await pReq33).json()).resolves.toStrictEqual( + expect.objectContaining({ + headers: expect.objectContaining({ c: 'three', z: 'Z' }) + }) + ); + }); +}); + +describe('RequestQueue::maxRequestsPerInterval', () => { + it('determines the maximum number of requests processed per interval', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 1 + }); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pReq1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(1), sentinel.reject(1)); + const pReq2 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(2), sentinel.reject(2)); + const pReq3 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(3), sentinel.reject(3)); + const pReq4 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(4), sentinel.reject(4)); + const pReq5 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(5), sentinel.reject(5)); + const pReq6 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(6), sentinel.reject(6)); + + setThreshold(1); + + queue.beginProcessingRequestQueue(); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq1).resolves.toBeInstanceOf(Response); + + setThreshold(3); + + queue.maxRequestsPerInterval = 2; + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq2).resolves.toBeInstanceOf(Response); + await expect(pReq3).resolves.toBeInstanceOf(Response); + + setThreshold(6); + + queue.maxRequestsPerInterval = 3; + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq4).resolves.toBeInstanceOf(Response); + await expect(pReq5).resolves.toBeInstanceOf(Response); + await expect(pReq6).resolves.toBeInstanceOf(Response); + }); + + it('can be changed in the midst of interval processing', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 2 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pReq1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(1), sentinel.reject(1)); + + const pReq2 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(2), sentinel.reject(2)); + + const pReq3 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(3), sentinel.reject(3)); + + const pReq4 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(4), sentinel.reject(4)); + + setThreshold(1); + + jest.advanceTimersByTime(queue.intervalPeriodMs); + queue.maxRequestsPerInterval = 1; + + await expect(pReq1).resolves.toBeInstanceOf(Response); + + queue.delayRequestProcessingByMs(1000); + jest.advanceTimersByTime(queue.intervalPeriodMs); + queue.maxRequestsPerInterval = 3; + + setThreshold(4); + + await expect(pReq2).resolves.toBeInstanceOf(Response); + await expect(pReq3).resolves.toBeInstanceOf(Response); + await expect(pReq4).resolves.toBeInstanceOf(Response); + }); + + it('keeps processing even if queue is smaller than maxRequestsPerInterval', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 1000, + maxRequestsPerInterval: 100 + }); + + queue.beginProcessingRequestQueue(); + + const [setThreshold, sentinel] = promiseSettledSentinel(); + + const pReq1 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(1), sentinel.reject(1)); + + setThreshold(1); + jest.advanceTimersByTime(queue.intervalPeriodMs); + await expect(pReq1).resolves.toBeInstanceOf(Response); + + // ? Some time passes... + jest.advanceTimersByTime(5 * queue.intervalPeriodMs); + + const pReq2 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(2), sentinel.reject(2)); + + const pReq3 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(3), sentinel.reject(3)); + + const pReq4 = queue + .addRequestToQueue('https://fake-url') + .then(sentinel.resolve(4), sentinel.reject(4)); + + setThreshold(4); + jest.advanceTimersByTime(queue.intervalPeriodMs); + + await expect(pReq2).resolves.toBeInstanceOf(Response); + await expect(pReq3).resolves.toBeInstanceOf(Response); + await expect(pReq4).resolves.toBeInstanceOf(Response); + }); +}); + +describe('RequestQueue::intervalPeriodMs', () => { + it('determines the delay period between intervals', async () => { + expect.hasAssertions(); + + const queue = new RequestQueue({ + intervalPeriodMs: 10_000, + maxRequestsPerInterval: 1 + }); + + queue.delayRequestProcessingByMs(1000); + queue.beginProcessingRequestQueue(); + + expect(mockSetTimeout).toBeCalledTimes(0); + + const [, sentinel] = promiseSettledSentinel(); + + void queue.addRequestToQueue('https://fake-url').then( + sentinel.resolve(1), + // ? Since this request will get sent, the halt below will abort it + sentinel.resolve(0) + ); + + jest.advanceTimersByTime(10_000); + expect(mockSetTimeout).toBeCalledTimes(1); + + void queue.addRequestToQueue('https://fake-url'); + + // ? Wait for the current interval microtask to finish before continuing + await Promise.resolve().then(() => queue.delayRequestProcessingByMs(1000)); + + jest.advanceTimersByTime(2500); + expect(mockSetTimeout).toBeCalledTimes(1); + jest.advanceTimersByTime(2500); + expect(mockSetTimeout).toBeCalledTimes(1); + jest.advanceTimersByTime(5000); + expect(mockSetTimeout).toBeCalledTimes(2); + + queue.immediatelyStopProcessingRequestQueue(); + + queue.delayRequestProcessingByMs(1000); + queue.intervalPeriodMs = 500; + + queue.beginProcessingRequestQueue(); + + void queue.addRequestToQueue('https://fake-url'); + + jest.advanceTimersByTime(250); + expect(mockSetTimeout).toBeCalledTimes(2); + jest.advanceTimersByTime(250); + expect(mockSetTimeout).toBeCalledTimes(3); + }); +}); diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 0000000..203b317 --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * These scripts are the constituent parts of the `npm run format` command. + */ +module.exports = { + '*.md': [ + 'npx remark --output --ignore-path=.prettierignore --silently-ignore', + 'npx doctoc --no-title --maxlevel=3 --update-only' + ], + 'package.json': 'sort-package-json', + '*': 'prettier --write --ignore-unknown' +}; diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..3d646cf --- /dev/null +++ b/next.config.js @@ -0,0 +1,49 @@ +'use strict'; + +const withBundleAnalyzer = require('@next/bundle-analyzer'); +const { verifyEnvironment } = require('./expect-env'); + +verifyEnvironment(); + +module.exports = () => { + return withBundleAnalyzer({ + enabled: process.env.ANALYZE === 'true' + })({ + // ? Renames the build dir "build" instead of ".next" + distDir: 'build', + + // ? Select some environment variables defined in .env to push to the + // ? client. + // !! DO NOT PUT ANY SECRET ENVIRONMENT VARIABLES HERE !! + env: { + RESULTS_PER_PAGE: process.env.RESULTS_PER_PAGE, + IGNORE_RATE_LIMITS: process.env.IGNORE_RATE_LIMITS, + LOCKOUT_ALL_CLIENTS: process.env.LOCKOUT_ALL_CLIENTS, + DISALLOWED_METHODS: process.env.DISALLOWED_METHODS, + MAX_CONTENT_LENGTH_BYTES: process.env.MAX_CONTENT_LENGTH_BYTES + }, + + eslint: { + // ! This prevents production builds from failing in the presence of + // ! ESLint errors; linting is handled during CL/CI rather than at deploy + // ! time. + ignoreDuringBuilds: true + }, + + typescript: { + // ! This prevents production builds from failing in the presence of + // ! TypeScript errors, e.g. when modules from dev deps cannot be found; + // ! linting is handled during CL/CI rather than at deploy time. + ignoreBuildErrors: true + }, + + async rewrites() { + return [ + { + source: '/:path*', + destination: '/api/:path*' + } + ]; + } + }); +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..79ea5d0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31417 @@ +{ + "name": "inbdpa.api.hscc.bdpa.org", + "version": "1.1.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "inbdpa.api.hscc.bdpa.org", + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.6", + "@babel/plugin-proposal-export-default-from": "^7.24.6", + "@babel/plugin-proposal-function-bind": "^7.24.6", + "@babel/plugin-transform-react-jsx-source": "^7.24.6", + "@babel/preset-env": "^7.24.6", + "@babel/preset-react": "^7.24.6", + "@babel/preset-typescript": "^7.24.6", + "@next/bundle-analyzer": "^14.2.3", + "@octokit/rest": "^20.1.1", + "@types/bytes": "^3.1.4", + "@types/cors": "^2.8.17", + "@types/debug": "^4.1.12", + "@types/folder-hash": "^4.0.4", + "@types/node": "^20.12.12", + "@types/react": "^18.3.3", + "@types/request-ip": "^0.0.41", + "@xunnamius/babel-preset-next-babel": "^1.0.1", + "@xunnamius/next-types": "^1.0.9", + "@xunnamius/types": "^1.3.0", + "babel-plugin-explicit-exports-references": "^1.0.2", + "babel-plugin-transform-default-named-imports": "^1.2.2", + "bytes": "^3.1.2", + "clone-deep": "^4.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "execa": "^5.1.1", + "find-up": "^5.0.0", + "is-plain-object": "^5.0.0", + "is-server-side": "^1.0.2", + "mongodb": "^6.6.2", + "named-app-errors": "^4.0.2", + "next": "^14.2.3", + "node-fetch": "cjs", + "raw-body": "^2.5.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-use": "^17.5.0", + "request-ip": "3.3.0", + "swr": "^2.2.5", + "toss-expression": "^0.1.2", + "typescript": "^5.4.5" + }, + "devDependencies": { + "@babel/cli": "^7.24.6", + "@babel/eslint-parser": "^7.24.6", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@faker-js/faker": "^8.4.1", + "@next/eslint-plugin-next": "^14.2.3", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@types/clone-deep": "^4.0.4", + "@types/confusing-browser-globals": "^1.0.3", + "@types/content-type": "^1.1.8", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.12", + "@types/jsonfile": "^6.1.4", + "@types/node-fetch": "^2.5.12", + "@types/semver": "^7.5.8", + "@types/tar-stream": "^3.1.3", + "@types/test-listen": "^1.1.2", + "@types/webpack": "^5.28.5", + "@typescript-eslint/eslint-plugin": "^7.11.0", + "@typescript-eslint/parser": "^7.11.0", + "@xunnamius/conventional-changelog-projector": "^1.2.1", + "@xunnamius/jest-types": "^1.1.3", + "auto-bind": "^4.0.0", + "babel-jest": "^29.7.0", + "babel-loader": "^9.1.3", + "chalk": "^4.1.2", + "confusing-browser-globals": "^1.0.11", + "conventional-changelog-cli": "^5.0.0", + "del": "^7.1.0", + "doctoc": "^2.2.1", + "dot-prop": "^9.0.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.3", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^28.5.0", + "eslint-plugin-jest-dom": "^5.4.0", + "eslint-plugin-react": "^7.34.2", + "eslint-plugin-unicorn": "^53.0.0", + "glob": "^10.4.1", + "glob-gitignore": "^1.0.14", + "html-entities": "^2.5.2", + "http-terminator": "^3.2.0", + "husky": "^9.0.11", + "inquirer": "^8.2.5", + "jest": "^29.7.0", + "jest-circus": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-extended": "^4.0.2", + "jest-silent-reporter": "^0.6.0", + "jsonfile": "^6.1.0", + "lint-staged": "^15.2.5", + "mongodb-memory-server": "^9.2.0", + "msw": "^2.3.0", + "next-test-api-route-handler": "^4.0.7", + "prettier": "^3.2.5", + "random-case": "^1.0.0", + "raw-body": "^2.5.2", + "remark-capitalize-headings": "^2.0.1", + "remark-cli": "^12.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-ignore": "^2.0.0", + "remark-lint": "^10.0.0", + "remark-lint-definition-case": "^4.0.0", + "remark-lint-fenced-code-flag": "^4.0.0", + "remark-lint-fenced-code-flag-case": "^2.0.0", + "remark-lint-file-extension": "^3.0.0", + "remark-lint-final-newline": "^3.0.0", + "remark-lint-first-heading-level": "^4.0.0", + "remark-lint-hard-break-spaces": "^4.0.0", + "remark-lint-heading-increment": "^4.0.0", + "remark-lint-heading-whitespace": "^1.0.0", + "remark-lint-heading-word-length": "^2.0.0", + "remark-lint-list-item-style": "^2.0.0", + "remark-lint-no-auto-link-without-protocol": "^3.1.2", + "remark-lint-no-blockquote-without-marker": "^6.0.0", + "remark-lint-no-duplicate-defined-urls": "^3.0.0", + "remark-lint-no-duplicate-definitions": "^4.0.0", + "remark-lint-no-duplicate-headings-in-section": "^4.0.0", + "remark-lint-no-empty-sections": "^4.0.0", + "remark-lint-no-empty-url": "https://xunn.at/remark-lint-no-empty-url", + "remark-lint-no-heading-content-indent": "^5.0.0", + "remark-lint-no-heading-like-paragraph": "^4.0.0", + "remark-lint-no-heading-punctuation": "^4.0.0", + "remark-lint-no-inline-padding": "^4.1.2", + "remark-lint-no-literal-urls": "^4.0.0", + "remark-lint-no-multiple-toplevel-headings": "^4.0.0", + "remark-lint-no-reference-like-url": "^4.0.0", + "remark-lint-no-shell-dollars": "^4.0.0", + "remark-lint-no-shortcut-reference-image": "^4.0.0", + "remark-lint-no-shortcut-reference-link": "^4.0.0", + "remark-lint-no-tabs": "^4.0.0", + "remark-lint-no-undefined-references": "^5.0.0", + "remark-lint-no-unused-definitions": "^4.0.0", + "remark-lint-ordered-list-marker-style": "^4.0.0", + "remark-lint-ordered-list-marker-value": "^4.0.0", + "remark-lint-strikethrough-marker": "^3.0.0", + "remark-lint-unordered-list-marker-style": "^4.0.0", + "remark-reference-links": "^7.0.0", + "remark-remove-unused-definitions": "^2.0.0", + "remark-remove-url-trailing-slash": "^2.0.0", + "remark-renumber-references": "^2.0.0", + "remark-sort-definitions": "^2.0.0", + "remark-tight-comments": "^2.0.0", + "remark-validate-links": "^13.0.1", + "semantic-release": "https://xunn.at/semantic-release-atam@22.0.7", + "simple-git": "^3.24.0", + "sort-package-json": "https://xunn.at/sort-package-json", + "spellchecker": "^3.7.1", + "text-extensions": "^2.4.0", + "tsconfig-replace-paths": "^0.0.14", + "type-fest": "^4.18.3", + "typedoc": "^0.25.13", + "typedoc-plugin-markdown": "^4.0.3", + "unfetch": "^4.2.0", + "unique-filename": "^3.0.0", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/cli": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.6.tgz", + "integrity": "sha512-Sm/YhG/0REw9SKByFHDf4hkk7PYsjcsOyZgHGz1nvab4tUTQ9N4XVv+ykK0Y+VCJ3OshA/7EDyxnwCd8NEP/mQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "commander": "^6.2.0", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/cli/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@babel/cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@babel/cli/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "dependencies": { + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", + "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", + "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helpers": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/traverse": "^7.24.6", + "@babel/types": "^7.24.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.6.tgz", + "integrity": "sha512-Q1BfQX42zXHx732PLW0w4+Y3wJjoZKEMaatFUEAmQ7Z+jCXxinzeqX9bvv2Q8xNPes/H6F0I23oGkcgjaItmLw==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", + "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "dependencies": { + "@babel/types": "^7.24.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.6.tgz", + "integrity": "sha512-+wnfqc5uHiMYtvRX7qu80Toef8BXeh4HHR1SPeonGb1SKPniNEd4a/nlaJJMv/OIEYvIVavvo0yR7u10Gqz0Iw==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", + "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", + "dependencies": { + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz", + "integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.6.tgz", + "integrity": "sha512-C875lFBIWWwyv6MHZUG9HmRrlTDgOsLWZfYR0nW69gaKJNe0/Mpxx5r0EID2ZdHQkdUmQo2t0uNckTL08/1BgA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", + "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz", + "integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", + "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", + "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz", + "integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.6.tgz", + "integrity": "sha512-1Qursq9ArRZPAMOZf/nuzVW8HgJLkTB9y9LfP4lW2MVp4e9WkLJDovfKBxoDcCk6VuzIxyqWHyBoaCtSRP10yg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-wrap-function": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz", + "integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", + "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz", + "integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", + "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.6.tgz", + "integrity": "sha512-f1JLrlw/jbiNfxvdrfBgio/gRBk3yTAEJWirpAkiJG2Hb22E7cEYKHWo0dFPTv/niPovzIdPdEDetrv6tC6gPQ==", + "dependencies": { + "@babel/helper-function-name": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", + "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.6.tgz", + "integrity": "sha512-bYndrJ6Ph6Ar+GaB5VAc0JPoP80bQCm4qon6JEzXfRl5QZyQ8Ur1K6k7htxWmPA5z+k7JQvaMUrtXlqclWYzKw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.6.tgz", + "integrity": "sha512-iVuhb6poq5ikqRq2XWU6OQ+R5o9wF+r/or9CeUyovgptz0UlnK4/seOQ1Istu/XybYjAhQv1FRSSfHHufIku5Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.6.tgz", + "integrity": "sha512-c8TER5xMDYzzFcGqOEp9l4hvB7dcbhcGjcLVwxWfe4P5DOafdwjsBJZKsmv+o3aXh7NhopvayQIovHrh2zSRUQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/plugin-transform-optional-chaining": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.6.tgz", + "integrity": "sha512-z8zEjYmwBUHN/pCF3NuWBhHQjJCrd33qAi8MgANfMrAvn72k2cImT8VjK9LJFu4ysOLJqhfkYYb3MvwANRUNZQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.6.tgz", + "integrity": "sha512-qPPDbYs9j5IArMFqYi85QxatHURSzRyskKpIbjrVoVglDuGdhu1s7UTCmXvP/qR2aHa3EdJ8X3iZvQAHjmdHUw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-export-default-from": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-function-bind": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.24.6.tgz", + "integrity": "sha512-v/VFLHAGLQPgBz11zjuHDSZRfvXluqWdW3veRqG3Qo+PVxmScB/Bf/iQysOC3A/UBvoJvmmq0bNLmoImADa7Ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-function-bind": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.6.tgz", + "integrity": "sha512-Nzl7kZ4tjOM2LJpejBMPwZs7OJfc26++2HsMQuSrw6gxpqXGtZZ3Rj4Zt4Qm7vulMZL2gHIGGc2stnlQnHQCqA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-function-bind": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.24.6.tgz", + "integrity": "sha512-DfewWNErk5qyYTCXCYTE6R7VNnDB6PkZq4q9LDoefp/S2qEggfvO09dRZP1CinpRgwFO17xvk/8CXd1kC/i87w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.6.tgz", + "integrity": "sha512-BE6o2BogJKJImTmGpkmOic4V0hlRRxVtzqxiSPa8TIFxyhi4EFjHm08nq1M4STK4RytuLMgnSz0/wfflvGFNOg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.6.tgz", + "integrity": "sha512-D+CfsVZousPXIdudSII7RGy52+dYRtbyKAZcvtQKq/NpsivyMVduepzcLqG5pMBugtMdedxdC8Ramdpcne9ZWQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.6.tgz", + "integrity": "sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.6.tgz", + "integrity": "sha512-TzCtxGgVTEJWWwcYwQhCIQ6WaKlo80/B+Onsk4RRCcYqpYGFcG9etPW94VToGte5AAcxRrhjPUFvUS3Y2qKi4A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.6.tgz", + "integrity": "sha512-jSSSDt4ZidNMggcLx8SaKsbGNEfIl0PHx/4mFEulorE7bpYLbN0d3pDW3eJ7Y5Z3yPhy3L3NaPCYyTUY7TuugQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.6.tgz", + "integrity": "sha512-VEP2o4iR2DqQU6KPgizTW2mnMx6BG5b5O9iQdrW9HesLkv8GIA8x2daXBQxw1MrsIkFQGA/iJ204CKoQ8UcnAA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-remap-async-to-generator": "^7.24.6", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.6.tgz", + "integrity": "sha512-NTBA2SioI3OsHeIn6sQmhvXleSl9T70YY/hostQLveWs0ic+qvbA3fa0kwAwQ0OA/XGaAerNZRQGJyRfhbJK4g==", + "dependencies": { + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-remap-async-to-generator": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.6.tgz", + "integrity": "sha512-XNW7jolYHW9CwORrZgA/97tL/k05qe/HL0z/qqJq1mdWhwwCM6D4BJBV7wAz9HgFziN5dTOG31znkVIzwxv+vw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.6.tgz", + "integrity": "sha512-S/t1Xh4ehW7sGA7c1j/hiOBLnEYCp/c2sEG4ZkL8kI1xX9tW2pqJTCHKtdhe/jHKt8nG0pFCrDHUXd4DvjHS9w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", + "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.6.tgz", + "integrity": "sha512-1QSRfoPI9RoLRa8Mnakc6v3e0gJxiZQTYrMfLn+mD0sz5+ndSzwymp2hDcYJTyT0MOn0yuWzj8phlIvO72gTHA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.6.tgz", + "integrity": "sha512-+fN+NO2gh8JtRmDSOB6gaCVo36ha8kfCW1nMq2Gc0DABln0VcHN4PrALDvF5/diLzIRKptC7z/d7Lp64zk92Fg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.6.tgz", + "integrity": "sha512-cRzPobcfRP0ZtuIEkA8QzghoUpSB3X3qSH5W2+FzG+VjWbJXExtx0nbRqwumdBN1x/ot2SlTNQLfBCnPdzp6kg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/template": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.6.tgz", + "integrity": "sha512-YLW6AE5LQpk5npNXL7i/O+U9CE4XsBCuRPgyjl1EICZYKmcitV+ayuuUGMJm2lC1WWjXYszeTnIxF/dq/GhIZQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.6.tgz", + "integrity": "sha512-rCXPnSEKvkm/EjzOtLoGvKseK+dS4kZwx1HexO3BtRtgL0fQ34awHn34aeSHuXtZY2F8a1X8xqBBPRtOxDVmcA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.6.tgz", + "integrity": "sha512-/8Odwp/aVkZwPFJMllSbawhDAO3UJi65foB00HYnK/uXvvCPm0TAXSByjz1mpRmp0q6oX2SIxpkUOpPFHk7FLA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.6.tgz", + "integrity": "sha512-vpq8SSLRTBLOHUZHSnBqVo0AKX3PBaoPs2vVzYVWslXDTDIpwAcCDtfhUcHSQQoYoUvcFPTdC8TZYXu9ZnLT/w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.6.tgz", + "integrity": "sha512-EemYpHtmz0lHE7hxxxYEuTYOOBZ43WkDgZ4arQ4r+VX9QHuNZC+WH3wUWmRNvR8ECpTRne29aZV6XO22qpOtdA==", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.6.tgz", + "integrity": "sha512-inXaTM1SVrIxCkIJ5gqWiozHfFMStuGbGJAxZFBoHcRRdDP0ySLb3jH6JOwmfiinPwyMZqMBX+7NBDCO4z0NSA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.6.tgz", + "integrity": "sha512-n3Sf72TnqK4nw/jziSqEl1qaWPbCRw2CziHH+jdRYvw4J6yeCzsj4jdw8hIntOEeDGTmHVe2w4MVL44PN0GMzg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.6.tgz", + "integrity": "sha512-sOajCu6V0P1KPljWHKiDq6ymgqB+vfo3isUS4McqW1DZtvSVU2v/wuMhmRmkg3sFoq6GMaUUf8W4WtoSLkOV/Q==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.6.tgz", + "integrity": "sha512-Uvgd9p2gUnzYJxVdBLcU0KurF8aVhkmVyMKW4MIY1/BByvs3EBpv45q01o7pRTVmTvtQq5zDlytP3dcUgm7v9w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.6.tgz", + "integrity": "sha512-f2wHfR2HF6yMj+y+/y07+SLqnOSwRp8KYLpQKOzS58XLVlULhXbiYcygfXQxJlMbhII9+yXDwOUFLf60/TL5tw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.6.tgz", + "integrity": "sha512-EKaWvnezBCMkRIHxMJSIIylzhqK09YpiJtDbr2wsXTwnO0TxyjMUkaw4RlFIZMIS0iDj0KyIg7H7XCguHu/YDA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.6.tgz", + "integrity": "sha512-9g8iV146szUo5GWgXpRbq/GALTnY+WnNuRTuRHWWFfWGbP9ukRL0aO/jpu9dmOPikclkxnNsjY8/gsWl6bmZJQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.6.tgz", + "integrity": "sha512-eAGogjZgcwqAxhyFgqghvoHRr+EYRQPFjUXrTYKBRb5qPnAVxOOglaxc4/byHqjvq/bqO2F3/CGwTHsgKJYHhQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.6.tgz", + "integrity": "sha512-JEV8l3MHdmmdb7S7Cmx6rbNEjRCgTQMZxllveHO0mx6uiclB0NflCawlQQ6+o5ZrwjUBYPzHm2XoK4wqGVUFuw==", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.6.tgz", + "integrity": "sha512-xg1Z0J5JVYxtpX954XqaaAT6NpAY6LtZXvYFCJmGFJWwtlz2EmJoR8LycFRGNE8dBKizGWkGQZGegtkV8y8s+w==", + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.6.tgz", + "integrity": "sha512-esRCC/KsSEUvrSjv5rFYnjZI6qv4R1e/iHQrqwbZIoRJqk7xCvEUiN7L1XrmW5QSmQe3n1XD88wbgDTWLbVSyg==", + "dependencies": { + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.6.tgz", + "integrity": "sha512-6DneiCiu91wm3YiNIGDWZsl6GfTTbspuj/toTEqLh9d4cx50UIzSdg+T96p8DuT7aJOBRhFyaE9ZvTHkXrXr6Q==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.6.tgz", + "integrity": "sha512-f8liz9JG2Va8A4J5ZBuaSdwfPqN6axfWRK+y66fjKYbwf9VBLuq4WxtinhJhvp1w6lamKUwLG0slK2RxqFgvHA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.6.tgz", + "integrity": "sha512-+QlAiZBMsBK5NqrBWFXCYeXyiU1y7BQ/OYaiPAcQJMomn5Tyg+r5WuVtyEuvTbpV7L25ZSLfE+2E9ywj4FD48A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.6.tgz", + "integrity": "sha512-6voawq8T25Jvvnc4/rXcWZQKKxUNZcKMS8ZNrjxQqoRFernJJKjE3s18Qo6VFaatG5aiX5JV1oPD7DbJhn0a4Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.6.tgz", + "integrity": "sha512-OKmi5wiMoRW5Smttne7BwHM8s/fb5JFs+bVGNSeHWzwZkWXWValR1M30jyXo1s/RaqgwwhEC62u4rFH/FBcBPg==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.6.tgz", + "integrity": "sha512-N/C76ihFKlZgKfdkEYKtaRUtXZAgK7sOY4h2qrbVbVTXPrKGIi8aww5WGe/+Wmg8onn8sr2ut6FXlsbu/j6JHg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.6.tgz", + "integrity": "sha512-L5pZ+b3O1mSzJ71HmxSCmTVd03VOT2GXOigug6vDYJzE5awLI7P1g0wFcdmGuwSDSrQ0L2rDOe/hHws8J1rv3w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.6.tgz", + "integrity": "sha512-cHbqF6l1QP11OkYTYQ+hhVx1E017O5ZcSPXk9oODpqhcAD1htsWG2NpHrrhthEO2qZomLK0FXS+u7NfrkF5aOQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.6.tgz", + "integrity": "sha512-ST7guE8vLV+vI70wmAxuZpIKzVjvFX9Qs8bl5w6tN/6gOypPWUmMQL2p7LJz5E63vEGrDhAiYetniJFyBH1RkA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.6.tgz", + "integrity": "sha512-T9LtDI0BgwXOzyXrvgLTT8DFjCC/XgWLjflczTLXyvxbnSR/gpv0hbmzlHE/kmh9nOvlygbamLKRo6Op4yB6aw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.6.tgz", + "integrity": "sha512-Qu/ypFxCY5NkAnEhCF86Mvg3NSabKsh/TPpBVswEdkGl7+FbsYHy1ziRqJpwGH4thBdQHh8zx+z7vMYmcJ7iaQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.6.tgz", + "integrity": "sha512-oARaglxhRsN18OYsnPTpb8TcKQWDYNsPNmTnx5++WOAsUJ0cSC/FZVlIJCKvPbU4yn/UXsS0551CFKJhN0CaMw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.6.tgz", + "integrity": "sha512-/3iiEEHDsJuj9QU09gbyWGSUxDboFcD7Nj6dnHIlboWSodxXAoaY/zlNMHeYAC0WsERMqgO9a7UaM77CsYgWcg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.6.tgz", + "integrity": "sha512-pCtPHhpRZHfwdA5G1Gpk5mIzMA99hv0R8S/Ket50Rw+S+8hkt3wBWqdqHaPw0CuUYxdshUgsPiLQ5fAs4ASMhw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-jsx": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.6.tgz", + "integrity": "sha512-F7EsNp5StNDouSSdYyDSxh4J+xvj/JqG+Cb6s2fA+jCyHOzigG5vTwgH8tU2U8Voyiu5zCG9bAK49wTr/wPH0w==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.6.tgz", + "integrity": "sha512-BQTBCXmFRreU3oTUXcGKuPOfXAGb1liNY4AvvFKsOBAJ89RKcTsIrSsnMYkj59fNa66OFKnSa4AJZfy5Y4B9WA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.6.tgz", + "integrity": "sha512-0HoDQlFJJkXRyV2N+xOpUETbKHcouSwijRQbKWVtxsPoq5bbB30qZag9/pSc5xcWVYjTHlLsBsY+hZDnzQTPNw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.6.tgz", + "integrity": "sha512-SMDxO95I8WXRtXhTAc8t/NFQUT7VYbIWwJCJgEli9ml4MhqUMh4S6hxgH6SmAC3eAQNWCDJFxcFeEt9w2sDdXg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.6.tgz", + "integrity": "sha512-DcrgFXRRlK64dGE0ZFBPD5egM2uM8mgfrvTMOSB2yKzOtjpGegVYkzh3s1zZg1bBck3nkXiaOamJUqK3Syk+4A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.6.tgz", + "integrity": "sha512-W3gQydMb0SY99y/2lV0Okx2xg/8KzmZLQsLaiCmwNRl1kKomz14VurEm+2TossUb+sRvBCnGe+wx8KtIgDtBbQ==", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.6.tgz", + "integrity": "sha512-xnEUvHSMr9eOWS5Al2YPfc32ten7CXdH7Zwyyk7IqITg4nX61oHj+GxpNvl+y5JHjfN3KXE2IV55wAWowBYMVw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.6.tgz", + "integrity": "sha512-h/2j7oIUDjS+ULsIrNZ6/TKG97FgmEk1PXryk/HQq6op4XUUUwif2f69fJrzK0wza2zjCS1xhXmouACaWV5uPA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.6.tgz", + "integrity": "sha512-fN8OcTLfGmYv7FnDrsjodYBo1DhPL3Pze/9mIIE2MGCT1KgADYIOD7rEglpLHZj8PZlC/JFX5WcD+85FLAQusw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.6.tgz", + "integrity": "sha512-BJbEqJIcKwrqUP+KfUIkxz3q8VzXe2R8Wv8TaNgO1cx+nNavxn/2+H8kp9tgFSOL6wYPPEgFvU6IKS4qoGqhmg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.6.tgz", + "integrity": "sha512-IshCXQ+G9JIFJI7bUpxTE/oA2lgVLAIK8q1KdJNoPXOpvRaNjMySGuvLfBw/Xi2/1lLo953uE8hyYSDW3TSYig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.6.tgz", + "integrity": "sha512-H0i+hDLmaYYSt6KU9cZE0gb3Cbssa/oxWis7PX4ofQzbvsfix9Lbh8SRk7LCPDlLWJHUiFeHU0qRRpF/4Zv7mQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/plugin-syntax-typescript": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.6.tgz", + "integrity": "sha512-bKl3xxcPbkQQo5eX9LjjDpU2xYHeEeNQbOhj0iPvetSzA+Tu9q/o5lujF4Sek60CM6MgYvOS/DJuwGbiEYAnLw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.6.tgz", + "integrity": "sha512-8EIgImzVUxy15cZiPii9GvLZwsy7Vxc+8meSlR3cXFmBIl5W5Tn9LGBf7CDKkHj4uVfNXCJB8RsVfnmY61iedA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.6.tgz", + "integrity": "sha512-pssN6ExsvxaKU638qcWb81RrvvgZom3jDgU/r5xFZ7TONkZGFf4MhI2ltMb8OcQWhHyxgIavEU+hgqtbKOmsPA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.6.tgz", + "integrity": "sha512-quiMsb28oXWIDK0gXLALOJRXLgICLiulqdZGOaPPd0vRT7fQp74NtdADAVu+D8s00C+0Xs0MxVP0VKF/sZEUgw==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.6.tgz", + "integrity": "sha512-CrxEAvN7VxfjOG8JNF2Y/eMqMJbZPZ185amwGUBp8D9USK90xQmv7dLdFSa+VbD7fdIqcy/Mfv7WtzG8+/qxKg==", + "dependencies": { + "@babel/compat-data": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.6", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.6", + "@babel/plugin-syntax-import-attributes": "^7.24.6", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.6", + "@babel/plugin-transform-async-generator-functions": "^7.24.6", + "@babel/plugin-transform-async-to-generator": "^7.24.6", + "@babel/plugin-transform-block-scoped-functions": "^7.24.6", + "@babel/plugin-transform-block-scoping": "^7.24.6", + "@babel/plugin-transform-class-properties": "^7.24.6", + "@babel/plugin-transform-class-static-block": "^7.24.6", + "@babel/plugin-transform-classes": "^7.24.6", + "@babel/plugin-transform-computed-properties": "^7.24.6", + "@babel/plugin-transform-destructuring": "^7.24.6", + "@babel/plugin-transform-dotall-regex": "^7.24.6", + "@babel/plugin-transform-duplicate-keys": "^7.24.6", + "@babel/plugin-transform-dynamic-import": "^7.24.6", + "@babel/plugin-transform-exponentiation-operator": "^7.24.6", + "@babel/plugin-transform-export-namespace-from": "^7.24.6", + "@babel/plugin-transform-for-of": "^7.24.6", + "@babel/plugin-transform-function-name": "^7.24.6", + "@babel/plugin-transform-json-strings": "^7.24.6", + "@babel/plugin-transform-literals": "^7.24.6", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.6", + "@babel/plugin-transform-member-expression-literals": "^7.24.6", + "@babel/plugin-transform-modules-amd": "^7.24.6", + "@babel/plugin-transform-modules-commonjs": "^7.24.6", + "@babel/plugin-transform-modules-systemjs": "^7.24.6", + "@babel/plugin-transform-modules-umd": "^7.24.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.6", + "@babel/plugin-transform-new-target": "^7.24.6", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.6", + "@babel/plugin-transform-numeric-separator": "^7.24.6", + "@babel/plugin-transform-object-rest-spread": "^7.24.6", + "@babel/plugin-transform-object-super": "^7.24.6", + "@babel/plugin-transform-optional-catch-binding": "^7.24.6", + "@babel/plugin-transform-optional-chaining": "^7.24.6", + "@babel/plugin-transform-parameters": "^7.24.6", + "@babel/plugin-transform-private-methods": "^7.24.6", + "@babel/plugin-transform-private-property-in-object": "^7.24.6", + "@babel/plugin-transform-property-literals": "^7.24.6", + "@babel/plugin-transform-regenerator": "^7.24.6", + "@babel/plugin-transform-reserved-words": "^7.24.6", + "@babel/plugin-transform-shorthand-properties": "^7.24.6", + "@babel/plugin-transform-spread": "^7.24.6", + "@babel/plugin-transform-sticky-regex": "^7.24.6", + "@babel/plugin-transform-template-literals": "^7.24.6", + "@babel/plugin-transform-typeof-symbol": "^7.24.6", + "@babel/plugin-transform-unicode-escapes": "^7.24.6", + "@babel/plugin-transform-unicode-property-regex": "^7.24.6", + "@babel/plugin-transform-unicode-regex": "^7.24.6", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.6.tgz", + "integrity": "sha512-8mpzh1bWvmINmwM3xpz6ahu57mNaWavMm+wBNjQ4AFu1nghKBiIRET7l/Wmj4drXany/BBGjJZngICcD98F1iw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "@babel/plugin-transform-react-display-name": "^7.24.6", + "@babel/plugin-transform-react-jsx": "^7.24.6", + "@babel/plugin-transform-react-jsx-development": "^7.24.6", + "@babel/plugin-transform-react-pure-annotations": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.6.tgz", + "integrity": "sha512-U10aHPDnokCFRXgyT/MaIRTivUu2K/mu0vJlwRS9LxJmJet+PFQNKpggPyFCUtC6zWSBPjvxjnpNkAn3Uw2m5w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "@babel/plugin-syntax-jsx": "^7.24.6", + "@babel/plugin-transform-modules-commonjs": "^7.24.6", + "@babel/plugin-transform-typescript": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, + "node_modules/@babel/runtime": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", + "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", + "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.3.0.tgz", + "integrity": "sha512-LgYWOwuDR7BSTQ9OLZ12m7F/qhNY+NpAyPBgo4YNMkACE7lGuUnuQq1yi9hz1KA4+3VqpOYl8H1rY/LYK43v7g==", + "dev": true, + "dependencies": { + "@commitlint/format": "^19.3.0", + "@commitlint/lint": "^19.2.2", + "@commitlint/load": "^19.2.0", + "@commitlint/read": "^19.2.1", + "@commitlint/types": "^19.0.3", + "execa": "^8.0.1", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cli/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@commitlint/cli/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@commitlint/cli/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@commitlint/cli/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.2.2.tgz", + "integrity": "sha512-mLXjsxUVLYEGgzbxbxicGPggDuyWNkf25Ht23owXIH+zV2pv1eJuzLK3t1gDY5Gp6pxdE60jZnWUY5cvgL3ufw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.0.3.tgz", + "integrity": "sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.0.3.tgz", + "integrity": "sha512-SZEpa/VvBLoT+EFZVb91YWbmaZ/9rPH3ESrINOl0HD2kMYsjvl0tF7nMHh0EpTcv4+gTtZBAe1y/SS6/OhfZzQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.0.0.tgz", + "integrity": "sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.3.0.tgz", + "integrity": "sha512-luguk5/aF68HiF4H23ACAfk8qS8AHxl4LLN5oxPc24H+2+JRPsNr1OS3Gaea0CrH7PKhArBMKBz5RX9sA5NtTg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.2.2.tgz", + "integrity": "sha512-eNX54oXMVxncORywF4ZPFtJoBm3Tvp111tg1xf4zWXGfhBPKpfKG6R+G3G4v5CPlRROXpAOpQ3HMhA9n1Tck1g==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.2.2.tgz", + "integrity": "sha512-xrzMmz4JqwGyKQKTpFzlN0dx0TAiT7Ran1fqEBgEmEj+PU98crOFtysJgY+QdeSagx6EDRigQIXJVnfrI0ratA==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^19.2.2", + "@commitlint/parse": "^19.0.3", + "@commitlint/rules": "^19.0.3", + "@commitlint/types": "^19.0.3" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.2.0.tgz", + "integrity": "sha512-XvxxLJTKqZojCxaBQ7u92qQLFMMZc4+p9qrIq/9kJDy8DOrEa7P1yx7Tjdc2u2JxIalqT4KOGraVgCE7eCYJyQ==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^19.0.3", + "@commitlint/execute-rule": "^19.0.0", + "@commitlint/resolve-extends": "^19.1.0", + "@commitlint/types": "^19.0.3", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@commitlint/message": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.0.0.tgz", + "integrity": "sha512-c9czf6lU+9oF9gVVa2lmKaOARJvt4soRsVmbR7Njwp9FpbBgste5i7l/2l5o8MmbwGh4yE1snfnsy2qyA2r/Fw==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.0.3.tgz", + "integrity": "sha512-Il+tNyOb8VDxN3P6XoBBwWJtKKGzHlitEuXA5BP6ir/3loWlsSqDr5aecl6hZcC/spjq4pHqNh0qPlfeWu38QA==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.0.3", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.2.1.tgz", + "integrity": "sha512-qETc4+PL0EUv7Q36lJbPG+NJiBOGg7SSC7B5BsPWOmei+Dyif80ErfWQ0qXoW9oCh7GTpTNRoaVhiI8RbhuaNw==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^19.0.0", + "@commitlint/types": "^19.0.3", + "execa": "^8.0.1", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@commitlint/read/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@commitlint/read/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/read/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@commitlint/read/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.1.0.tgz", + "integrity": "sha512-z2riI+8G3CET5CPgXJPlzftH+RiWYLMYv4C9tSLdLXdr6pBNimSKukYP9MS27ejmscqCTVA4almdLh0ODD2KYg==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^19.0.3", + "@commitlint/types": "^19.0.3", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.0.3.tgz", + "integrity": "sha512-TspKb9VB6svklxNCKKwxhELn7qhtY1rFF8ls58DcFd0F97XoG07xugPjjbVnLqmMkRjZDbDIwBKt9bddOfLaPw==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^19.0.3", + "@commitlint/message": "^19.0.0", + "@commitlint/to-lines": "^19.0.0", + "@commitlint/types": "^19.0.3", + "execa": "^8.0.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@commitlint/rules/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@commitlint/rules/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@commitlint/rules/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.0.0.tgz", + "integrity": "sha512-vkxWo+VQU5wFhiP9Ub9Sre0FYe019JxFikrALVoD5UGa8/t3yOJEpEhxC5xKiENKKhUkTpEItMTRAjHw2SCpZw==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.0.0.tgz", + "integrity": "sha512-KKjShd6u1aMGNkCkaX4aG1jOGdn7f8ZI8TR1VEuNqUOjWTOdcDSsmglinglJ18JTjuBX5I1PtjrhQCRcixRVFQ==", + "dev": true, + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level/node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@commitlint/top-level/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/types": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.0.3.tgz", + "integrity": "sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA==", + "dev": true, + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@hutson/parse-repository-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-5.0.0.tgz", + "integrity": "sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.8.tgz", + "integrity": "sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^8.2.1", + "@inquirer/type": "^1.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.1.tgz", + "integrity": "sha512-TIcuQMn2qrtyYe0j136UpHeYpk7AcR/trKeT/7YY0vRgcS9YSfJuQ2+PudPhSofLLsHNnRYAHScQCcVZrJkMqA==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.2", + "@inquirer/type": "^1.3.2", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.12", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/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, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@inquirer/core/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.2.tgz", + "integrity": "sha512-4F1MBwVr3c/m4bAUef6LgkvBfSjzwH+OfldgHqcuacWwSUetFebM2wi58WfG9uk1rR98U6GwLed4asLJbwdV5w==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.2.tgz", + "integrity": "sha512-5Frickan9c89QbPkSu6I6y8p+9eR6hZkdPahGmNDsTFX8FHLPAozyzCZMKUeW8FyYwnlCKUjqIEqxY+UctARiw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "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, + "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.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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 + }, + "node_modules/@isaacs/cliui/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, + "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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "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, + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kamilkisiela/fast-url-parser": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", + "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==", + "dev": true + }, + "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, + "dependencies": { + "debug": "^4.1.1" + } + }, + "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 + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", + "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@mswjs/cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@next/bundle-analyzer": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-14.2.3.tgz", + "integrity": "sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA==", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, + "node_modules/@next/env": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.3.tgz", + "integrity": "sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==", + "dev": true, + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/config": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.3.2.tgz", + "integrity": "sha512-IMzf+fhRXibqh9mBwXK/QFIr97SAlZjfwsWPEz/2pST1cE9k9LcwznO7aDNXJoMrDjxPHZmb2bAAKASsa6EedA==", + "dev": true, + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.2", + "nopt": "^7.2.1", + "proc-log": "^4.2.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/config/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/config/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", + "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", + "dev": true, + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dev": true, + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry/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 + }, + "node_modules/@octokit/plugin-retry/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, + "dependencies": { + "@octokit/openapi-types": "^20.0.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, + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/plugin-throttling/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 + }, + "node_modules/@octokit/plugin-throttling/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, + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.1.tgz", + "integrity": "sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.3.1", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.2.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "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, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", + "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@semantic-release/changelog": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", + "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "fs-extra": "^11.0.0", + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/commit-analyzer": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-10.0.4.tgz", + "integrity": "sha512-pFGn99fn8w4/MHE0otb2A/l5kxgOuxaaauIh4u30ncoTJuqWj4hXTgEJ03REqjS+w1R2vPftSsO26WC61yOcpw==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^6.0.0", + "conventional-commits-filter": "^3.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "import-from": "^4.0.0", + "lodash-es": "^4.17.21", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/commit-analyzer/node_modules/conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@semantic-release/commit-analyzer/node_modules/conventional-commits-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz", + "integrity": "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==", + "dev": true, + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/exec": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-6.0.3.tgz", + "integrity": "sha512-bxAq8vLOw76aV89vxxICecEa8jfaWwYITw6X74zzlO0mc/Bgieqx9kBRz9z96pHectiTAtsCwsQcUyLYWnp3VQ==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "parse-json": "^5.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/git": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", + "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "dir-glob": "^3.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "micromatch": "^4.0.0", + "p-reduce": "^2.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/github": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.2.6.tgz", + "integrity": "sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==", + "dev": true, + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "globby": "^14.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^6.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.0", + "p-filter": "^4.0.0", + "url-join": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/github/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 + }, + "node_modules/@semantic-release/github/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@semantic-release/github/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, + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/github/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@semantic-release/github/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/globby": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@semantic-release/github/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@semantic-release/github/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-10.0.6.tgz", + "integrity": "sha512-DyqHrGE8aUyapA277BB+4kV0C4iMHh3sHzUWdf0jTgp5NNJxVUz76W1f57FB64Ue03him3CBXxFqQD2xGabxow==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "execa": "^8.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^9.5.0", + "rc": "^1.2.8", + "read-pkg": "^8.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/npm/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/npm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/npm/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@semantic-release/npm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-11.0.7.tgz", + "integrity": "sha512-T09QB9ImmNx7Q6hY6YnnEbw/rEJ6a+22LBxfZq+pSAXg/OL/k0siwEm5cK4k1f9dE2Z2mPIjJKKohzUm0jbxcQ==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^6.0.0", + "conventional-changelog-writer": "^6.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "get-stream": "^7.0.0", + "import-from": "^4.0.0", + "into-stream": "^7.0.0", + "lodash-es": "^4.17.21", + "read-pkg-up": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-changelog-writer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-6.0.1.tgz", + "integrity": "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ==", + "dev": true, + "dependencies": { + "conventional-commits-filter": "^3.0.0", + "dateformat": "^3.0.3", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "meow": "^8.1.2", + "semver": "^7.0.0", + "split": "^1.0.1" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-changelog-writer/node_modules/conventional-commits-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz", + "integrity": "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==", + "dev": true, + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-commits-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-4.0.0.tgz", + "integrity": "sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", + "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg-up": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", + "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", + "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^10.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-12.6.1.tgz", + "integrity": "sha512-uzlJ+ZsCAyJm+lBi7j0UeBbj+Oy6w/VWoGJ3iHRHE5eZ8Z4iK66mq+PG/spupmbllLtz77OJbY89BYqgFyjXmA==", + "dev": true + }, + "node_modules/@textlint/markdown-to-ast": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-12.6.1.tgz", + "integrity": "sha512-T0HO+VrU9VbLRiEx/kH4+gwGMHNMIGkp0Pok+p0I33saOOLyhfGvwOKQgvt2qkxzQEV2L5MtGB8EnW4r5d3CqQ==", + "dev": true, + "dependencies": { + "@textlint/ast-node-types": "^12.6.1", + "debug": "^4.3.4", + "mdast-util-gfm-autolink-literal": "^0.1.3", + "remark-footnotes": "^3.0.0", + "remark-frontmatter": "^3.0.0", + "remark-gfm": "^1.0.0", + "remark-parse": "^9.0.0", + "traverse": "^0.6.7", + "unified": "^9.2.2" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/mdast-util-frontmatter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-0.2.0.tgz", + "integrity": "sha512-FHKL4w4S5fdt1KjJCwB0178WJ0evnyyQr5kXTM3wrOVpytD0hrkvd+AOOjU9Td8onOejCkmZ+HQRT3CZ3coHHQ==", + "dev": true, + "dependencies": { + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/mdast-util-gfm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz", + "integrity": "sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==", + "dev": true, + "dependencies": { + "mdast-util-gfm-autolink-literal": "^0.1.0", + "mdast-util-gfm-strikethrough": "^0.2.0", + "mdast-util-gfm-table": "^0.1.0", + "mdast-util-gfm-task-list-item": "^0.1.0", + "mdast-util-to-markdown": "^0.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/mdast-util-gfm-strikethrough": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz", + "integrity": "sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/mdast-util-gfm-table": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz", + "integrity": "sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==", + "dev": true, + "dependencies": { + "markdown-table": "^2.0.0", + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/mdast-util-gfm-task-list-item": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz", + "integrity": "sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-frontmatter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-0.2.2.tgz", + "integrity": "sha512-q6nPLFCMTLtfsctAuS0Xh4vaolxSFUWUWR6PZSrXXiRy+SANGllpcqdXFv2z07l0Xz/6Hl40hK0ffNCJPH2n1A==", + "dev": true, + "dependencies": { + "fault": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz", + "integrity": "sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==", + "dev": true, + "dependencies": { + "micromark": "~2.11.0", + "micromark-extension-gfm-autolink-literal": "~0.5.0", + "micromark-extension-gfm-strikethrough": "~0.6.5", + "micromark-extension-gfm-table": "~0.4.0", + "micromark-extension-gfm-tagfilter": "~0.3.0", + "micromark-extension-gfm-task-list-item": "~0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz", + "integrity": "sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==", + "dev": true, + "dependencies": { + "micromark": "~2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm-strikethrough": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz", + "integrity": "sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==", + "dev": true, + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm-table": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz", + "integrity": "sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==", + "dev": true, + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm-tagfilter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz", + "integrity": "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/micromark-extension-gfm-task-list-item": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz", + "integrity": "sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==", + "dev": true, + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/remark-frontmatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-3.0.0.tgz", + "integrity": "sha512-mSuDd3svCHs+2PyO29h7iijIZx4plX0fheacJcAoYAASfgzgVIcXGYSq9GFyYocFLftQs8IOmmkgtOovs6d4oA==", + "dev": true, + "dependencies": { + "mdast-util-frontmatter": "^0.2.0", + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@textlint/markdown-to-ast/node_modules/remark-gfm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", + "integrity": "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==", + "dev": true, + "dependencies": { + "mdast-util-gfm": "^0.1.0", + "micromark-extension-gfm": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__plugin-transform-runtime": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@types/babel__plugin-transform-runtime/-/babel__plugin-transform-runtime-7.9.5.tgz", + "integrity": "sha512-m/y2Sb3nld95N69piVZrgTn0hoBV9ng03Ch8/6duE5ss8A4OuPlbQ9CJPTRYQSerU3qhHnbUtOPRrOYcuPgljg==", + "peer": true + }, + "node_modules/@types/babel__preset-env": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@types/babel__preset-env/-/babel__preset-env-7.9.6.tgz", + "integrity": "sha512-PaOA2V4J3CZZopQaTGT1e8WEWCqHWc1k12zLlci4T9eR2lQIlA/GbnVbloFDqYVFr1BNiCXnotH32Up8WdgTxQ==", + "peer": true + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bytes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.4.tgz", + "integrity": "sha512-A0uYgOj3zNc4hNjHc5lYUfJQ/HVyBXiUMKdXd7ysclaE6k9oJdavQzODHuwjpUu2/boCP8afjQYi8z/GtvNCWA==" + }, + "node_modules/@types/clone-deep": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz", + "integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==", + "dev": true + }, + "node_modules/@types/concat-stream": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.3.tgz", + "integrity": "sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/confusing-browser-globals": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/confusing-browser-globals/-/confusing-browser-globals-1.0.3.tgz", + "integrity": "sha512-q+6axdE3RyjrSsy2ONE4UpF89rwOfpoMBP3lqJ+OzLuOeYHwP+o2GITzuleKb1UT3FSYybO8QmeACgyHleu2CA==", + "dev": true + }, + "node_modules/@types/content-type": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", + "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", + "dev": true + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/folder-hash": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/folder-hash/-/folder-hash-4.0.4.tgz", + "integrity": "sha512-c+PwHm51Dw3fXM8SDK+93PO3oXdk4XNouCCvV67lj4aijRkZz5g67myk+9wqWWnyv3go6q96hT6ywcd3XtoZiQ==" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/hosted-git-info": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==", + "dev": true + }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, + "node_modules/@types/is-empty": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/is-empty/-/is-empty-1.2.3.tgz", + "integrity": "sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/request-ip": { + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@types/request-ip/-/request-ip-0.0.41.tgz", + "integrity": "sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "node_modules/@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "dev": true + }, + "node_modules/@types/tar-stream": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.3.tgz", + "integrity": "sha512-Zbnx4wpkWBMBSu5CytMbrT5ZpMiF55qgM+EpHzR4yIDu7mv52cej8hTkOc6K+LzpkOAbxwn/m7j3iO+/l42YkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/test-listen": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/test-listen/-/test-listen-1.1.2.tgz", + "integrity": "sha512-1z4+FBjfjHOu9d3dXUWE5yD/iwVe3OrHALX6Y2Qsv3CAL1+U2lvsIgbrIyHifi8HH1CUkHTYNDUFuUNXk1gusA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/text-table": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/text-table/-/text-table-0.2.5.tgz", + "integrity": "sha512-hcZhlNvMkQG/k1vcZ6yHOl6WAYftQ2MLfTHcYRZ2xYZFD8tGVnE3qFV0lj1smQeDSR7/yY0PyuUalauf33bJeA==", + "dev": true + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "dev": true, + "dependencies": { + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.11.tgz", + "integrity": "sha512-LS8tSomZa3YHnntpWt3PP43iFEEl6YeIsvDakczHBKlay5LdkXFr8w7v8H6akpG5nRrzydyB0k1iE2eoL6aKIQ==", + "dev": true, + "dependencies": { + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/server": { + "version": "0.9.34", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.9.34.tgz", + "integrity": "sha512-1sHRjqUtZIyTR2m2dS/dJpzS5OcNDpPuUSVDa2PoEgzYVKr4GsqJaYtRaEXXFohvvyh6PkouYCc1rE7jMDWVCA==", + "dev": true, + "dependencies": { + "@whatwg-node/fetch": "^0.9.17", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@xunnamius/babel-preset-next-babel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@xunnamius/babel-preset-next-babel/-/babel-preset-next-babel-1.0.1.tgz", + "integrity": "sha512-peT2wEXhJSCvKLs2+WEuMFvQf6AhYyF5p6mMDt/ymlM4XV91CSxRSjJebfT1DZLxoGrIagxonx70n+CikTU3Dw==", + "deprecated": "the use case for this package was resolved; just use Next.js normally", + "engines": { + "node": "^16.20.0 || ^18.16.0 || >=20.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.22.5", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-transform-runtime": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-react": "^7.22.5", + "@babel/preset-typescript": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@types/babel__plugin-transform-runtime": "^7.9.2", + "@types/babel__preset-env": "^7.9.2", + "next": ">=13.4.6", + "styled-jsx": ">=5.1.1" + } + }, + "node_modules/@xunnamius/conventional-changelog-projector": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@xunnamius/conventional-changelog-projector/-/conventional-changelog-projector-1.2.1.tgz", + "integrity": "sha512-MMud6Ncz7ebiIMGWysLq7mjQ76vomDBmIEFCXNeisbtfZjfGmeKtPpb7ZBe9XR24yWQi0DfYMd+JFGQAuDvHIw==", + "dev": true, + "dependencies": { + "assign-deep": "^1.0.1", + "debug": "^4.3.4", + "is-plain-object": "^5.0.0", + "semver": "^7.3.8", + "toss-expression": "^0.1.1" + }, + "engines": { + "node": "^14.20.0 || ^16.16.0 || >=18.5.0" + } + }, + "node_modules/@xunnamius/conventional-changelog-projector/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@xunnamius/jest-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@xunnamius/jest-types/-/jest-types-1.1.3.tgz", + "integrity": "sha512-htMyGTIO5tWcJKCksqCsvMZDQShdyqJPLiIl/TdgVdxf4ZD3Zu9Zv3pb0z7DNQsc3oAL4lgYgmdNL9f6XR6Ufw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/jest": ">=27", + "@xunnamius/types": ">=1" + } + }, + "node_modules/@xunnamius/next-types": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@xunnamius/next-types/-/next-types-1.0.9.tgz", + "integrity": "sha512-6g3MYhaxy5+Cqm3La7XphqntcAsUrqGm28AUctndcilHgk3Tbtf/J984QiHH7X1upjZLur9jo3Lg0DsCQqG1ig==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "next": ">=9" + } + }, + "node_modules/@xunnamius/types": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@xunnamius/types/-/types-1.3.0.tgz", + "integrity": "sha512-RAWA75sRcOsZFROjNPz5+WPeSwJj/T751iNUeT+uVmV4f9tlN+cgiV+A6yUxUPEL5NR6pTzTv8ZwZ/HU78+TcQ==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "type-fest": ">=2.8" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", + "dev": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/anchor-markdown-header": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.6.0.tgz", + "integrity": "sha512-v7HJMtE1X7wTpNFseRhxsY/pivP4uAJbidVhPT+yhz4i/vV1+qx371IXuV9V7bN6KjFtheLJxqaSm0Y/8neJTA==", + "dev": true, + "dependencies": { + "emoji-regex": "~10.1.0" + } + }, + "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, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/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, + "engines": { + "node": ">=10" + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "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" + } + ] + }, + "node_modules/arg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-1.0.0.tgz", + "integrity": "sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==", + "dev": true + }, + "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, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argv-formatter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", + "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", + "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.1.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assign-deep": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/assign-deep/-/assign-deep-1.0.1.tgz", + "integrity": "sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA==", + "dev": true, + "dependencies": { + "assign-symbols": "^2.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/assign-symbols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-2.0.2.tgz", + "integrity": "sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-explicit-exports-references": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-explicit-exports-references/-/babel-plugin-explicit-exports-references-1.0.2.tgz", + "integrity": "sha512-z+weAyF11Mr1azXIR5pfhAeXeK8ZvacXSKgPLGdwBoR7efyqnUxYvlGUny7eHZxAO/Q1C2O1+xO9lwgGPDaBlw==", + "dependencies": { + "@babel/core": "^7.13.15", + "@babel/template": "^7.12.13", + "@babel/types": "^7.13.14", + "debug": "^4.3.1" + }, + "engines": { + "node": ">= 12.x" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-default-named-imports": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-default-named-imports/-/babel-plugin-transform-default-named-imports-1.2.2.tgz", + "integrity": "sha512-xLfgaSMqNEgGhcCIXvSqdWjbcucLPDSNUmVHBYyLLWENLDJ3uOOBRFWyHCItzT727OQeMbpML8DN8ntUS9loSw==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/types": "^7.16.0", + "webpack-node-module-types": "^1.2.1" + }, + "engines": { + "node": ">= 12.x" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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 + }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "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" + } + ] + }, + "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==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true + }, + "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 + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", + "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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, + "engines": { + "node": "*" + } + }, + "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 + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001625", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", + "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dev": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/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, + "engines": { + "node": ">=0.8.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, + "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, + "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, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clipboardy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.2.tgz", + "integrity": "sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==", + "dev": true, + "dependencies": { + "arch": "^2.1.0", + "execa": "^0.8.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/clipboardy/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clipboardy/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/clipboardy/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "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, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "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, + "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 + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/compare-func/node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-6.0.0.tgz", + "integrity": "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-atom": "^5.0.0", + "conventional-changelog-codemirror": "^5.0.0", + "conventional-changelog-conventionalcommits": "^8.0.0", + "conventional-changelog-core": "^8.0.0", + "conventional-changelog-ember": "^5.0.0", + "conventional-changelog-eslint": "^6.0.0", + "conventional-changelog-express": "^5.0.0", + "conventional-changelog-jquery": "^6.0.0", + "conventional-changelog-jshint": "^5.0.0", + "conventional-changelog-preset-loader": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-5.0.0.tgz", + "integrity": "sha512-WfzCaAvSCFPkznnLgLnfacRAzjgqjLUjvf3MftfsJzQdDICqkOOpcMtdJF3wTerxSpv2IAAjX8doM3Vozqle3g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-cli/-/conventional-changelog-cli-5.0.0.tgz", + "integrity": "sha512-9Y8fucJe18/6ef6ZlyIlT2YQUbczvoQZZuYmDLaGvcSBP+M6h+LAvf7ON7waRxKJemcCII8Yqu5/8HEfskTxJQ==", + "dev": true, + "dependencies": { + "add-stream": "^1.0.0", + "conventional-changelog": "^6.0.0", + "meow": "^13.0.0", + "tempfile": "^5.0.0" + }, + "bin": { + "conventional-changelog": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-5.0.0.tgz", + "integrity": "sha512-8gsBDI5Y3vrKUCxN6Ue8xr6occZ5nsDEc4C7jO/EovFGozx8uttCAyfhRrvoUAWi2WMm3OmYs+0mPJU7kQdYWQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-core": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-8.0.0.tgz", + "integrity": "sha512-EATUx5y9xewpEe10UEGNpbSHRC6cVZgO+hXQjofMqpy+gFIrcGvH3Fl6yk2VFKh7m+ffenup2N7SZJYpyD9evw==", + "dev": true, + "dependencies": { + "@hutson/parse-repository-url": "^5.0.0", + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-parser": "^6.0.0", + "git-raw-commits": "^5.0.0", + "git-semver-tags": "^8.0.0", + "hosted-git-info": "^7.0.0", + "normalize-package-data": "^6.0.0", + "read-package-up": "^11.0.0", + "read-pkg": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-core/node_modules/@conventional-changelog/git-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-1.0.1.tgz", + "integrity": "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw==", + "dev": true, + "dependencies": { + "@types/semver": "^7.5.5", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/conventional-changelog-core/node_modules/conventional-commits-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.0.0.tgz", + "integrity": "sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==", + "dev": true, + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-core/node_modules/git-raw-commits": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.0.tgz", + "integrity": "sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==", + "dev": true, + "dependencies": { + "@conventional-changelog/git-client": "^1.0.0", + "meow": "^13.0.0" + }, + "bin": { + "git-raw-commits": "src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-5.0.0.tgz", + "integrity": "sha512-RPflVfm5s4cSO33GH/Ey26oxhiC67akcxSKL8CLRT3kQX2W3dbE19sSOM56iFqUJYEwv9mD9r6k79weWe1urfg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-6.0.0.tgz", + "integrity": "sha512-eiUyULWjzq+ybPjXwU6NNRflApDWlPEQEHvI8UAItYW/h22RKkMnOAtfCZxMmrcMO1OKUWtcf2MxKYMWe9zJuw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-5.0.0.tgz", + "integrity": "sha512-D8Q6WctPkQpvr2HNCCmwU5GkX22BVHM0r4EW8vN0230TSyS/d6VQJDAxGb84lbg0dFjpO22MwmsikKL++Oo/oQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-6.0.0.tgz", + "integrity": "sha512-2kxmVakyehgyrho2ZHBi90v4AHswkGzHuTaoH40bmeNqUt20yEkDOSpw8HlPBfvEQBwGtbE+5HpRwzj6ac2UfA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-5.0.0.tgz", + "integrity": "sha512-gGNphSb/opc76n2eWaO6ma4/Wqu3tpa2w7i9WYqI6Cs2fncDSI2/ihOfMvXveeTTeld0oFvwMVNV+IYQIk3F3g==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-5.0.0.tgz", + "integrity": "sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.0.0.tgz", + "integrity": "sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==", + "dev": true, + "dependencies": { + "@types/semver": "^7.5.5", + "conventional-commits-filter": "^5.0.0", + "handlebars": "^4.7.7", + "meow": "^13.0.0", + "semver": "^7.5.2" + }, + "bin": { + "conventional-changelog-writer": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-writer/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog/node_modules/conventional-changelog-angular": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz", + "integrity": "sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog/node_modules/conventional-changelog-conventionalcommits": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-8.0.0.tgz", + "integrity": "sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-filter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", + "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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 + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "dependencies": { + "jiti": "^1.19.1" + }, + "engines": { + "node": ">=v16" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=8.2", + "typescript": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz", + "integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==", + "dev": true, + "dependencies": { + "globby": "^13.1.2", + "graceful-fs": "^4.2.10", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^5.5.0", + "rimraf": "^3.0.2", + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "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==" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", + "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctoc": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/doctoc/-/doctoc-2.2.1.tgz", + "integrity": "sha512-qNJ1gsuo7hH40vlXTVVrADm6pdg30bns/Mo7Nv1SxuXSM1bwF9b4xQ40a6EFT/L1cI+Yylbyi8MPI4G4y7XJzQ==", + "dev": true, + "dependencies": { + "@textlint/markdown-to-ast": "^12.1.1", + "anchor-markdown-header": "^0.6.0", + "htmlparser2": "^7.2.0", + "minimist": "^1.2.6", + "underscore": "^1.13.2", + "update-section": "^0.3.3" + }, + "bin": { + "doctoc": "doctoc.js" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/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 + }, + "node_modules/duplexer2/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, + "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/duplexer2/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 + }, + "node_modules/duplexer2/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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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 + }, + "node_modules/electron-to-chromium": { + "version": "1.4.783", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", + "integrity": "sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.1.0.tgz", + "integrity": "sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", + "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-ci": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-9.1.1.tgz", + "integrity": "sha512-Im2yEWeF4b2RAMAaWvGioXk6m0UNaIjD8hj28j2ij5ldnIFrDQT0+pzDvpbRkcjurhXhf/AsBKv8P2rtmGi9Aw==", + "dev": true, + "dependencies": { + "execa": "^7.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^16.14 || >=18" + } + }, + "node_modules/env-ci/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/env-ci/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/env-ci/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.3.tgz", + "integrity": "sha512-ZkNztm3Q7hjqvB1rRlOX8P9E/cXRL9ajRcs8jufEtwMfTVYRqnmtnaSu57QqHyBlovMuiB8LEzfLBkh5RYV6Fg==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "14.2.3", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-next/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-import-resolver-alias": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", + "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "dev": true, + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.5.0.tgz", + "integrity": "sha512-6np6DGdmNq/eBbA7HOUNV8fkfL86PYwBfwyb8n23FXgJNTR8+ot3smRHjza9LGsBBZRypK3qyF79vMjohIL8eQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest-dom": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.4.0.tgz", + "integrity": "sha512-yBqvFsnpS5Sybjoq61cJiUsenRkC9K32hYQBFS9doBR7nbQZZ5FyO+X7MlmfM1C48Ejx/qTuOCgukDUNyzKZ7A==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.3", + "requireindex": "^1.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", + "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/dom": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/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 + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "53.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz", + "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^3.0.2", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-loops": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", + "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" + }, + "node_modules/fast-printf": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz", + "integrity": "sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==", + "dev": true, + "dependencies": { + "boolean": "^3.1.4" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dev": true, + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/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 + }, + "node_modules/from2/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, + "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/from2/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 + }, + "node_modules/from2/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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-hooks-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz", + "integrity": "sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==", + "dev": true, + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, + "node_modules/git-log-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", + "integrity": "sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==", + "dev": true, + "dependencies": { + "argv-formatter": "~1.0.0", + "spawn-error-forwarder": "~1.0.0", + "split2": "~1.0.0", + "stream-combiner2": "~1.1.1", + "through2": "~2.0.0", + "traverse": "~0.6.6" + } + }, + "node_modules/git-log-parser/node_modules/split2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", + "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", + "dev": true, + "dependencies": { + "through2": "~2.0.0" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-semver-tags": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-8.0.0.tgz", + "integrity": "sha512-N7YRIklvPH3wYWAR2vysaqGLPRcpwQ0GKdlqTiVN5w1UmCdaeY3K8s6DMKRCh54DDdzyt/OAB6C8jgVtb7Y2Fg==", + "dev": true, + "dependencies": { + "@conventional-changelog/git-client": "^1.0.0", + "meow": "^13.0.0" + }, + "bin": { + "git-semver-tags": "src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/git-semver-tags/node_modules/@conventional-changelog/git-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-1.0.1.tgz", + "integrity": "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw==", + "dev": true, + "dependencies": { + "@types/semver": "^7.5.5", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/git-semver-tags/node_modules/conventional-commits-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.0.0.tgz", + "integrity": "sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/git-semver-tags/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-gitignore": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/glob-gitignore/-/glob-gitignore-1.0.14.tgz", + "integrity": "sha512-YuAEPqL58bOQDqDF2kMv009rIjSAtPs+WPzyGbwRWK+wD0UWQVRoP34Pz6yJ6ivco65C9tZnaIt0I3JCuQ8NZQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "ignore": "^5.0.5", + "lodash.difference": "^4.5.0", + "lodash.union": "^4.6.0", + "make-array": "^1.0.5", + "util.inherits": "^1.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-gitignore/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob-gitignore/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-gitignore/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "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, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, + "node_modules/hook-std": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", + "integrity": "sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-terminator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz", + "integrity": "sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==", + "dev": true, + "dependencies": { + "delay": "^5.0.0", + "p-wait-for": "^3.2.0", + "roarr": "^7.0.4", + "type-fest": "^2.3.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/http-terminator/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.5.tgz", + "integrity": "sha512-fedL7PRwmeVkgyhu9hLeTBaI6wcGk7JGJswdaRsa5aUbkXI1kr1xZwTPBtaYPpwf56878iDek6VbVnuWMebJmw==" + }, + "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, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", + "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, + "engines": { + "node": ">=12.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", + "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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 + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inline-style-prefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", + "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/into-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", + "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "devOptional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "devOptional": true + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "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" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-empty": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-1.2.0.tgz", + "integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-server-side": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-server-side/-/is-server-side-1.0.2.tgz", + "integrity": "sha512-AazkGxiRd+xwIv0OYcvxHIITZccolSEPqVDOxmmApqtEGYX4G+7sucsVUs9CAwcVW+RIRsRNvqr3u7L8vlHdHg==", + "deprecated": "this package has been superseded by the @xunnamius/next-is-server-side package" + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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 + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/issue-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", + "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", + "dev": true, + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-circus/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-config/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-runtime/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-silent-reporter": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jest-silent-reporter/-/jest-silent-reporter-0.6.0.tgz", + "integrity": "sha512-4nmS+5o7ycVlvbQOTx7CnGdbBtP2646hnDgQpQLaVhjHcQNHD+gqBAetyjRDlgpZ8+8N82MWI59K+EX2LsVk7g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-util": "^26.0.0" + } + }, + "node_modules/jest-silent-reporter/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-silent-reporter/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-silent-reporter/node_modules/jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "devOptional": true + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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 + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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 + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levenshtein-edit-distance": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/levenshtein-edit-distance/-/levenshtein-edit-distance-1.0.0.tgz", + "integrity": "sha512-gpgBvPn7IFIAL32f0o6Nsh2g+5uOvkt4eK9epTfgE4YVxBxwVhJ/p1888lMm/u8mXdu1ETLSi6zeEmkBI+0F3w==", + "dev": true, + "bin": { + "levenshtein-edit-distance": "cli.js" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "15.2.5", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.5.tgz", + "integrity": "sha512-j+DfX7W9YUvdzEZl3Rk47FhDF6xwDBV5wwsCPw6BwWZVPYJemusQmvb9bRsW23Sqsaa+vRloAWogbK4BUuU2zA==", + "dev": true, + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.4", + "execa": "~8.0.1", + "lilconfig": "~3.1.1", + "listr2": "~8.2.1", + "micromatch": "~4.0.7", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.4.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.1.tgz", + "integrity": "sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-plugin": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-6.0.3.tgz", + "integrity": "sha512-kc0X2FEUZr145odl68frm+lMJuQ23+rTXYmR6TImqPtbpmXC4vVXbWKDQ9IzndA0HfyQamWfKLhzsqGSTxE63w==", + "dev": true, + "dependencies": { + "@npmcli/config": "^8.0.0", + "import-meta-resolve": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true + }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true + }, + "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 + }, + "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 + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-array": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/make-array/-/make-array-1.0.5.tgz", + "integrity": "sha512-sgK2SAzxT19rWU+qxKUcn6PAh/swiIiz2F8C2cZjLc1z4iwYIfdoihqFIDQ8BDzAGtWPYJ6Sr13K1j/DXynDLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz", + "integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.2.0.tgz", + "integrity": "sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", + "chalk": "^5.2.0", + "cli-table3": "^0.6.3", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.3.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/marked-terminal/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mdast-comment-marker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-comment-marker/-/mdast-comment-marker-3.0.0.tgz", + "integrity": "sha512-bt08sLmTNg00/UtVDiqZKocxqvQqqyQZAg1uaRuO/4ysXV5motg7RolF5o5yy/sY1rG0v2XgZEqFWho1+2UquA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-mdx-expression": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", + "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive/node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/mdast-util-directive/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz", + "integrity": "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^4.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-footnote": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/mdast-util-footnote/-/mdast-util-footnote-0.1.7.tgz", + "integrity": "sha512-QxNdO8qSxqbO2e3m09KwDKfWiLgqyCurdWTQ198NpbZ2hxntdc+VKS4fDJCmNWbAroUdYnSthu+XbZ8ovh8C3w==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "^0.6.0", + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz", + "integrity": "sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==", + "dev": true, + "dependencies": { + "ccount": "^1.0.0", + "mdast-util-find-and-replace": "^1.1.0", + "micromark": "^2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm/node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-hidden": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdast-util-hidden/-/mdast-util-hidden-1.1.4.tgz", + "integrity": "sha512-fmmBDVseRZ9N/Lin0q9aI7aHRDrjNFU1ODRZXBCZCpcy0Ke3RjdtEA/NN153GBnQAof0MPtLIu8mLEtA/X6MHw==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.10", + "@types/unist": "^2.0.6", + "unist-util-remove-position": "^4.0.2", + "unist-util-visit": "^4.1.2" + }, + "engines": { + "node": "^14.19.0 || ^16.13.0 || >=17.4.0" + } + }, + "node_modules/mdast-util-hidden/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-hidden/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/mdast-util-hidden/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-hidden/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-hidden/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dev": true, + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "dev": true, + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dev": true, + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/mdast-util-tight-comments/-/mdast-util-tight-comments-1.0.6.tgz", + "integrity": "sha512-xX6+ksA2T7K3YY4U2YhicRvqpEZIwFxMaVBF09ON6/vMWD3Pc3EXFRYmrnOSVc2u4/CNhLnXOFl0kE4u/WDMKg==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "^1.5.0" + }, + "engines": { + "node": "^14.19.0 || ^16.13.0 || >=17.4.0" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/mdast-util-tight-comments/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-tight-comments/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-tight-comments/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-tight-comments/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-footnote": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/micromark-extension-footnote/-/micromark-extension-footnote-0.3.2.tgz", + "integrity": "sha512-gr/BeIxbIWQoUm02cIfK7mdMZ/fbroRpLsck4kvFtjbzP4yi+OPVbnukTc/zy0i7spC2xYE/dbX1Sur8BEDJsQ==", + "dev": true, + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dev": true, + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", + "dev": true, + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", + "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", + "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", + "dev": true, + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", + "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "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, + "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, + "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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "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, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mongodb": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz", + "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mongodb-memory-server": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.2.0.tgz", + "integrity": "sha512-w/usKdYtby5EALERxmA0+et+D0brP0InH3a26shNDgGefXA61hgl6U0P3IfwqZlEGRZdkbZig3n57AHZgDiwvg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "mongodb-memory-server-core": "9.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.2.0.tgz", + "integrity": "sha512-9SWZEy+dGj5Fvm5RY/mtqHZKS64o4heDwReD4SsfR7+uNgtYo+JN41kPCcJeIH3aJf04j25i5Dia2s52KmsMPA==", + "dev": true, + "dependencies": { + "async-mutex": "^0.4.0", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.6", + "https-proxy-agent": "^7.0.4", + "mongodb": "^5.9.1", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.0", + "tar-stream": "^3.1.7", + "tslib": "^2.6.2", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msw": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.0.tgz", + "integrity": "sha512-cDr1q/QTMzaWhY8n9lpGhceY209k29UZtdTgJ3P8Bzne3TSMchX2EM/ldvn4ATLOktpCefCU2gcEgzHc31GTPw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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 + }, + "node_modules/named-app-errors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/named-app-errors/-/named-app-errors-4.0.2.tgz", + "integrity": "sha512-CvrfIEtZgRYFhgS+QDa9ZVte14/azaVpi7LfpcALGaTN8pZDQUcY0AyZ5Dopx8LiSVwjEOa2ooIaD/DG2Tfjgw==" + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "dev": true + }, + "node_modules/nano-css": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", + "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.0", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nerf-dart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", + "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", + "dev": true + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/next": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", + "dependencies": { + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-test-api-route-handler": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/next-test-api-route-handler/-/next-test-api-route-handler-4.0.7.tgz", + "integrity": "sha512-7lSpltVIVrJK1X06MNVVSgq5MvIzo5LZjdtUmWigt4v4/xpF78QeOgBorB+wfbiIAj6tjzf1/0zeEWiaxbyLsQ==", + "dev": true, + "dependencies": { + "@whatwg-node/server": "^0.9.33", + "cookie": "^0.6.0", + "core-js": "^3.37.0" + }, + "engines": { + "node": "^18.18.2 || ^20.10.0 || >=21.2.0" + }, + "peerDependencies": { + "next": ">=9" + } + }, + "node_modules/next-test-api-route-handler/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next/node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm": { + "version": "9.9.3", + "resolved": "https://registry.npmjs.org/npm/-/npm-9.9.3.tgz", + "integrity": "sha512-Z1l+rcQ5kYb17F3hHtO601arEpvdRYnCLtg8xo3AGtyj3IthwaraEOexI9903uANkifFbqHC8hT53KIrozWg8A==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/run-script", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "sigstore", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^6.5.0", + "@npmcli/config": "^6.4.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^4.0.1", + "@npmcli/promise-spawn": "^6.0.2", + "@npmcli/run-script": "^6.0.2", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^17.1.4", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.3", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^6.1.1", + "ini": "^4.1.1", + "init-package-json": "^5.0.0", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^3.0.1", + "libnpmaccess": "^7.0.2", + "libnpmdiff": "^5.0.20", + "libnpmexec": "^6.0.4", + "libnpmfund": "^4.2.1", + "libnpmhook": "^9.0.3", + "libnpmorg": "^5.0.4", + "libnpmpack": "^5.0.20", + "libnpmpublish": "^7.5.1", + "libnpmsearch": "^6.0.2", + "libnpmteam": "^5.0.3", + "libnpmversion": "^4.0.2", + "make-fetch-happen": "^11.1.1", + "minimatch": "^9.0.3", + "minipass": "^7.0.4", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^9.4.1", + "nopt": "^7.2.0", + "normalize-package-data": "^5.0.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.2", + "npm-profile": "^7.0.1", + "npm-registry-fetch": "^14.0.5", + "npm-user-validate": "^2.0.0", + "npmlog": "^7.0.1", + "p-map": "^4.0.0", + "pacote": "^15.2.0", + "parse-conflict-json": "^3.0.1", + "proc-log": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^2.1.0", + "semver": "^7.6.0", + "sigstore": "^1.9.0", + "spdx-expression-parse": "^3.0.1", + "ssri": "^10.0.5", + "supports-color": "^9.4.0", + "tar": "^6.2.0", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.0", + "which": "^3.0.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "6.5.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/installed-package-contents": "^2.0.2", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^5.0.0", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^4.0.0", + "@npmcli/query": "^3.1.0", + "@npmcli/run-script": "^6.0.0", + "bin-links": "^4.0.1", + "cacache": "^17.0.4", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "json-stringify-nice": "^1.1.4", + "minimatch": "^9.0.0", + "nopt": "^7.0.0", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.1", + "npm-registry-fetch": "^14.0.3", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "parse-conflict-json": "^3.0.0", + "proc-log": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.2", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.1", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "6.4.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^17.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^15.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "make-fetch-happen": "^11.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.0", + "tuf-js": "^1.1.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "17.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/gauge": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.3.10", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hasown": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.0.0", + "promzard": "^1.0.0", + "read": "^2.0.0", + "read-package-json": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.3.6", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "5.0.21", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/disparity-colors": "^3.0.0", + "@npmcli/installed-package-contents": "^2.0.2", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^9.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8", + "tar": "^6.1.13" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "6.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/run-script": "^6.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^10.1.0", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "proc-log": "^3.0.0", + "read": "^2.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "4.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "9.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "5.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "5.0.21", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/run-script": "^6.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "7.5.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^5.0.0", + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3", + "proc-log": "^3.0.0", + "semver": "^7.3.7", + "sigstore": "^1.4.0", + "ssri": "^10.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.1", + "@npmcli/run-script": "^6.0.0", + "json-parse-even-better-errors": "^3.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "11.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "9.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "16.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "10.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "14.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npmlog": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "15.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/pacote/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.10.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "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" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.6.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "1.9.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "@sigstore/sign": "^1.0.0", + "@sigstore/tuf": "^1.0.3", + "make-fetch-happen": "^11.0.1" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.17", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": 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/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": 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/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "1.1.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", + "dev": true + }, + "node_modules/p-each-series": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", + "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "dev": true, + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.2.tgz", + "integrity": "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map/node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map/node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", + "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", + "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", + "dev": true, + "dependencies": { + "p-timeout": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "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, + "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.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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 + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/propose": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/propose/-/propose-0.0.5.tgz", + "integrity": "sha512-Jary1vb+ap2DIwOGfyiadcK4x1Iu3pzpkDBy8tljFPmQvnc9ES3m1PMZOMiWOG50cfoAyYNtGeBzrp+Rlh4G9A==", + "dev": true, + "dependencies": { + "levenshtein-edit-distance": "^1.0.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/quotation": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quotation/-/quotation-2.0.3.tgz", + "integrity": "sha512-yEc24TEgCFLXx7D4JHJJkK4JFVtatO8fziwUxY4nB/Jbea9o9CVS3gt22mA0W7rPYAGW2fWzYDSOtD94PwOyqA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/random-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-case/-/random-case-1.0.0.tgz", + "integrity": "sha512-bMlDjeg8GB38Ju0hK8eII3qPQucgFm+sFWCmucwnHlgeCqm99uhmwR3d+dU3sJV2Dn+bp7XnFUygUidrRbM4Vg==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", + "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", + "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "index-to-position": "^0.1.2", + "type-fest": "^4.7.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dev": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/regexpu-core/node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-capitalize-headings": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remark-capitalize-headings/-/remark-capitalize-headings-2.0.1.tgz", + "integrity": "sha512-EsaPpdH2i65malnC3QFRQFqh0Qenh4k/K5+X7b51+tzggSykrXgmhTvj3npSbtRiwWvJ34d44m9qsgszuBowhg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "mdast-util-to-string": "^4.0.0", + "title": "^3.5.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-capitalize-headings/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-capitalize-headings/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-cli": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/remark-cli/-/remark-cli-12.0.1.tgz", + "integrity": "sha512-2NAEOACoTgo+e+YAaCTODqbrWyhMVmlUyjxNCkTrDRHHQvH6+NbrnqVvQaLH/Q8Ket3v90A43dgAJmXv8y5Tkw==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^4.0.0", + "markdown-extensions": "^2.0.0", + "remark": "^15.0.0", + "unified-args": "^11.0.0" + }, + "bin": { + "remark": "cli.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-footnotes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-3.0.0.tgz", + "integrity": "sha512-ZssAvH9FjGYlJ/PBVKdSmfyPc3Cz4rTWgZLI4iE/SX8Nt5l3o3oEjv3wwG5VD7xOjktzdwp5coac+kJV9l4jgg==", + "dev": true, + "dependencies": { + "mdast-util-footnote": "^0.1.0", + "micromark-extension-footnote": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-frontmatter/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm/node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-ignore": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-ignore/-/remark-ignore-2.0.0.tgz", + "integrity": "sha512-6firt+yoiPJCPbU/6ep/lOUzcZw5HnmUjXdJcuvmSyyXo5vZNIZYKBB7/dq67nuxF7G8W715Tg73HBUbL1mOkg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "mdast-comment-marker": "^3.0.0", + "mdast-util-hidden": "^1.1.4", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-ignore/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-ignore/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-inline-links": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/remark-inline-links/-/remark-inline-links-7.0.0.tgz", + "integrity": "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-definitions": "^6.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/remark-lint/-/remark-lint-10.0.0.tgz", + "integrity": "sha512-E8yHHDOJ8b+qI0G49BRu24pe8t0fNNBWv8ENQJpCGNrVeTeyBIGEbaUe1yuF7OG8faA6PVpcN/pqWjzW9fcBWQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-message-control": "^8.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-definition-case": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-definition-case/-/remark-lint-definition-case-4.0.0.tgz", + "integrity": "sha512-XBmMmj8ii0KZUuJf7ZaVXDGp2+DWE02re9qn/6mV23rBpsDmpz7U1lQWRlwFQIE5q5bxIxP5pX7hDeTH0Egy9Q==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-definition-case/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-definition-case/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-fenced-code-flag/-/remark-lint-fenced-code-flag-4.0.0.tgz", + "integrity": "sha512-Zs0wJd4nRvBo/9NWQVfWg5Ykapbo0Zzw/SyZc3f0h73S1gTZZcfeU+bA5oDivlBdcUgLBsyHRE0QaoaVvN3/Wg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "quotation": "^2.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag-case": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-fenced-code-flag-case/-/remark-lint-fenced-code-flag-case-2.0.0.tgz", + "integrity": "sha512-tp+4msMsx9LKRtv3CZNBWsvvKuX9z2vosuSpmYhA6DYFmlZYyPNnIzWitIMZj5Sw4sbmT8XQeHNsrbYeYx2SOg==", + "dev": true, + "dependencies": { + "unified-lint-rule": "^2.1.2", + "unist-util-generated": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag-case/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-flag/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-file-extension": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-file-extension/-/remark-lint-file-extension-3.0.0.tgz", + "integrity": "sha512-wrOKiGvcl/ftB7FkeX2/l13ALvhKXV77HGR8AXo86cVY2pD+K0WdOC52DV3ldgpUXpWzE9kcgF8bbkxwzKpFFg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "quotation": "^2.0.0", + "unified-lint-rule": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-final-newline/-/remark-lint-final-newline-3.0.0.tgz", + "integrity": "sha512-NaPyn6FiOn3IV/6gIcwWfJmgraPT2IaVLjhakfPglZkKVfn/FrOfETyY8Bp+HLoSRI9967OH0yRDnK7/pPIWeQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "unified-lint-rule": "^3.0.0", + "vfile-location": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-first-heading-level": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-first-heading-level/-/remark-lint-first-heading-level-4.0.0.tgz", + "integrity": "sha512-CAMSDt03MwzGD1bgOZ+UaE0lN1xZXtJZGRXwMg7OmGazLVDtTsRkMEK0YY2LUyozdHBUFoq6FQt5pKyYdt/Tmw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-mdx": "^3.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-first-heading-level/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-first-heading-level/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-hard-break-spaces/-/remark-lint-hard-break-spaces-4.0.0.tgz", + "integrity": "sha512-zCTq7/xfM0ZL3bMopXse9DH2nk38wE1LrxmYwnTrqASBLnEAJWE2U2//tRGVMEBfSAnNvmIo96twz6zkLWjbGA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-increment": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-heading-increment/-/remark-lint-heading-increment-4.0.0.tgz", + "integrity": "sha512-TARnsjXWzY/yLwxh/y4+KnDSXO3Koue8Crp55T8G9pjj3vw+XgTAG35zSpIIY9HmGiQ2a3R0SOj2pAxATpnckg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-mdx": "^3.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-increment/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-increment/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-heading-whitespace/-/remark-lint-heading-whitespace-1.0.0.tgz", + "integrity": "sha512-DykoBIXNbkihg64D+mztSOv3l82RTH4tIZW/HUB4QM4NpIEB+pVIPQpCYD0K4pTgvKiwoqsj4NY8qJ1EhNHAmQ==", + "dev": true, + "dependencies": { + "mdast-util-to-string": "^1.0.4", + "unified-lint-rule": "^1.0.2", + "unist-util-visit": "^1.3.0" + } + }, + "node_modules/remark-lint-heading-whitespace/node_modules/mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-whitespace/node_modules/unified-lint-rule": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-1.0.6.tgz", + "integrity": "sha512-YPK15YBFwnsVorDFG/u0cVVQN5G2a3V8zv5/N6KN3TCG+ajKtaALcy7u14DCSrJI+gZeyYquFL9cioJXOGXSvg==", + "dev": true, + "dependencies": { + "wrapped": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-whitespace/node_modules/unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "dev": true + }, + "node_modules/remark-lint-heading-whitespace/node_modules/unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dev": true, + "dependencies": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "node_modules/remark-lint-heading-whitespace/node_modules/unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dev": true, + "dependencies": { + "unist-util-is": "^3.0.0" + } + }, + "node_modules/remark-lint-heading-word-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-heading-word-length/-/remark-lint-heading-word-length-2.0.0.tgz", + "integrity": "sha512-433PRsnCxmwfksZ34yvms/YgKRP95gwDY/nxPvs4oNILydo0g+DOxw4Wosy0tfk9kBXSSyOvl2pvIGhLGAwxNQ==", + "dev": true, + "dependencies": { + "mdast-util-to-string": "^4.0.0", + "unified-lint-rule": "^2.1.2", + "unist-util-generated": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-heading-word-length/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-word-length/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-style": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-style/-/remark-lint-list-item-style-2.0.0.tgz", + "integrity": "sha512-iMTrdMGS28vIWXd+sg6fUjOBp2gah7Z15VCOcEP3Sl7/Zhpkeq5hNQb5ITaRb/IpSWaG9PGY2DMCd4PjUy8klA==", + "dev": true, + "dependencies": { + "mdast-util-to-string": "^4.0.0", + "unified-lint-rule": "^2.1.2", + "unist-util-generated": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-list-item-style/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-auto-link-without-protocol/-/remark-lint-no-auto-link-without-protocol-3.1.2.tgz", + "integrity": "sha512-mPIdFOGxdDhCMa2qIzjzjDzDoQeyK+/1BBgsseqThuBtoAoXR5l1TZfII2isNbBo6L8d+fMFdx1/3qALoDjtcA==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-auto-link-without-protocol/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-blockquote-without-marker/-/remark-lint-no-blockquote-without-marker-6.0.0.tgz", + "integrity": "sha512-fBhoTpkWcl5tG4FdwPdJIyb8XLrdr6MdLk1+K2BQ6Rom3rRsIYvuox4ohxOunNrXuth8xyw8kC6wDmODR44oFw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-directive": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "pluralize": "^8.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-location": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-defined-urls": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-defined-urls/-/remark-lint-no-duplicate-defined-urls-3.0.0.tgz", + "integrity": "sha512-i6oGry+ISf3M5bu3okPspbOd6tgZj6biWJIOFT6K7lbKunnFpbIeosmoa1ZyY6CBp3N6i33toSiX+xk50F06vQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-phrasing": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-defined-urls/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-defined-urls/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-definitions/-/remark-lint-no-duplicate-definitions-4.0.0.tgz", + "integrity": "sha512-21fcOACkCyhNsHkedKlpvqIywYx+5zGR507bW8e59gzdGhTbnBwQ9du4ACmN9jxPTfIBhUVMz0bWezkGrHE7Bg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-phrasing": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-headings-in-section": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-headings-in-section/-/remark-lint-no-duplicate-headings-in-section-4.0.0.tgz", + "integrity": "sha512-O6TPMdSs8AFEQeTA1mPI9CgCtJJ9UkfnXz1IlqRdTrpsCJ2DlwaPPXjS2LafQCHoJyf45DgjSwekHeV4WxHYxw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-headings-in-section/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-headings-in-section/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-sections": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-empty-sections/-/remark-lint-no-empty-sections-4.0.0.tgz", + "integrity": "sha512-Tx1nCu7Dq3dsJ500402sSvM0uVK/6khSuEjx8K8u9aHN+Y4vjL6h88xVzdzCmZq2J2yqyFnvMjG1y7lQv+DRvg==", + "dev": true, + "dependencies": { + "mdast-util-to-string": "^1.0.2", + "unified-lint-rule": "^1.0.0", + "unist-util-visit": "^1.0.0" + } + }, + "node_modules/remark-lint-no-empty-sections/node_modules/mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-sections/node_modules/unified-lint-rule": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-1.0.6.tgz", + "integrity": "sha512-YPK15YBFwnsVorDFG/u0cVVQN5G2a3V8zv5/N6KN3TCG+ajKtaALcy7u14DCSrJI+gZeyYquFL9cioJXOGXSvg==", + "dev": true, + "dependencies": { + "wrapped": "^1.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-sections/node_modules/unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "dev": true + }, + "node_modules/remark-lint-no-empty-sections/node_modules/unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dev": true, + "dependencies": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "node_modules/remark-lint-no-empty-sections/node_modules/unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dev": true, + "dependencies": { + "unist-util-is": "^3.0.0" + } + }, + "node_modules/remark-lint-no-empty-url": { + "version": "3.1.1", + "resolved": "https://xunn.at/remark-lint-no-empty-url", + "integrity": "sha512-OkmCBzKdg4K87CqTOb2j/u8tJ35rl0RSEMcL+fGzvG1/SbUehWHeOgP5S04T0gB6DdJxNnbo/q/aaCZUclHb4g==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-no-empty-url/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-empty-url/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-heading-content-indent/-/remark-lint-no-heading-content-indent-5.0.0.tgz", + "integrity": "sha512-psYSlD2BjcVkgpeXOLwPcYFBrbtJWp8E8JX1J4vSfoHPeY6aIxgYxXkf57cjGTApfRL8xawBmMDiF1FgQvpZYg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "pluralize": "^8.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-like-paragraph": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-heading-like-paragraph/-/remark-lint-no-heading-like-paragraph-4.0.0.tgz", + "integrity": "sha512-6P22xHF+2omrQUA9ujQ5yBUlZr3goaAxoa1t3wNSIydzqkbgwLS+RlpGsVvilW1Q8h4JgWpB42Zpflat6xr90g==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "pluralize": "^8.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-like-paragraph/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-like-paragraph/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-punctuation": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-heading-punctuation/-/remark-lint-no-heading-punctuation-4.0.0.tgz", + "integrity": "sha512-7V23C3Q4yX9zEOLZdbv6o8wVxxeWB/F+h9by55zPyk2AwbqF2t2xevnAmN3XFmKZABDTqLwjQxtK6bCVv/S1PQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-punctuation/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-punctuation/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-inline-padding/-/remark-lint-no-inline-padding-4.1.2.tgz", + "integrity": "sha512-dGyhWsiqCZS3Slob0EVBUfsFBbdpMIBCvb56LlCgaHbnLsnNYx8PpF/wA5CgsN8BXIbXfRpyPB5cIJwIq5taYg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-lint-no-inline-padding/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-literal-urls/-/remark-lint-no-literal-urls-4.0.0.tgz", + "integrity": "sha512-rl/3Ai4Ax9IH/fRpOJZuXk1HgYX6oFTauhmBOilpqbq/YT2kN3FuXaneXdRfKv1bgMdHaLKxHWxGj/mDyA2n8w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-multiple-toplevel-headings": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-multiple-toplevel-headings/-/remark-lint-no-multiple-toplevel-headings-4.0.0.tgz", + "integrity": "sha512-JW11iYxza7asDdhQuKfr8SH1u4NBOCQ4U7Ru0HrKCPcT4y/AB1C1il5uMQzbcervgYPBq69xzyQ24+AJeL0t3A==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-mdx": "^3.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-multiple-toplevel-headings/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-multiple-toplevel-headings/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-reference-like-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-reference-like-url/-/remark-lint-no-reference-like-url-4.0.0.tgz", + "integrity": "sha512-YkP8qWdrWDr9s8JLbJoC/U5Z0fU7hpa59Nin0i8zEOtD9coiYoI/YtXRKqSXuAKDmweb+JckhqtKuGJS5u77+w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-reference-like-url/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-reference-like-url/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shell-dollars": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-shell-dollars/-/remark-lint-no-shell-dollars-4.0.0.tgz", + "integrity": "sha512-ye2h8FzjsgqqQV0HHN2g9N4FqI3eD9Gpgu7tU5ADIJyQ3mUJdwBoFn7IlGnpmumR1fb/l6u/AhRavIZxXYqG+Q==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "collapse-white-space": "^2.0.0", + "mdast-util-phrasing": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shell-dollars/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shell-dollars/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-image/-/remark-lint-no-shortcut-reference-image-4.0.0.tgz", + "integrity": "sha512-YEiCpW5F/8/LZyxlOuVK2L/n0NJ1AB0AJK7oP39OVyEk3Xl7w+JQi6nZ3KiH6REh+PWGqKn6M0KEPL9cT/iAOw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-link/-/remark-lint-no-shortcut-reference-link-4.0.0.tgz", + "integrity": "sha512-6jka2Zz3I6G2MvDcKrwADYhTOxHMFMK854u1cfBEIH5/XnCCXROtoqiiDtbZw+NJqbmwsBKvGL4t2gnmEJUmgg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-tabs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-tabs/-/remark-lint-no-tabs-4.0.0.tgz", + "integrity": "sha512-rQR7LDdcw047ajB3D+v9uzdB8aZfZtEdlUJvQXKkcVDteWiuXGC3PcIrmM/8n3J/wlFMuwoAaW2IcdlJf8HzXQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "vfile-location": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-undefined-references/-/remark-lint-no-undefined-references-5.0.0.tgz", + "integrity": "sha512-O0q8bHpRHK1T85oqO+uep4BkvQnZZp3y+wahDeeLLq9dCJfF56sq6Tt5OOTt1BAOZlpobS3OPQHUiJWYP6hX1w==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-location": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-no-unused-definitions/-/remark-lint-no-unused-definitions-4.0.0.tgz", + "integrity": "sha512-YCZ6k575NCTx7mnN+9ls0G6YgMsZHi0LYQqfLW8MNVHBtbpTBvfmk8I39bmsvuKWeBD98weZoXSDqIiIGg+Q/g==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-ordered-list-marker-style/-/remark-lint-ordered-list-marker-style-4.0.0.tgz", + "integrity": "sha512-xZ7Xppy5fzACH4b9h1b4lTzVtNY2AlUkNTfl1Oe6cIKN8tk3juFxN0wL2RpktPtSZ7iRIabzFmg6l8WPhlASJA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "micromark-util-character": "^2.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-ordered-list-marker-value/-/remark-lint-ordered-list-marker-value-4.0.0.tgz", + "integrity": "sha512-7UjNU2Nv9LGEONTU9GPmTVoNoGKD5aL1X2xHzMbSJiTc50bfcazYqZawO+qj1pQ04WPhto1qHnl0HRB5wwSVwA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-phrasing": "^4.0.0", + "micromark-util-character": "^2.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-value/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-value/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strikethrough-marker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-strikethrough-marker/-/remark-lint-strikethrough-marker-3.0.0.tgz", + "integrity": "sha512-XPt5oO3txmoL6vvAZjQ9e42Q5JpaahubryJNzsFy0dAotuEws+Lpi93OH/T68/T0/PSzD/EpoZmSASkySAwG7Q==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strikethrough-marker/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strikethrough-marker/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-unordered-list-marker-style": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-lint-unordered-list-marker-style/-/remark-lint-unordered-list-marker-style-4.0.0.tgz", + "integrity": "sha512-XlP4Wr4KJNovyWVv0H5axfUlF23iE9Kt2SxaVq4+ieum5YcMmKE6KsL+aqt3kiJb60SH1u6a0bxKFvdM/9riOA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-phrasing": "^4.0.0", + "unified-lint-rule": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-unordered-list-marker-style/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-unordered-list-marker-style/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-lint/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/remark-message-control/-/remark-message-control-8.0.0.tgz", + "integrity": "sha512-brpzOO+jdyE/mLqvqqvbogmhGxKygjpCUCG/PwSCU43+JZQ+RM+sSzkCWBcYvgF3KIAVNIoPsvXjBkzO7EdsYQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-comment-marker": "^3.0.0", + "unified-message-control": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-parse/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reference-links": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-7.0.0.tgz", + "integrity": "sha512-OMACEps7CkpBio5nutUToCcXFJr9QkkoHdku41iIholMdFZ0jdRxgFmPm2B7R+DSvW83ZShdA3ubWTH+C3M6Eg==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-remove-unused-definitions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-remove-unused-definitions/-/remark-remove-unused-definitions-2.0.0.tgz", + "integrity": "sha512-qfcg6h9baITxn8IK16CUGgrQHTuBVxI8sOQBOq69P9anSvme00/XQ6+fwQiyle6RCcahfmaRTeuJoVZhjsUvNQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "unified": "^11.0.4", + "unist-util-generated": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-remove-unused-definitions/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-remove-unused-definitions/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-remove-url-trailing-slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-remove-url-trailing-slash/-/remark-remove-url-trailing-slash-2.0.0.tgz", + "integrity": "sha512-qv5ujMH42OEhfDbWw9LEPbacSOnZkhtHZgsNQnxljhT5qVyNFbb+6JEsyJYRxCAqzodU14t8SqE8TleBfJMAxw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-remove-url-trailing-slash/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-remove-url-trailing-slash/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-renumber-references": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-renumber-references/-/remark-renumber-references-2.0.0.tgz", + "integrity": "sha512-V6P6ki5vTxxM8Y+z1JiEvrhh3eM+ToxAidUI3g7S3WaqgaHAMQXHBKNxGqIZwlTCXY8Ub0LJTfV4Njm9ElGcvQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "mdast-util-hidden": "^1.1.4", + "remark-inline-links": "^7.0.0", + "remark-reference-links": "^7.0.0", + "unified": "^11.0.4", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-renumber-references/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-renumber-references/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-renumber-references/node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-sort-definitions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-sort-definitions/-/remark-sort-definitions-2.0.0.tgz", + "integrity": "sha512-rZDoGDisBI3n6w0V6dQ1yeltI3t/8/tayD6y+7Pu8evZlr4VB6pekigbNcT9OOpnEnzZtWPq3Tvb5yFEUpuCSQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-sort-definitions/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-sort-definitions/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-stringify/node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-tight-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-tight-comments/-/remark-tight-comments-2.0.0.tgz", + "integrity": "sha512-vytOQpV8/nuZL7D5QDdUuS2eZa15mueTyRrgQvGmButMV7UMnozn/VC/lQiYefwtruSJ0WfelHhPRAxHZksj/g==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.3", + "mdast-util-tight-comments": "^1.0.6", + "unified": "^11.0.4" + }, + "engines": { + "node": "^18.16.0 || >=20.4.0" + } + }, + "node_modules/remark-tight-comments/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-tight-comments/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-validate-links": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/remark-validate-links/-/remark-validate-links-13.0.1.tgz", + "integrity": "sha512-GWDZWJAQU0+Fsm1GCLNeJoVcE9L3XTVrWCgQZOYREfXqRFIYaSoIBbARZizLm/vBESq+a3GwEBnIflSCNw26tw==", + "dev": true, + "dependencies": { + "@types/hosted-git-info": "^3.0.0", + "@types/mdast": "^4.0.0", + "github-slugger": "^2.0.0", + "hosted-git-info": "^7.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "propose": "0.0.5", + "trough": "^2.0.0", + "unified-engine": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark/node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request-ip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", + "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==" + }, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/roarr": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz", + "integrity": "sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==", + "dev": true, + "dependencies": { + "fast-printf": "^1.6.9", + "safe-stable-stringify": "^2.4.3", + "semver-compare": "^1.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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 + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release": { + "version": "22.0.7", + "resolved": "https://xunn.at/semantic-release-atam@22.0.7", + "integrity": "sha512-4odsrCDkGV2J7RdqaK/BaQclsYd+wGCeyn4h57h6qMqDLvA5hBUI3aER1UyJ0KFaS/6hEvmIryaGI6fGOnL9Tg==", + "dev": true, + "dependencies": { + "@semantic-release/commit-analyzer": "^10.0.0", + "@semantic-release/error": "^4.0.0", + "@semantic-release/github": "^9.0.0", + "@semantic-release/npm": "^10.0.2", + "@semantic-release/release-notes-generator": "^11.0.0", + "aggregate-error": "^4.0.1", + "cosmiconfig": "^8.0.0", + "debug": "^4.0.0", + "env-ci": "^9.0.0", + "execa": "^7.0.0", + "figures": "^5.0.0", + "find-versions": "^5.1.0", + "get-stream": "^6.0.0", + "git-log-parser": "^1.2.0", + "hook-std": "^3.0.0", + "hosted-git-info": "^6.0.0", + "lodash-es": "^4.17.21", + "marked": "^5.0.0", + "marked-terminal": "^5.1.1", + "micromatch": "^4.0.2", + "p-each-series": "^3.0.0", + "p-reduce": "^3.0.0", + "read-pkg-up": "^9.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.2", + "semver-diff": "^4.0.0", + "signale": "^1.2.1", + "yargs": "^17.5.1" + }, + "bin": { + "semantic-release": "bin/semantic-release.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/semantic-release/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/semantic-release/node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/semantic-release/node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/semantic-release/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/semantic-release/node_modules/figures": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/semantic-release/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/semantic-release/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/semantic-release/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/semantic-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semantic-release/node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semantic-release/node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semantic-release/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/semantic-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/read-pkg": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", + "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/read-pkg-up": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", + "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^7.1.0", + "type-fest": "^2.5.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semantic-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/semantic-release/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.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, + "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/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "node_modules/signale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", + "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", + "dev": true, + "dependencies": { + "chalk": "^2.3.2", + "figures": "^2.0.0", + "pkg-conf": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/signale/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, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/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, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/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, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/signale/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 + }, + "node_modules/signale/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, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/signale/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/simple-git": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.24.0.tgz", + "integrity": "sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "devOptional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sort-object-keys": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", + "integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==", + "dev": true + }, + "node_modules/sort-package-json": { + "version": "2.3.0", + "resolved": "https://xunn.at/sort-package-json", + "integrity": "sha512-efTk1wTodMGOvLTM+HX2HveURBKttzewX+v/q0t0fHNecJLnbf2SIbYhQ16Z/r/+Em/+6Sphusi2QGTgWxzrEQ==", + "dev": true, + "dependencies": { + "detect-indent": "^7.0.1", + "detect-newline": "^4.0.0", + "git-hooks-list": "^3.0.0", + "globby": "^13.1.2", + "is-plain-obj": "^4.1.0", + "sort-object-keys": "^1.1.3" + }, + "bin": { + "sort-package-json": "cli.js" + } + }, + "node_modules/sort-package-json/node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/spawn-error-forwarder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", + "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/spellchecker": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/spellchecker/-/spellchecker-3.7.1.tgz", + "integrity": "sha512-j36QRZrekxPXy58fo2B/Le3GzHryLv9Zq2Hqz907+JmUBCP35tJlwwhCo4n1lwisBDK40IFHqEHPUe5gwUkpwA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "any-promise": "^1.3.0", + "nan": "^2.14.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "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 + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", + "dev": true, + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-combiner2/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 + }, + "node_modules/stream-combiner2/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, + "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/stream-combiner2/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 + }, + "node_modules/stream-combiner2/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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "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, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "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": { + "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, + "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 + }, + "node_modules/string-width-cjs/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/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 + }, + "node_modules/string-width/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, + "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, + "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, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-5.0.0.tgz", + "integrity": "sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==", + "dev": true, + "dependencies": { + "temp-dir": "^3.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/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 + }, + "node_modules/through2/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, + "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/through2/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 + }, + "node_modules/through2/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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/title": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/title/-/title-3.5.3.tgz", + "integrity": "sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==", + "dev": true, + "dependencies": { + "arg": "1.0.0", + "chalk": "2.3.0", + "clipboardy": "1.2.2", + "titleize": "1.0.0" + }, + "bin": { + "title": "bin/title.js" + } + }, + "node_modules/title/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, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/title/node_modules/chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/title/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, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/title/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 + }, + "node_modules/title/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, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/title/node_modules/has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/title/node_modules/supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==", + "dev": true, + "dependencies": { + "has-flag": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/titleize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.0.tgz", + "integrity": "sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "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, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toss-expression": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/toss-expression/-/toss-expression-0.1.2.tgz", + "integrity": "sha512-NYAtPRTJwhGkswslP2MSzbWA3QGTwETSvzO4C1V0wMMt7y3UcH7GSDTnJLMSZvWE2AI9plELrBt1NweQbB782A==" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/traverse": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz", + "integrity": "sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==", + "dev": true, + "dependencies": { + "gopd": "^1.0.1", + "typedarray.prototype.slice": "^1.0.3", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig-replace-paths": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/tsconfig-replace-paths/-/tsconfig-replace-paths-0.0.14.tgz", + "integrity": "sha512-If9w1Z/HdSJkrZvCRSKHA3vAL3SwRWvI7rLmuTozlmKuBjPF/R8dWJku5C9ncOqiTOd8Dzd7SZ1cIYVZfpva8g==", + "dev": true, + "dependencies": { + "commander": "^3.0.2", + "globby": "^10.0.1", + "json5": "^2.2.0" + }, + "bin": { + "tsconfig-replace-paths": "dist/commonjs/index.js" + } + }, + "node_modules/tsconfig-replace-paths/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/tsconfig-replace-paths/node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true + }, + "node_modules/tsconfig-replace-paths/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tsconfig-replace-paths/node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsconfig-replace-paths/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tsconfig-replace-paths/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", + "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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 + }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz", + "integrity": "sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-errors": "^1.3.0", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-offset": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", + "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.7" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.0.3.tgz", + "integrity": "sha512-0tZbeVGGCd4+lpoIX+yHWgUfyaLZCQCgJOpuVdTtOtD3+jKaedJ4sl/tkNaYBPeWVKiyDkSHfGuHkq53jlzIFg==", + "dev": true, + "peerDependencies": { + "typedoc": "0.25.x" + } + }, + "node_modules/typedoc/node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dev": true, + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-args": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/unified-args/-/unified-args-11.0.1.tgz", + "integrity": "sha512-WEQghE91+0s3xPVs0YW6a5zUduNLjmANswX7YbBfksHNDGMjHxaWCql4SR7c9q0yov/XiIEdk6r/LqfPjaYGcw==", + "dev": true, + "dependencies": { + "@types/text-table": "^0.2.0", + "chalk": "^5.0.0", + "chokidar": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "json5": "^2.0.0", + "minimist": "^1.0.0", + "strip-ansi": "^7.0.0", + "text-table": "^0.2.0", + "unified-engine": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-args/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/unified-args/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/unified-args/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/unified-engine": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/unified-engine/-/unified-engine-11.2.1.tgz", + "integrity": "sha512-xBAdZ8UY2X4R9Hm6X6kMne4Nz0PlpOc1oE6DPeqJnewr5Imkb8uT5Eyvy1h7xNekPL3PSWh3ZJyNrMW6jnNQBg==", + "dev": true, + "dependencies": { + "@types/concat-stream": "^2.0.0", + "@types/debug": "^4.0.0", + "@types/is-empty": "^1.0.0", + "@types/node": "^20.0.0", + "@types/unist": "^3.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.0.0", + "extend": "^3.0.0", + "glob": "^10.0.0", + "ignore": "^5.0.0", + "is-empty": "^1.0.0", + "is-plain-obj": "^4.0.0", + "load-plugin": "^6.0.0", + "parse-json": "^7.0.0", + "trough": "^2.0.0", + "unist-util-inspect": "^8.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0", + "vfile-reporter": "^8.0.0", + "vfile-statistics": "^3.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-engine/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unified-engine/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/unified-engine/node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-engine/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-lint-rule": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-3.0.0.tgz", + "integrity": "sha512-Sz96ILLsTy3djsG3H44zFb2b77MFf9CQVYnV3PWkxgRX8/n31fFrr+JnzUaJ6cbOHTtZnL1A71+YodsTjzwAew==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "trough": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule/node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified-lint-rule/node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unified-message-control/-/unified-message-control-5.0.0.tgz", + "integrity": "sha512-B2cSAkpuMVVmPP90KCfKdBhm1e9KYJ+zK3x5BCa0N65zpq1Ybkc9C77+M5qwR8FWO7RF3LM5QRRPZtgjW6DUCw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/unified/node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified/node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-generated": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-3.0.0.tgz", + "integrity": "sha512-yUjV3LueHK6gH74UKRwZ/Frc43/Mil9oYBHgiQp3z+v0+yS43pJ+Ye8RvZtApW0Xnx1cdWR0z4f3OZhSruF0/Q==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-inspect": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/unist-util-inspect/-/unist-util-inspect-8.0.0.tgz", + "integrity": "sha512-/3Wn/wU6/H6UEo4FoYUeo8KUePN8ERiZpQYFWYoihOsr1DoDuv80PeB0hobVZyYSvALa2e556bG1A1/AbwU4yg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", + "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/unist-util-remove-position/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/unist-util-visit/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==" + }, + "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, + "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, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-section": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/update-section/-/update-section-0.3.3.tgz", + "integrity": "sha512-BpRZMZpgXLuTiKeiu7kK0nIPwGdyrqrs6EDSaXtjD/aQ2T+qVo9a5hRC3HN3iJjCMxNT/VxoLGQ7E/OzE5ucnw==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "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 + }, + "node_modules/util.inherits": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util.inherits/-/util.inherits-1.0.3.tgz", + "integrity": "sha512-gMirHcfcq5D87nXDwbZqf5vl65S0mpMZBsHXJsXOO3Hc3G+JoQLwgaJa1h+PL7h3WhocnuLqoe8CuvMlztkyCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-8.1.1.tgz", + "integrity": "sha512-qxRZcnFSQt6pWKn3PAk81yLK2rO2i7CDXpy8v8ZquiEOMLSnPw6BMSi9Y1sUCwGGl7a9b3CJT1CKpnRF7pp66g==", + "dev": true, + "dependencies": { + "@types/supports-color": "^8.0.0", + "string-width": "^6.0.0", + "supports-color": "^9.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0", + "vfile-sort": "^4.0.0", + "vfile-statistics": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/vfile-reporter/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/vfile-reporter/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vfile-reporter/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/vfile-reporter/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/vfile-sort": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-4.0.0.tgz", + "integrity": "sha512-lffPI1JrbHDTToJwcq0rl6rBmkjQmMuXkAxsZPRS9DXbaJQvc642eCg6EGxcX2i1L+esbuhq+2l9tBll5v8AeQ==", + "dev": true, + "dependencies": { + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-3.0.0.tgz", + "integrity": "sha512-/qlwqwWBWFOmpXujL/20P+Iuydil0rZZNglR+VNm6J0gpLHwuVM5s7g2TfVoswbXjZ4HuIhLMySEyIw5i7/D8w==", + "dev": true, + "dependencies": { + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walk-up-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.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, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.16.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-node-module-types": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/webpack-node-module-types/-/webpack-node-module-types-1.2.3.tgz", + "integrity": "sha512-YQqxeaW+eKYDpTTausYdMa17WPlaeUDyDNUX4P9KpyQslMDmX56Lsl86ieG2cTyT0ZWpAFxB6+Z5SsLiD0AGyw==", + "engines": { + "node": ">= 12.x" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "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, + "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, + "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/wrapped": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wrapped/-/wrapped-1.0.1.tgz", + "integrity": "sha512-ZTKuqiTu3WXtL72UKCCnQLRax2IScKH7oQ+mvjbpvNE+NJxIWIemDqqM2GxNr4N16NCjOYpIgpin5pStM7kM5g==", + "dev": true, + "dependencies": { + "co": "3.1.0", + "sliced": "^1.0.1" + } + }, + "node_modules/wrapped/node_modules/co": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", + "integrity": "sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==", + "dev": true + }, + "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==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "dev": true, + "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-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "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, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0b44a6 --- /dev/null +++ b/package.json @@ -0,0 +1,226 @@ +{ + "name": "inbdpa.api.hscc.bdpa.org", + "version": "1.1.3", + "private": true, + "description": "https://inbdpa.api.hscc.bdpa.org", + "homepage": "https://inbdpa.api.hscc.bdpa.org", + "repository": { + "type": "git", + "url": "https://github.com/nhscc/inbdpa.api.hscc.bdpa.org", + "lens": "next" + }, + "license": "MIT", + "author": "Xunnamius", + "type": "commonjs", + "scripts": { + "__test:repeat:all": "echo 'Repeating test suite [initializing]...'; (i=0; while [ \"$((( i += 1 ) <= 100 ))\" -ne 0 ]; do sleep 0.1 && echo \"\\r\\033[1A\\033[0KRepeating test suite [run $i/100]...\" && JEST_SILENT_REPORTER_SHOW_WARNINGS=true NODE_ENV=test npx jest --reporters=jest-silent-reporter || exit; done) && echo \"All tests passed! Congrats!\"", + "__test:repeat:unit": "echo 'Repeating test suite [initializing]...'; (i=0; while [ \"$((( i += 1 ) <= 100 ))\" -ne 0 ]; do sleep 0.1 && echo \"\\r\\033[1A\\033[0KRepeating test suite [run $i/100]...\" && JEST_SILENT_REPORTER_SHOW_WARNINGS=true NODE_ENV=test npx jest --reporters=jest-silent-reporter --testPathIgnorePatterns '/(integration|e2e).*?\\.test\\.tsx?' '/dist/' || exit; done) && echo \"All tests passed! Congrats!\"", + "build": "npm run build:dist --", + "build:changelog": "conventional-changelog --outfile CHANGELOG.md --config ./conventional.config.js --release-count 0 --skip-unstable && (if [ \"$CHANGELOG_SKIP_TITLE\" != 'true' ]; then { node -e 'console.log(require(\"./conventional.config.js\").changelogTitle)'; cat CHANGELOG.md; } > CHANGELOG.md.ignore && mv CHANGELOG.md.ignore CHANGELOG.md; fi) && NODE_ENV=format remark --output --frail CHANGELOG.md && prettier --write CHANGELOG.md", + "build:dist": "next build", + "build:docs": "typedoc --plugin typedoc-plugin-markdown --cleanOutputDir --tsconfig tsconfig.docs.json --out docs --entryPointStrategy expand --readme none lib src test types external-scripts --exclude '**/*.test.*' --exclude external-scripts/bin", + "build:externals": "NODE_ENV=external webpack --config-name externals", + "build:stats": "NODE_ENV=production webpack --json > bundle-stats.ignore.json", + "clean": "git ls-files --exclude-standard --ignored --others --directory | grep -vE '^((\\.(env|vscode|husky))|next-env\\.d\\.ts|node_modules)($|\\/)' | xargs -p rm -rf", + "deploy": "npm run deploy:production && npm run deploy:preview && npm --no-git-tag-version version patch", + "deploy:preview": "url=$(vercel deploy) && vercel alias set \"$url\" elections-irv.preview-api.hscc.bdpa.org", + "deploy:production": "vercel deploy --prod", + "dev": "next -p `npx -q acquire-port`", + "format": "MD_FILES=$(node -e 'console.log(require(`glob-gitignore`).sync(`**/*.md`, { ignore: require(`fs`).readFileSync(`.prettierignore`, `utf8`).split(`\n`).filter(Boolean), dot: true }).join(`\n`))') && (echo $MD_FILES | xargs remark --no-config --no-stdout --quiet --frail --use gfm --use lint-no-undefined-references || (echo -n '\u001b' && echo '[37;41;1m FAIL \u001b[0m cannot continue with undefined references present' && false)) && sort-package-json './package.json' './packages/*/package.json' && echo $MD_FILES | NODE_ENV=format xargs remark --output --frail && echo $MD_FILES | xargs doctoc --no-title --maxlevel 3 --update-only && prettier --write .", + "lint": "stdbuf -i0 -o0 -e0 tsc --project tsconfig.lint.json; X=$?; stdbuf -i0 -o0 -e0 eslint --parser-options=project:tsconfig.lint.json --no-error-on-unmatched-pattern packages src; Y=$?; MD_FILES=$(node -e 'console.log(require(`glob-gitignore`).sync(`**/*.md`, { ignore: require(`fs`).readFileSync(`.prettierignore`, `utf8`).split(`\n`).filter(Boolean), dot: true }).join(`\n`))') && echo $MD_FILES | NODE_ENV=lint xargs remark --quiet --frail --no-stdout; Z=$?; [ $X -eq 0 ] && [ $Y -eq 0 ] && [ $Z -eq 0 ]", + "lint:all": "stdbuf -i0 -o0 -e0 tsc --project tsconfig.eslint.json; X=$?; stdbuf -i0 -o0 -e0 eslint --parser-options=project:tsconfig.eslint.json .; Y=$?; MD_FILES=$(node -e 'console.log(require(`glob-gitignore`).sync(`**/*.md`, { ignore: require(`fs`).readFileSync(`.prettierignore`, `utf8`).split(`\n`).filter(Boolean), dot: true }).join(`\n`))') && echo $MD_FILES | NODE_ENV=lint xargs remark --quiet --frail --no-stdout; Z=$?; [ $X -eq 0 ] && [ $Y -eq 0 ] && [ $Z -eq 0 ]", + "list-tasks": "node -e 'console.log(Object.keys(require(\"./package.json\").scripts).join(\"\\n\"))' && (npm run -ws list-tasks --if-present 2>/dev/null || true)", + "prepare": "node -e \"execa = require('execa'); if(process.env.CI === undefined && (process.env.NODE_ENV === undefined || process.env.NODE_ENV === 'development')) { execa.sync('npx', ['husky', 'install'], { stdout: 'inherit', stderr: 'inherit' }); } else { console.log('skipped installing husky git hooks'); }\"", + "start": "next start", + "test": "npm run test:unit --", + "test:all": "NODE_ENV=test jest --coverage", + "test:e2e": "NODE_ENV=test jest 'e2e.*?\\.test\\.tsx?'", + "test:integration": "NODE_ENV=test jest '/integration.*?\\.test\\.tsx?'", + "test:integration:client": "NODE_ENV=test jest '/integration-client.*?\\.test\\.tsx?'", + "test:integration:compile": "NODE_ENV=test jest '/integration-compile.*?\\.test\\.tsx?'", + "test:integration:externals": "NODE_ENV=test jest '/integration-external.*?\\.test\\.tsx?'", + "test:integration:node": "NODE_ENV=test jest '/integration-node.*?\\.test\\.tsx?'", + "test:repeat:all": "npm run __test:repeat:all --silent", + "test:repeat:unit": "npm run __test:repeat:unit --silent", + "test:unit": "NODE_ENV=test jest '/unit-.*\\.test\\.tsx?'" + }, + "config": { + "mongodbMemoryServer": { + "version": "6.0.15" + } + }, + "dependencies": { + "@babel/core": "^7.24.6", + "@babel/plugin-proposal-export-default-from": "^7.24.6", + "@babel/plugin-proposal-function-bind": "^7.24.6", + "@babel/plugin-transform-react-jsx-source": "^7.24.6", + "@babel/preset-env": "^7.24.6", + "@babel/preset-react": "^7.24.6", + "@babel/preset-typescript": "^7.24.6", + "@next/bundle-analyzer": "^14.2.3", + "@octokit/rest": "^20.1.1", + "@types/bytes": "^3.1.4", + "@types/cors": "^2.8.17", + "@types/debug": "^4.1.12", + "@types/folder-hash": "^4.0.4", + "@types/node": "^20.12.12", + "@types/react": "^18.3.3", + "@types/request-ip": "^0.0.41", + "@xunnamius/babel-preset-next-babel": "^1.0.1", + "@xunnamius/next-types": "^1.0.9", + "@xunnamius/types": "^1.3.0", + "babel-plugin-explicit-exports-references": "^1.0.2", + "babel-plugin-transform-default-named-imports": "^1.2.2", + "bytes": "^3.1.2", + "clone-deep": "^4.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "execa": "^5.1.1", + "find-up": "^5.0.0", + "is-plain-object": "^5.0.0", + "is-server-side": "^1.0.2", + "mongodb": "^6.6.2", + "named-app-errors": "^4.0.2", + "next": "^14.2.3", + "node-fetch": "cjs", + "raw-body": "^2.5.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-use": "^17.5.0", + "request-ip": "3.3.0", + "swr": "^2.2.5", + "toss-expression": "^0.1.2", + "typescript": "^5.4.5" + }, + "devDependencies": { + "@babel/cli": "^7.24.6", + "@babel/eslint-parser": "^7.24.6", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@faker-js/faker": "^8.4.1", + "@next/eslint-plugin-next": "^14.2.3", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@types/clone-deep": "^4.0.4", + "@types/confusing-browser-globals": "^1.0.3", + "@types/content-type": "^1.1.8", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.12", + "@types/jsonfile": "^6.1.4", + "@types/node-fetch": "^2.5.12", + "@types/semver": "^7.5.8", + "@types/tar-stream": "^3.1.3", + "@types/test-listen": "^1.1.2", + "@types/webpack": "^5.28.5", + "@typescript-eslint/eslint-plugin": "^7.11.0", + "@typescript-eslint/parser": "^7.11.0", + "@xunnamius/conventional-changelog-projector": "^1.2.1", + "@xunnamius/jest-types": "^1.1.3", + "auto-bind": "^4.0.0", + "babel-jest": "^29.7.0", + "babel-loader": "^9.1.3", + "chalk": "^4.1.2", + "confusing-browser-globals": "^1.0.11", + "conventional-changelog-cli": "^5.0.0", + "del": "^7.1.0", + "doctoc": "^2.2.1", + "dot-prop": "^9.0.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.3", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^28.5.0", + "eslint-plugin-jest-dom": "^5.4.0", + "eslint-plugin-react": "^7.34.2", + "eslint-plugin-unicorn": "^53.0.0", + "glob": "^10.4.1", + "glob-gitignore": "^1.0.14", + "html-entities": "^2.5.2", + "http-terminator": "^3.2.0", + "husky": "^9.0.11", + "inquirer": "^8.2.5", + "jest": "^29.7.0", + "jest-circus": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-extended": "^4.0.2", + "jest-silent-reporter": "^0.6.0", + "jsonfile": "^6.1.0", + "lint-staged": "^15.2.5", + "mongodb-memory-server": "^9.2.0", + "msw": "^2.3.0", + "next-test-api-route-handler": "^4.0.7", + "prettier": "^3.2.5", + "random-case": "^1.0.0", + "raw-body": "^2.5.2", + "remark-capitalize-headings": "^2.0.1", + "remark-cli": "^12.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-ignore": "^2.0.0", + "remark-lint": "^10.0.0", + "remark-lint-definition-case": "^4.0.0", + "remark-lint-fenced-code-flag": "^4.0.0", + "remark-lint-fenced-code-flag-case": "^2.0.0", + "remark-lint-file-extension": "^3.0.0", + "remark-lint-final-newline": "^3.0.0", + "remark-lint-first-heading-level": "^4.0.0", + "remark-lint-hard-break-spaces": "^4.0.0", + "remark-lint-heading-increment": "^4.0.0", + "remark-lint-heading-whitespace": "^1.0.0", + "remark-lint-heading-word-length": "^2.0.0", + "remark-lint-list-item-style": "^2.0.0", + "remark-lint-no-auto-link-without-protocol": "^3.1.2", + "remark-lint-no-blockquote-without-marker": "^6.0.0", + "remark-lint-no-duplicate-defined-urls": "^3.0.0", + "remark-lint-no-duplicate-definitions": "^4.0.0", + "remark-lint-no-duplicate-headings-in-section": "^4.0.0", + "remark-lint-no-empty-sections": "^4.0.0", + "remark-lint-no-empty-url": "https://xunn.at/remark-lint-no-empty-url", + "remark-lint-no-heading-content-indent": "^5.0.0", + "remark-lint-no-heading-like-paragraph": "^4.0.0", + "remark-lint-no-heading-punctuation": "^4.0.0", + "remark-lint-no-inline-padding": "^4.1.2", + "remark-lint-no-literal-urls": "^4.0.0", + "remark-lint-no-multiple-toplevel-headings": "^4.0.0", + "remark-lint-no-reference-like-url": "^4.0.0", + "remark-lint-no-shell-dollars": "^4.0.0", + "remark-lint-no-shortcut-reference-image": "^4.0.0", + "remark-lint-no-shortcut-reference-link": "^4.0.0", + "remark-lint-no-tabs": "^4.0.0", + "remark-lint-no-undefined-references": "^5.0.0", + "remark-lint-no-unused-definitions": "^4.0.0", + "remark-lint-ordered-list-marker-style": "^4.0.0", + "remark-lint-ordered-list-marker-value": "^4.0.0", + "remark-lint-strikethrough-marker": "^3.0.0", + "remark-lint-unordered-list-marker-style": "^4.0.0", + "remark-reference-links": "^7.0.0", + "remark-remove-unused-definitions": "^2.0.0", + "remark-remove-url-trailing-slash": "^2.0.0", + "remark-renumber-references": "^2.0.0", + "remark-sort-definitions": "^2.0.0", + "remark-tight-comments": "^2.0.0", + "remark-validate-links": "^13.0.1", + "semantic-release": "https://xunn.at/semantic-release-atam@22.0.7", + "simple-git": "^3.24.0", + "sort-package-json": "https://xunn.at/sort-package-json", + "spellchecker": "^3.7.1", + "text-extensions": "^2.4.0", + "tsconfig-replace-paths": "^0.0.14", + "type-fest": "^4.18.3", + "typedoc": "^0.25.13", + "typedoc-plugin-markdown": "^4.0.3", + "unfetch": "^4.2.0", + "unique-filename": "^3.0.0", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..ba7ab22 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + endOfLine: 'lf', + printWidth: 80, + proseWrap: 'always', + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'none', + overrides: [ + { + files: '**/*.@(ts|?(@(c|m))js)?(x)', + options: { + parser: 'babel-ts', + printWidth: 86 + } + } + ] +}; diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..7b06067 --- /dev/null +++ b/release.config.js @@ -0,0 +1,99 @@ +'use strict'; + +const debug = require('debug')( + `${require('./package.json').name}:semantic-release-config` +); + +// TODO: turn this into @xunnamius/semantic-release-projector-config + +const updateChangelog = + process.env.UPDATE_CHANGELOG === 'true' || + // ? Legacy + process.env.SHOULD_UPDATE_CHANGELOG === 'true'; + +debug(`will update changelog: ${updateChangelog ? 'yes' : 'no'}`); + +const { + changelogTitle, + parserOpts, + writerOpts +} = require('./conventional.config.js'); + +module.exports = { + branches: [ + '+([0-9])?(.{+([0-9]),x}).x', + 'main', + { + name: 'canary', + channel: 'canary', + prerelease: true + } + ], + plugins: [ + [ + '@semantic-release/commit-analyzer', + { + parserOpts, + releaseRules: [ + // ? releaseRules are checked first; if none match, defaults are + // ? checked next. + + // ! These two lines must always appear first and in order: + { breaking: true, release: 'major' }, + { revert: true, release: 'patch' }, + + // * Custom release rules, if any, may appear next: + { type: 'build', release: 'patch' } + ] + } + ], + [ + '@semantic-release/release-notes-generator', + { + parserOpts, + writerOpts + } + ], + ...(updateChangelog + ? [ + [ + '@semantic-release/exec', + { + prepareCmd: 'CHANGELOG_SKIP_TITLE=true npm run build-changelog' + } + ], + ['@semantic-release/changelog', { changelogTitle }], + [ + '@semantic-release/exec', + { + prepareCmd: + 'remark -o --use reference-links --use gfm --use frontmatter CHANGELOG.md' + } + ], + [ + '@semantic-release/exec', + { + prepareCmd: 'npx prettier --write CHANGELOG.md' + } + ] + ] + : []), + ['@semantic-release/npm'], + [ + '@semantic-release/git', + { + assets: [ + 'package.json', + 'package-lock.json', + 'npm-shrinkwrap.json', + 'CHANGELOG.md', + 'docs' + ], + message: 'release: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' + } + ], + ['@semantic-release/github'] + ] +}; + +debug('exports: %O', module.exports); diff --git a/spellcheck-commit.js b/spellcheck-commit.js new file mode 100644 index 0000000..438cc51 --- /dev/null +++ b/spellcheck-commit.js @@ -0,0 +1,116 @@ +'use strict'; +/* eslint-disable no-console */ + +const spellcheck = require('spellchecker'); +const pkg = require('./package.json'); +const read = require('node:fs').promises.readFile; +const execa = require('execa'); + +const debug = require('debug')(`${require('./package.json').name}:spellcheck-commit`); + +const tryToRead = async (path) => { + try { + debug(`attempting to read ${path}`); + const data = await read(path); + debug(`successfully read ${path}`); + return data; + } catch { + debug(`failed to read ${path}`); + return ''; + } +}; + +const asJson = (str) => { + try { + const json = JSON.parse(str.toString('utf8')); + debug('json parse successful!'); + return [ + ...(json?.['cSpell.words'] || []), + ...(json?.['cSpell.userWords'] || []), + ...(json?.['cSpell.ignoreWords'] || []) + ]; + } catch { + debug('json parse failed!'); + return []; + } +}; + +const asText = (str) => str.toString('utf8').split('\n'); +const isPascalCase = (w) => /^([A-Z]{2,}.+|[A-Z][a-z]+[A-Z].*)$/.test(w); +const isCamelCase = (w) => /^[a-z]+[A-Z]+.*$/.test(w); +const isAllCaps = (w) => /^[^a-z]+$/.test(w); + +const splitOutWords = (phrase) => + [...phrase.split(/[^A-Za-z]+/g), phrase].filter(Boolean); + +const keys = (obj) => (obj ? Object.keys(obj).map((key) => splitOutWords(key)) : []); + +(async () => { + const lastCommitMessage = (await read('./.git/COMMIT_EDITMSG')).toString('utf8'); + const homeDir = require('node:os').homedir(); + + debug(`lastCommitMsg: ${lastCommitMessage}`); + debug(`homeDir: ${homeDir}`); + + const ignoreWords = new Set( + [ + ...(await Promise.all([ + tryToRead('./.spellcheckignore').then(asText), + tryToRead(`${homeDir}/.config/_spellcheckignore`).then(asText), + tryToRead('./.vscode/settings.json').then(asJson), + tryToRead(`${homeDir}/.config/Code/User/settings.json`).then(asJson) + ])), + ...require('text-extensions'), + // ? Popular contractions + // eslint-disable-next-line unicorn/no-useless-spread + ...['ve', 're', 's', 'll', 't', 'd', 'o', 'ol'], + ...keys(pkg.dependencies), + ...keys(pkg.devDependencies), + ...keys(pkg.scripts), + ...splitOutWords( + (await execa('git', ['log', '--format="%B"', 'HEAD~1'])).stdout + ).slice(0, -1) + ] + .flat() + .filter(Boolean) + .flatMap((word) => splitOutWords(word.trim().toLowerCase())) + ); + + const typos = Array.from( + new Set( + spellcheck + .checkSpelling(lastCommitMessage) + .flatMap((typoLocation) => + lastCommitMessage + .slice(typoLocation.start, typoLocation.end) + .trim() + .split("'") + ) + .filter((w) => !isAllCaps(w) && !isCamelCase(w) && !isPascalCase(w)) + .map((w) => w.toLowerCase()) + .filter((typo) => !ignoreWords.has(typo)) + ) + ); + + debug('typos: %O', typos); + + if (typos.length) { + console.warn('WARNING: there may be misspelled words in your commit message!'); + console.warn( + 'Commit messages can be fixed before push with `git commit -S --amend`' + ); + console.warn('---'); + + for (const typo of typos.slice(0, 5)) { + const corrections = spellcheck.getCorrectionsForMisspelling(typo); + const suggestion = corrections.length + ? ` (did you mean ${corrections.slice(0, 5).join(', ')}?)` + : ''; + + console.warn(`${typo}${suggestion}`); + } + + typos.length > 5 && console.warn(`${typos.length - 5} more...`); + typos.length && console.warn('---'); + } +})(); diff --git a/src/backend/api.ts b/src/backend/api.ts new file mode 100644 index 0000000..f32779a --- /dev/null +++ b/src/backend/api.ts @@ -0,0 +1,44 @@ +import { getEnv } from 'universe/backend/env'; + +import type { PageConfig } from 'next'; +import type { IncomingHttpHeaders } from 'node:http'; + +// ! Since this is imported via jsdom for some tests, beware imports with +// ! respect to https://github.com/jsdom/jsdom/issues/2524. + +/** + * The default app-wide Next.js API configuration object. + * + * @see https://nextjs.org/docs/api-routes/api-middlewares#custom-config + */ +export const defaultConfig: PageConfig = { + api: { + bodyParser: { + get sizeLimit() { + return getEnv().MAX_CONTENT_LENGTH_BYTES; + } + } + } +}; + +/** + * Returns the owner token attribute cross-referenced by the + * `authorizationHeader`. + */ +export async function authorizationHeaderToOwnerAttribute( + authorizationHeader: Required +) { + // ? Ensure these are not imported unless and until they're needed thanks to: + // ? https://github.com/jsdom/jsdom/issues/2524 + const { getTokenByDerivation } = await import('multiverse/next-auth'); + + try { + const { + attributes: { owner } + } = await getTokenByDerivation({ from: authorizationHeader }); + + return owner; + } catch { + return ''; + } +} diff --git a/src/backend/db.ts b/src/backend/db.ts new file mode 100644 index 0000000..19e2e7f --- /dev/null +++ b/src/backend/db.ts @@ -0,0 +1,234 @@ +import { getCommonSchemaConfig } from 'multiverse/mongo-common'; +import { getDb, type DbSchema } from 'multiverse/mongo-schema'; +import { getEnv } from 'universe/backend/env'; + +import type { Document, ObjectId, WithId } from 'mongodb'; +import type { TokenAttributes } from 'multiverse/next-auth'; + +/** + * A JSON representation of the backend Mongo database structure. This is used + * for consistent app-wide db access across projects and to generate transient + * versions of the db during testing. + */ +export function getSchemaConfig(): DbSchema { + return getCommonSchemaConfig({ + databases: { + 'hscc-api-elections_irv': { + collections: [ + { + name: 'elections', + indices: [ + { spec: '__provenance' }, + { spec: 'deleted' }, + { spec: 'opensAt' }, + { spec: 'closesAt' } + ] + }, + { + name: 'ballots', + indices: [ + { spec: 'election_id' }, + // ! Keys must be in this order when searching to take advantage + // ! of this index + { spec: ['voter_id', 'election_id'], options: { unique: true } } + ] + } + ] + } + }, + aliases: { + app: 'hscc-api-elections_irv' + } + }); +} + +/** + * Return the well-known "elections" collection after calling {@link getDb} on the + * `'app'` database. + */ +export async function getElectionsDb() { + return (await getDb({ name: 'app' })).collection('elections'); +} + +/** + * Return the well-known "ballots" collection after calling {@link getDb} on the + * `'app'` database. + */ +export async function getBallotsDb() { + return (await getDb({ name: 'app' })).collection('ballots'); +} + +export type TokenAttributeOwner = TokenAttributes['owner']; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ElectionId extends ObjectId {} +export type VoterId = string; + +/** + * Represents an object that tracks provenance. + */ +export type WithProvenance = { + __provenance: TokenAttributeOwner; +} & T; + +/** + * The shape of an internal application election. + */ +export type InternalElection = WithProvenance< + WithId<{ + title: string; + description: string; + options: string[]; + createdAt: number; + opensAt: number; + closesAt: number; + deleted: boolean; + }> +>; + +/** + * The shape of a public application election. + */ +export type PublicElection = Pick< + InternalElection, + | 'title' + | 'description' + | 'options' + | 'createdAt' + | 'opensAt' + | 'closesAt' + | 'deleted' +> & { + owned: boolean; + election_id: string; +}; + +/** + * The shape of a new application election. + */ +export type NewElection = Pick< + InternalElection, + 'title' | 'description' | 'options' | 'opensAt' | 'closesAt' +>; + +/** + * The shape of a patch application election. + */ +export interface PatchElection extends Partial {} + +/** + * The shape of an internal application election. + */ +export type InternalBallot = WithProvenance< + WithId<{ + election_id: ElectionId; + voter_id: VoterId; + ranking: Record; + }> +>; + +/** + * The shape of a public application ballot. + */ +export type PublicBallot = Pick; + +/** + * The shape of a new or patch application ballot. + */ +export type NewOrPatchBallot = Pick; + +/** + * The shape of public info. + */ +export type PublicInfo = { + upcomingElections: number; + openElections: number; + closedElections: number; +}; + +/** + * Transforms an internal election into a public election. + */ +export function toPublicElection( + internalElection: InternalElection, + { owned }: { owned: boolean } +): PublicElection { + return { + election_id: internalElection._id.toString(), + title: internalElection.title, + description: internalElection.description, + options: internalElection.options, + createdAt: internalElection.createdAt, + opensAt: internalElection.opensAt, + closesAt: internalElection.closesAt, + owned, + deleted: internalElection.deleted + }; +} + +/** + * Transforms an internal ballot into a public ballot. + */ +export function toPublicBallot(internalBallot: InternalBallot): PublicBallot { + return { + voter_id: internalBallot.voter_id.toString(), + ranking: internalBallot.ranking + }; +} + +/** + * Transforms the internal info data into a publicly consumable info. + */ +export function toPublicInfo({ + closedElections, + openElections, + upcomingElections +}: PublicInfo): PublicInfo { + return { + upcomingElections, + openElections, + closedElections + }; +} + +/** + * A MongoDB cursor projection that transforms an internal election into a public + * election. + */ +export const incompletePublicElectionProjection = { + _id: false, + election_id: { $toString: '$_id' }, + title: true, + description: true, + options: true, + createdAt: true, + opensAt: true, + closesAt: true, + deleted: true +} as const; + +/** + * Returns a MongoDB aggregation pipeline that transforms internal elections + * into public elections, each including the `owned` property. Prepend a `$match` or + * similar stage to return only a subset of elections. + */ +export function publicElectionAggregation( + tokenAttributeOwner: TokenAttributeOwner +): Document[] { + return [ + // ? LIFO + { $sort: { _id: -1 } }, + { $limit: getEnv().RESULTS_PER_PAGE }, + { $set: { owned: { $eq: ['$__provenance', tokenAttributeOwner] } } }, + { $project: { ...incompletePublicElectionProjection, owned: true } } + ]; +} + +/** + * A MongoDB cursor projection that transforms an internal ballot into a public + * ballot. + */ +export const publicBallotProjection = { + _id: false, + voter_id: true, + ranking: true +} as const; diff --git a/src/backend/env.ts b/src/backend/env.ts new file mode 100644 index 0000000..35c902a --- /dev/null +++ b/src/backend/env.ts @@ -0,0 +1,78 @@ +import { parse as parseAsBytes } from 'bytes'; +import { getEnv as getDefaultEnv } from 'multiverse/next-env'; +import { InvalidAppEnvironmentError } from 'universe/error'; + +import type { Environment } from 'multiverse/next-env'; + +// TODO: replace validation logic with zod instead (including defaults) and +// TODO: integrate that logic with expect-env (also zod-based) + +// TODO: fix the badness that happens when trying to reference a non-existent +// TODO: key from getEnv() (right now it says something like Primitive | +// TODO: Primitive[] when it should be never) + +/** + * Returns an object representing the application's runtime environment. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function getEnv() { + const env = getDefaultEnv({ + MAX_PARAMS_PER_REQUEST: Number(process.env.MAX_PARAMS_PER_REQUEST) || 100, + MIN_ELECTION_TITLE_LENGTH: Number(process.env.MIN_ELECTION_TITLE_LENGTH) || 4, + MAX_ELECTION_TITLE_LENGTH: Number(process.env.MAX_ELECTION_TITLE_LENGTH) || 144, + MAX_ELECTION_DESC_LENGTH: Number(process.env.MAX_ELECTION_DESC_LENGTH) || 200, + MAX_ELECTION_OPTIONS_ITEMS: Number(process.env.MAX_ELECTION_OPTIONS_ITEMS) || 50, + MAX_ELECTION_OPTION_LENGTH: Number(process.env.MAX_ELECTION_OPTION_LENGTH) || 60, + MAX_VOTERID_LENGTH: Number(process.env.MAX_VOTERID_LENGTH) || 24, + MAX_BALLOTS_PER_ELECTION: Number(process.env.MAX_BALLOTS_PER_ELECTION) || 100, + + PRUNE_DATA_MAX_ELECTIONS_BYTES: + parseAsBytes(process.env.PRUNE_DATA_MAX_ELECTIONS_BYTES ?? '-Infinity') || null, + PRUNE_DATA_MAX_BALLOTS_BYTES: + parseAsBytes(process.env.PRUNE_DATA_MAX_BALLOTS_BYTES ?? '-Infinity') || null + }); + + /* istanbul ignore next */ + if ( + (env.NODE_ENV !== 'test' && env.OVERRIDE_EXPECT_ENV !== 'force-no-check') || + env.OVERRIDE_EXPECT_ENV === 'force-check' + ) { + const errors: string[] = []; + + ( + [ + 'MAX_PARAMS_PER_REQUEST', + 'MAX_BALLOTS_PER_ELECTION', + 'MIN_ELECTION_TITLE_LENGTH', + 'MAX_ELECTION_TITLE_LENGTH', + 'MAX_ELECTION_DESC_LENGTH', + 'MAX_ELECTION_OPTIONS_ITEMS', + 'MAX_ELECTION_OPTION_LENGTH', + 'MAX_VOTERID_LENGTH' + ] as (keyof typeof env)[] + ).forEach((name) => { + const value = env[name]; + if (!value || (Number.isSafeInteger(value) && (value as number) <= 0)) { + errors.push( + `bad ${name}, saw "${env[name]}" (expected a non-negative number)` + ); + } + }); + + if (env.MIN_ELECTION_TITLE_LENGTH >= env.MAX_ELECTION_TITLE_LENGTH) { + errors.push( + 'MIN_ELECTION_TITLE_LENGTH must be strictly less than MAX_ELECTION_TITLE_LENGTH' + ); + } + + // TODO: make it easier to reuse error code from getDefaultEnv. Or is it + // TODO: obsoleted by expect-env package? Either way, factor this logic out! + if (errors.length) { + throw new InvalidAppEnvironmentError( + `bad variables:\n - ${errors.join('\n - ')}` + ); + } + } + + return env as typeof env & T; +} diff --git a/src/backend/index.ts b/src/backend/index.ts new file mode 100644 index 0000000..7b71413 --- /dev/null +++ b/src/backend/index.ts @@ -0,0 +1,475 @@ +import { MongoServerError, ObjectId } from 'mongodb'; +import assert from 'node:assert'; + +import { getEnv } from 'universe/backend/env'; + +import { + AppValidationError, + ErrorMessage, + ItemNotFoundError, + NotAuthorizedError, + ValidationError +} from 'universe/error'; + +import { + getBallotsDb, + getElectionsDb, + publicBallotProjection, + publicElectionAggregation, + toPublicElection, + toPublicInfo, + type ElectionId, + type InternalBallot, + type InternalElection, + type PublicBallot, + type PublicElection, + type PublicInfo, + type TokenAttributeOwner +} from 'universe/backend/db'; + +import { + validateElectionInvariants, + validateNewElectionData, + validateNewOrPatchBallotData, + validatePatchElectionData, + validateVoterId +} from 'universe/backend/validators'; + +import { itemExists, itemToObjectId } from 'multiverse/mongo-item'; + +export async function getAllElections({ + after_id, + provenance +}: { + after_id: string | undefined; + provenance: TokenAttributeOwner; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + const electionsDb = await getElectionsDb(); + const afterId = after_id ? itemToObjectId(after_id) : undefined; + + if (afterId && !(await itemExists(electionsDb, afterId))) { + throw new ItemNotFoundError(after_id, 'after_id'); + } + + return electionsDb + .aggregate([ + { + $match: { + ...(afterId + ? { + _id: { + // ? LIFO + $lt: afterId + } + } + : {}) + } + }, + ...publicElectionAggregation(provenance) + ]) + .toArray(); +} + +export async function getAllBallotsForElection({ + election_id +}: { + election_id: string | undefined; +}): Promise { + const electionsDb = await getElectionsDb(); + const ballotsDb = await getBallotsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + return ballotsDb + .find( + { election_id: electionId }, + { + projection: publicBallotProjection, + limit: getEnv().RESULTS_PER_PAGE, + sort: { _id: 1 } + } + ) + .toArray(); +} + +export async function getElection({ + election_id, + provenance +}: { + election_id: string | undefined; + provenance: TokenAttributeOwner; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + const electionsDb = await getElectionsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + const election = await electionsDb + .aggregate([ + { $match: { _id: electionId } }, + ...publicElectionAggregation(provenance) + ]) + .next(); + + if (!election) { + throw new ItemNotFoundError(election_id, 'election'); + } + + return election; +} + +export async function getBallotForElection({ + election_id, + voter_id +}: { + election_id: string | undefined; + voter_id: string | undefined; +}): Promise { + validateVoterId(voter_id); + + const electionsDb = await getElectionsDb(); + const ballotsDb = await getBallotsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + const ballot = await ballotsDb.findOne( + { election_id: electionId, voter_id }, + { projection: publicBallotProjection } + ); + + if (!ballot) { + throw new ItemNotFoundError(voter_id, 'ballot'); + } + + return ballot; +} + +export async function getInfo(): Promise { + const electionsDb = await getElectionsDb(); + + const [closedElections, openElections, upcomingElections] = await Promise.all([ + electionsDb.countDocuments({ + deleted: false, + closesAt: { $lte: Date.now() } + }), + electionsDb.countDocuments({ + deleted: false, + closesAt: { $gt: Date.now() }, + opensAt: { $lte: Date.now() } + }), + electionsDb.countDocuments({ + deleted: false, + opensAt: { $gt: Date.now() } + }) + ]); + + return toPublicInfo({ closedElections, openElections, upcomingElections }); +} + +export async function createElection({ + data, + provenance +}: { + data: unknown; + provenance: TokenAttributeOwner; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + validateNewElectionData(data); + await validateElectionInvariants(data); + + const createdAt = Date.now(); + const electionsDb = await getElectionsDb(); + const { title, description, options, opensAt, closesAt } = data; + + const newElection: InternalElection = { + _id: new ObjectId(), + __provenance: provenance, + title, + description, + options, + createdAt, + opensAt, + closesAt, + deleted: false + }; + + await electionsDb.insertOne(newElection); + return toPublicElection(newElection, { owned: true }); +} + +export async function upsertBallot({ + election_id, + voter_id, + data, + provenance +}: { + election_id: string | undefined; + voter_id: string | undefined; + provenance: TokenAttributeOwner; + data: unknown; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + validateVoterId(voter_id); + validateNewOrPatchBallotData(data); + + const electionsDb = await getElectionsDb(); + const ballotsDb = await getBallotsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + if (!(await isProvenant(electionId, provenance))) { + throw new NotAuthorizedError(); + } + + const { ranking } = data; + + // ? Try an update first + const { matchedCount } = await ballotsDb.updateOne( + { voter_id, election_id: electionId }, + { $set: { ranking } } + ); + + /* istanbul ignore else */ + if (matchedCount === 0) { + // ? Fall back to an insert + const { MAX_BALLOTS_PER_ELECTION } = getEnv(); + const existingBallotCount = await ballotsDb.countDocuments({ + election_id: electionId + }); + + if (existingBallotCount >= MAX_BALLOTS_PER_ELECTION) { + throw new ValidationError( + ErrorMessage.TooMany('ballots', MAX_BALLOTS_PER_ELECTION) + ); + } + + const newBallot: InternalBallot = { + _id: new ObjectId(), + __provenance: provenance, + voter_id, + election_id: electionId, + ranking + }; + + try { + await ballotsDb.insertOne(newBallot); + } catch (error) { + if (error instanceof MongoServerError && error.code === 11_000) { + throw new ValidationError(ErrorMessage.DuplicateFieldValue('voter_id')); + } + + throw error; + } + } + + assert( + matchedCount === 0 || matchedCount === 1, + `expected 0 or 1 matched documents, ${matchedCount} were matched` + ); +} + +export async function updateElection({ + election_id, + data, + provenance +}: { + election_id: string | undefined; + provenance: TokenAttributeOwner; + data: unknown; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + validatePatchElectionData(data); + + const electionsDb = await getElectionsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + if (!(await isProvenant(electionId, provenance))) { + throw new NotAuthorizedError(); + } + + await validateElectionInvariants({ ...data, _id: electionId }); + + const { matchedCount } = await electionsDb.updateOne( + { _id: electionId }, + { + $set: Object.fromEntries( + Object.entries(data).filter(([, value]) => value !== undefined) + ) + } + ); + + if (matchedCount !== 1) { + throw new ItemNotFoundError(election_id, 'election'); + } +} + +export async function deleteElection({ + election_id, + provenance +}: { + election_id: string | undefined; + provenance: TokenAttributeOwner; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + const electionsDb = await getElectionsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + if (!(await isProvenant(electionId, provenance))) { + throw new NotAuthorizedError(); + } + + const { modifiedCount } = await electionsDb.updateOne( + { _id: electionId }, + { $set: { deleted: true } } + ); + + assert( + modifiedCount === 1, + `expected 1 modified document, ${modifiedCount} were modified` + ); +} + +export async function superDeleteElectionAndRelatedBallots({ + election_id +}: { + election_id: string | undefined; +}): Promise { + const electionsDb = await getElectionsDb(); + const ballotsDb = await getElectionsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + const { deletedCount } = await electionsDb.deleteOne({ _id: electionId }); + + if (deletedCount === 0) { + throw new ItemNotFoundError(election_id, 'election'); + } else { + assert( + deletedCount === 1, + `expected 1 deleted document, ${deletedCount} were deleted` + ); + } + + await ballotsDb.deleteMany({ election_id: electionId }); +} + +export async function deleteBallotFromElection({ + election_id, + voter_id, + provenance +}: { + election_id: string | undefined; + voter_id: string | undefined; + provenance: TokenAttributeOwner; +}): Promise { + if (typeof provenance !== 'string') { + throw new AppValidationError(ErrorMessage.BadProvenanceToken()); + } + + validateVoterId(voter_id); + + const electionsDb = await getElectionsDb(); + const ballotsDb = await getBallotsDb(); + + const electionId = election_id + ? itemToObjectId(election_id) + : undefined; + + if (!electionId || !(await itemExists(electionsDb, electionId))) { + throw new ItemNotFoundError(election_id, 'election'); + } + + if (!(await isProvenant(electionId, provenance))) { + throw new NotAuthorizedError(); + } + + const { deletedCount } = await ballotsDb.deleteOne({ + voter_id, + election_id: electionId + }); + + if (deletedCount === 0) { + throw new ItemNotFoundError(voter_id, 'ballot'); + } else { + assert( + deletedCount === 1, + `expected 1 deleted document, ${deletedCount} were deleted` + ); + } +} + +/** + * Returns `true` if the given `provenance` matches the election's + * `__provenance` property, which implies that the current user is authorized to + * mutate the associated resource. + */ +async function isProvenant(electionId: ElectionId, provenance: TokenAttributeOwner) { + const electionsDb = await getElectionsDb(); + return !!(await electionsDb.findOne({ + _id: electionId, + __provenance: provenance + })); +} diff --git a/src/backend/middleware.ts b/src/backend/middleware.ts new file mode 100644 index 0000000..6665f62 --- /dev/null +++ b/src/backend/middleware.ts @@ -0,0 +1,115 @@ +import { middlewareFactory } from 'multiverse/next-api-glue'; +import type { Simplify } from 'type-fest'; + +import logRequest, { + type Options as LogRequestOptions +} from 'multiverse/next-adhesive/log-request'; + +import checkVersion, { + type Options as CheckVersionOptions +} from 'multiverse/next-adhesive/check-version'; + +import useCors, { + type Options as UseCorsOptions +} from 'multiverse/next-adhesive/use-cors'; + +import authRequest, { + type Options as AuthRequestOptions +} from 'multiverse/next-adhesive/auth-request'; + +import limitRequest, { + type Options as LimitRequestOptions +} from 'multiverse/next-adhesive/limit-request'; + +import checkMethod, { + type Options as CheckMethodOptions +} from 'multiverse/next-adhesive/check-method'; + +import checkContentType, { + type Options as CheckContentTypeOptions +} from 'multiverse/next-adhesive/check-content-type'; + +import handleError, { + type Options as HandleErrorOptions +} from 'multiverse/next-adhesive/handle-error'; + +import contriveError, { + type Options as ContriveErrorOptions +} from 'multiverse/next-adhesive/contrive-error'; + +type ExposedOptions = LogRequestOptions & + CheckVersionOptions & + CheckMethodOptions & + CheckContentTypeOptions; + +/** + * The shape of an API endpoint metadata object. + * + * This export is heavily relied upon by most of the testing infrastructure and + * should be exported alongside `defaultEndpointConfig`/`config` in every + * Next.js API handler file. + */ +export type EndpointMetadata = Simplify; + +/** + * Primary middleware runner for the REST API. Decorates a request handler. + * + * Passing `undefined` as `handler` or not calling `res.end()` (and not sending + * headers) in your handler or use chain will trigger an `HTTP 501 Not + * Implemented` response. This can be used to to stub out endpoints and their + * middleware for later implementation. + */ +/* istanbul ignore next */ +const withMiddleware = middlewareFactory< + ExposedOptions & + UseCorsOptions & + AuthRequestOptions & + LimitRequestOptions & + HandleErrorOptions & + ContriveErrorOptions +>({ + use: [ + logRequest, + checkVersion, + useCors, + authRequest, + limitRequest, + checkMethod, + checkContentType, + contriveError + ], + useOnError: [handleError], + options: { + allowedContentTypes: ['application/json'], + requiresAuth: true, + enableContrivedErrors: true + } +}); + +/** + * Middleware runner for the special /sys API endpoints. Decorates a request + * handler. + * + * Passing `undefined` as `handler` or not calling `res.end()` (and not sending + * headers) in your handler or use chain will trigger an `HTTP 501 Not + * Implemented` response. This can be used to to stub out endpoints and their + * middleware for later implementation. + */ +/* istanbul ignore next */ +const withSysMiddleware = middlewareFactory< + LogRequestOptions & + AuthRequestOptions & + LimitRequestOptions & + CheckMethodOptions & + CheckContentTypeOptions & + HandleErrorOptions +>({ + use: [logRequest, authRequest, limitRequest, checkMethod, checkContentType], + useOnError: [handleError], + options: { + allowedContentTypes: ['application/json'], + requiresAuth: { constraints: 'isGlobalAdmin' } + } +}); + +export { withMiddleware, withSysMiddleware }; diff --git a/src/backend/validators.ts b/src/backend/validators.ts new file mode 100644 index 0000000..e2b0cc8 --- /dev/null +++ b/src/backend/validators.ts @@ -0,0 +1,262 @@ +import { isPlainObject } from 'multiverse/is-plain-object'; +import { getEnv } from 'universe/backend/env'; +import { ErrorMessage, ValidationError } from 'universe/error'; + +import { + getElectionsDb, + type InternalElection, + type NewElection, + type NewOrPatchBallot, + type PatchElection, + type VoterId +} from 'universe/backend/db'; + +export async function validateElectionInvariants({ + opensAt, + closesAt, + _id, + options +}: Partial>) { + const { opensAt: opensAt_, closesAt: closesAt_ } = + (_id && (await (await getElectionsDb()).findOne({ _id }))) || {}; + + opensAt ??= opensAt_; + closesAt ??= closesAt_; + + if (opensAt! >= closesAt!) { + throw new ValidationError(ErrorMessage.InvariantViolation('opensAt < closesAt')); + } + + if (Array.isArray(options) && new Set(options).size !== options.length) { + throw new ValidationError( + ErrorMessage.InvariantViolation('options must be unique') + ); + } +} + +/** + * Assert that `data` is of type {@link NewElection}. + */ +export function validateNewElectionData(data: unknown): asserts data is NewElection { + validateGenericElectionData(data, { isPatchData: false }); +} + +/** + * Assert that `data` is of type {@link PatchElection}. + */ +export function validatePatchElectionData( + data: unknown +): asserts data is PatchElection { + validateGenericElectionData(data, { isPatchData: true }); +} + +/** + * Assert that `data` is of type {@link NewOrPatchBallot}. + */ +export function validateNewOrPatchBallotData( + data: unknown +): asserts data is NewOrPatchBallot { + validateGenericBallotData(data); +} + +export function validateVoterId(data: unknown): asserts data is VoterId { + const { MAX_VOTERID_LENGTH } = getEnv(); + + if ( + typeof data !== 'string' || + data.length < 1 || + data.length > MAX_VOTERID_LENGTH + ) { + throw new ValidationError( + ErrorMessage.InvalidStringLength('voter_id', 1, MAX_VOTERID_LENGTH, 'string') + ); + } +} + +// ** ** +// ** Generic validator functions ** +// ** ** + +function validateGenericElectionData( + data: unknown, + { isPatchData }: { isPatchData: false } +): asserts data is NewElection; +function validateGenericElectionData( + data: unknown, + { isPatchData }: { isPatchData: true } +): asserts data is PatchElection; +function validateGenericElectionData( + data: unknown, + { isPatchData }: { isPatchData: boolean } +): void { + if (!isPlainObject(data)) { + throw new ValidationError(ErrorMessage.InvalidJSON()); + } + + if (Object.keys(data).length === 0) { + throw new ValidationError(ErrorMessage.EmptyJSONBody()); + } + + const { + MIN_ELECTION_TITLE_LENGTH, + MAX_ELECTION_TITLE_LENGTH, + MAX_ELECTION_DESC_LENGTH, + MAX_ELECTION_OPTIONS_ITEMS, + MAX_ELECTION_OPTION_LENGTH + } = getEnv(); + + if ( + (!isPatchData || (isPatchData && data.title !== undefined)) && + (typeof data.title !== 'string' || + data.title.length < MIN_ELECTION_TITLE_LENGTH || + data.title.length > MAX_ELECTION_TITLE_LENGTH) + ) { + throw new ValidationError( + ErrorMessage.InvalidStringLength( + 'title', + MIN_ELECTION_TITLE_LENGTH, + MAX_ELECTION_TITLE_LENGTH, + 'string' + ) + ); + } + + if ( + (!isPatchData || (isPatchData && data.description !== undefined)) && + (typeof data.description !== 'string' || + data.description.length > MAX_ELECTION_DESC_LENGTH) + ) { + throw new ValidationError( + ErrorMessage.InvalidStringLength( + 'description', + 0, + MAX_ELECTION_DESC_LENGTH, + 'string' + ) + ); + } + + if ( + (!isPatchData || (isPatchData && data.options !== undefined)) && + !Array.isArray(data.options) + ) { + throw new ValidationError(ErrorMessage.InvalidFieldValue('options')); + } + + if (Array.isArray(data.options)) { + if (data.options.length > MAX_ELECTION_OPTIONS_ITEMS) { + throw new ValidationError( + ErrorMessage.TooMany('options', MAX_ELECTION_OPTIONS_ITEMS) + ); + } + + data.options.forEach((option: NewElection['options'][number], index) => { + if (option.length < 1 || option.length > MAX_ELECTION_OPTION_LENGTH) { + throw new ValidationError( + ErrorMessage.InvalidArrayValue('options', option, index, [ + `strings of length between 1 and ${MAX_ELECTION_OPTION_LENGTH}` + ]) + ); + } + }); + } + + if ( + (!isPatchData || (isPatchData && data.opensAt !== undefined)) && + (typeof data.opensAt !== 'number' || + !Number.isInteger(data.opensAt) || + data.opensAt < 0) + ) { + throw new ValidationError( + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ); + } + + if ( + (!isPatchData || (isPatchData && data.closesAt !== undefined)) && + (typeof data.closesAt !== 'number' || + !Number.isInteger(data.closesAt) || + data.closesAt < 0) + ) { + throw new ValidationError( + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { + title: _1, + description: _2, + options: _3, + opensAt: _4, + closesAt: _5, + ...rest + } = data; + + const restKeys = Object.keys(rest); + + if (restKeys.length !== 0) { + throw new ValidationError(ErrorMessage.UnknownField(restKeys[0])); + } +} + +function validateGenericBallotData(data: unknown): asserts data is NewOrPatchBallot { + if (!isPlainObject(data)) { + throw new ValidationError(ErrorMessage.InvalidJSON()); + } + + if (Object.keys(data).length === 0) { + throw new ValidationError(ErrorMessage.EmptyJSONBody()); + } + + const { MAX_ELECTION_OPTIONS_ITEMS, MAX_ELECTION_OPTION_LENGTH } = getEnv(); + + if (!isPlainObject(data.ranking)) { + throw new ValidationError(ErrorMessage.InvalidJSON('ranking')); + } + + const ranking = Object.entries(data.ranking); + const cardinality = ranking.length; + + if (cardinality < 1 || cardinality > MAX_ELECTION_OPTIONS_ITEMS) { + throw new ValidationError( + ErrorMessage.InvalidLength( + 'ranking', + cardinality, + 1, + MAX_ELECTION_OPTIONS_ITEMS + ) + ); + } + + for (const [rankedOption, rank] of ranking) { + if (rankedOption.length < 1 || rankedOption.length > MAX_ELECTION_OPTION_LENGTH) { + throw new ValidationError( + ErrorMessage.InvalidObjectKey('ranking', rankedOption, [ + `strings between 1 and ${MAX_ELECTION_OPTION_LENGTH} characters` + ]) + ); + } + + if ( + typeof rank !== 'number' || + !Number.isInteger(rank) || + !Number.isSafeInteger(rank) || + rank < 0 + ) { + throw new ValidationError( + ErrorMessage.InvalidObjectKeyValue('ranking', rank, [ + `safe non-negative integers` + ]) + ); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ranking: _, ...rest } = data as NewOrPatchBallot; + const restKeys = Object.keys(rest); + + if (restKeys.length !== 0) { + throw new ValidationError(ErrorMessage.UnknownField(restKeys[0])); + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..13996ad --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +/* eslint-disable unicorn/prefer-export-from */ +import { name as pkgName } from 'package'; + +/** + * This value is used in first party source app-wide as a debug package + * namespace. + */ +export const debugNamespace = pkgName; diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..6de5977 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,118 @@ +import { ErrorMessage as NamedErrorMessage } from 'named-app-errors'; +import type { LiteralUnion } from 'type-fest'; + +export * from 'named-app-errors'; + +/** + * A collection of possible error and warning messages. + */ +/* istanbul ignore next */ +export const ErrorMessage = { + ...NamedErrorMessage, + DuplicateFieldValue: (property: string) => + `an item with that "${property}" already exists`, + DuplicateArrayValue: (element: string) => + `duplicate array element "${element}" is not allowed`, + InvalidLength: ( + property: string, + actualLength: number, + minLength: number, + maxLength?: number | null + ) => + `\`${property}\` has invalid length (${actualLength}). Length must be between ${minLength} and ${maxLength} (inclusive)`, + InvalidFieldValue: ( + property: string, + value?: string | null, + validValues?: readonly string[] + ) => + `\`${property}\` field has ${ + value + ? `invalid or illegal value "${value}"` + : 'a missing, invalid, or illegal value' + }${validValues ? `. Valid values: ${validValues.join(', ')}` : ''}`, + InvalidArrayValue: ( + property: string, + value?: string, + index?: number, + validValues?: readonly string[] + ) => + `the \`${property}\` array element ${value !== undefined ? ` "${value}"` : ''}${ + index !== undefined ? ` at index ${index}` : '' + } is invalid or illegal${ + validValues ? `. Valid values: ${validValues.join(', ')}` : '' + }`, + InvalidObjectKey: ( + property: string, + key?: string, + validValues?: readonly string[] + ) => + `the \`${property}\` object key \`${key}\` is invalid${validValues ? `. Valid keys: ${validValues.join(', ')}` : ''}`, + InvalidObjectKeyValue: ( + property: string, + value?: unknown, + validValues?: readonly string[] + ) => + `a \`${property}\` object key has ${ + value + ? `invalid or illegal value "${String(value)}"` + : 'a missing, invalid, or illegal value' + }${validValues ? `. Valid values: ${validValues.join(', ')}` : ''}`, + InvalidJSON: (property?: string) => + 'encountered invalid JSON' + (property ? ` in property \`${property}\`` : ''), + EmptyJSONBody: () => 'encountered unexpectedly empty JSON object in request body', + InvalidNumberValue: ( + property: string, + min: number | string, + max: number | string | null, + type: LiteralUnion<'number' | 'integer', string>, + nullable = false, + isArray = false + ) => + `${isArray ? `each \`${property}\` element` : `\`${property}\``} must be a${ + type === 'integer' ? 'n integer' : type === 'number' ? ' number' : type + } ${max ? `between ${min} and ${max} (inclusive)` : `>= ${min}`}${ + nullable ? ' or null' : '' + }`, + InvalidStringLength: ( + property: string, + min: number | string, + max: number | string | null, + syntax: LiteralUnion<'string' | 'alphanumeric' | 'hexadecimal' | 'bytes', string>, + nullable = false, + isArray = false + ) => + `${isArray ? `each \`${property}\` element` : `\`${property}\``} must be a${ + syntax === 'alphanumeric' + ? 'n alphanumeric' + : syntax !== 'bytes' + ? ` ${syntax}` + : '' + } ${ + max + ? `${syntax !== 'string' ? 'string ' : ''}between ${min} and ${max} ${ + syntax === 'bytes' ? 'byte' : 'character' + }s (inclusive)` + : `${min} ${syntax === 'bytes' ? 'byte' : 'character'} string` + }${nullable ? ' or null' : ''}`, + InvalidObjectId: (id: unknown) => `invalid ObjectId "${id}"`, + UnknownField: (property: string) => + `encountered unknown or illegal field \`${property}\``, + UnknownSpecifier: (property: string, sub = false) => + `encountered unknown or illegal ${sub ? 'sub-' : ''}specifier \`${property}\``, + InvalidSpecifierValueType: (property: string, type: string, sub = false) => + `\`${property}\` has invalid ${ + sub ? 'sub-' : '' + }specifier value type (must be ${type})`, + InvalidRegexString: (property: string) => + `\`${property}\` has invalid or illegal regex value`, + BadProvenanceToken: () => 'invalid provenance token attribute owner', + InvariantViolation: (invariant: string) => `invariant violated: ${invariant}`, + IllegalOperation: () => + 'this user is not authorized to execute operations on this item', + // 'navLinks', 0, 5, 'navigation links' + TooMany: (resource?: string, max?: number | string) => { + return `resource limit reached${resource ? `: ${resource}` : ''}${ + max !== undefined ? ` (exceeded maximum of ${max})` : '' + }`; + } +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..6ab50d6 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import * as React from 'react'; +import Head from 'next/head'; + +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + No Browser Access! + + + + + + ); +} diff --git a/src/pages/api/[[...catchAllForNotFound]].ts b/src/pages/api/[[...catchAllForNotFound]].ts new file mode 100644 index 0000000..2cc517c --- /dev/null +++ b/src/pages/api/[[...catchAllForNotFound]].ts @@ -0,0 +1,36 @@ +/* eslint-disable unicorn/filename-case */ +import { withMiddleware } from 'universe/backend/middleware'; +import { sendHttpNotFound } from 'multiverse/next-api-respond'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: 'catch-all-for-not-found' +}; + +const descriptorPrefix = '404:'; + +export default withMiddleware(async (_req, res) => sendHttpNotFound(res), { + prependUse: [ + (req, _res, context) => { + context.runtime.endpoint.descriptor = `${descriptorPrefix}/${ + [req.query.catchAllForNotFound].flat().join('/') || '/' + }`; + } + ], + descriptor: descriptorPrefix, + options: { + allowedMethods: [ + 'CONNECT', + 'DELETE', + 'GET', + 'HEAD', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + 'TRACE' + ] + } +}); diff --git a/src/pages/api/sys/auth/[auth_id].ts b/src/pages/api/sys/auth/[auth_id].ts new file mode 100644 index 0000000..5b4cc0b --- /dev/null +++ b/src/pages/api/sys/auth/[auth_id].ts @@ -0,0 +1,46 @@ +/* eslint-disable unicorn/filename-case */ +import { withSysMiddleware } from 'universe/backend/middleware'; +import { sendHttpOk } from 'multiverse/next-api-respond'; + +import { + getTokenById, + updateTokenAttributesById, + deleteTokenById +} from 'multiverse/next-auth'; + +// ? https://nextjs.org/docs/api-routes/api-middlewares#custom-config +export { defaultConfig as config } from 'universe/backend/api'; + +export default withSysMiddleware( + async (req, res) => { + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + fullToken: await getTokenById({ auth_id: req.query.auth_id?.toString() }) + }); + break; + } + + case 'PATCH': { + sendHttpOk(res, { + updated: await updateTokenAttributesById({ + auth_id: req.query.auth_id?.toString(), + update: req.body?.attributes + }) + }); + break; + } + + case 'DELETE': { + sendHttpOk(res, { + deleted: await deleteTokenById({ auth_id: req.query.auth_id?.toString() }) + }); + break; + } + } + }, + { + descriptor: '/sys/auth/:auth_id', + options: { allowedMethods: ['GET', 'PATCH', 'DELETE'] } + } +); diff --git a/src/pages/api/sys/auth/index.ts b/src/pages/api/sys/auth/index.ts new file mode 100644 index 0000000..4a6bd41 --- /dev/null +++ b/src/pages/api/sys/auth/index.ts @@ -0,0 +1,31 @@ +import { withSysMiddleware } from 'universe/backend/middleware'; +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { getTokensByAttribute, createToken } from 'multiverse/next-auth'; + +// ? https://nextjs.org/docs/api-routes/api-middlewares#custom-config +export { defaultConfig as config } from 'universe/backend/api'; + +export default withSysMiddleware( + async (req, res) => { + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + fullTokens: await getTokensByAttribute({ + filter: {}, + after_id: req.query.after?.toString() + }) + }); + break; + } + + case 'POST': { + sendHttpOk(res, { fullToken: await createToken({ data: req.body }) }); + break; + } + } + }, + { + descriptor: '/sys/auth', + options: { allowedMethods: ['GET', 'POST'] } + } +); diff --git a/src/pages/api/sys/auth/search.ts b/src/pages/api/sys/auth/search.ts new file mode 100644 index 0000000..f1a9319 --- /dev/null +++ b/src/pages/api/sys/auth/search.ts @@ -0,0 +1,50 @@ +/* eslint-disable unicorn/filename-case */ +import { withSysMiddleware } from 'universe/backend/middleware'; +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { validateAndParseJson } from 'multiverse/next-api-util'; + +import { + getTokensByAttribute, + updateTokensAttributesByAttribute, + deleteTokensByAttribute +} from 'multiverse/next-auth'; + +// ? https://nextjs.org/docs/api-routes/api-middlewares#custom-config +export { defaultConfig as config } from 'universe/backend/api'; + +export default withSysMiddleware( + async (req, res) => { + const filter = validateAndParseJson(req.query.filter?.toString(), 'filter'); + + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + fullTokens: await getTokensByAttribute({ + filter, + after_id: req.query.after?.toString() + }) + }); + break; + } + + case 'PATCH': { + sendHttpOk(res, { + updated: await updateTokensAttributesByAttribute({ + filter, + update: req.body?.attributes + }) + }); + break; + } + + case 'DELETE': { + sendHttpOk(res, { deleted: await deleteTokensByAttribute({ filter }) }); + break; + } + } + }, + { + descriptor: '/sys/auth/search', + options: { allowedMethods: ['GET', 'PATCH', 'DELETE'] } + } +); diff --git a/src/pages/api/sys/ping.ts b/src/pages/api/sys/ping.ts new file mode 100644 index 0000000..7e558a2 --- /dev/null +++ b/src/pages/api/sys/ping.ts @@ -0,0 +1,22 @@ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { withSysMiddleware } from 'universe/backend/middleware'; + +// ? https://nextjs.org/docs/api-routes/api-middlewares#custom-config +export { defaultConfig as config } from 'universe/backend/api'; + +export default withSysMiddleware( + async (req, res) => { + const { name = 'Mr. World' } = req.query; + sendHttpOk(res, { + message: `Hello to ${name} at ${new Date(Date.now()).toLocaleString()}` + }); + }, + { + descriptor: '/sys/ping', + options: { + allowedMethods: ['GET'], + allowedContentTypes: 'any', + requiresAuth: false + } + } +); diff --git a/src/pages/api/v1/elections/[election_id]/ballots/[voter_id].ts b/src/pages/api/v1/elections/[election_id]/ballots/[voter_id].ts new file mode 100644 index 0000000..e20c77c --- /dev/null +++ b/src/pages/api/v1/elections/[election_id]/ballots/[voter_id].ts @@ -0,0 +1,58 @@ +/* eslint-disable unicorn/filename-case */ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { withMiddleware } from 'universe/backend/middleware'; + +import { + deleteBallotFromElection, + getBallotForElection, + upsertBallot +} from 'universe/backend'; +import { authorizationHeaderToOwnerAttribute } from 'universe/backend/api'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: '/v1/elections/:election_id/ballots/:voter_id' +}; + +export default withMiddleware( + async (req, res) => { + const election_id = req.query.election_id?.toString(); + const voter_id = req.query.voter_id?.toString(); + const provenance = await authorizationHeaderToOwnerAttribute( + req.headers.authorization + ); + + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + ballot: await getBallotForElection({ election_id, voter_id }) + }); + break; + } + + case 'PUT': { + sendHttpOk(res, { + ballot: await upsertBallot({ + election_id, + voter_id, + data: req.body, + provenance + }) + }); + break; + } + + case 'DELETE': { + await deleteBallotFromElection({ election_id, voter_id, provenance }); + sendHttpOk(res); + break; + } + } + }, + { + descriptor: metadata.descriptor, + options: { allowedMethods: ['GET', 'PUT', 'DELETE'], apiVersion: '1' } + } +); diff --git a/src/pages/api/v1/elections/[election_id]/ballots/index.ts b/src/pages/api/v1/elections/[election_id]/ballots/index.ts new file mode 100644 index 0000000..629f439 --- /dev/null +++ b/src/pages/api/v1/elections/[election_id]/ballots/index.ts @@ -0,0 +1,29 @@ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { getAllBallotsForElection } from 'universe/backend'; +import { withMiddleware } from 'universe/backend/middleware'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: '/v1/elections' +}; + +export default withMiddleware( + async (req, res) => { + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + ballots: await getAllBallotsForElection({ + election_id: req.query.election_id?.toString() + }) + }); + break; + } + } + }, + { + descriptor: metadata.descriptor, + options: { allowedMethods: ['GET'], apiVersion: '1' } + } +); diff --git a/src/pages/api/v1/elections/[election_id]/index.ts b/src/pages/api/v1/elections/[election_id]/index.ts new file mode 100644 index 0000000..cec2515 --- /dev/null +++ b/src/pages/api/v1/elections/[election_id]/index.ts @@ -0,0 +1,45 @@ +/* eslint-disable unicorn/filename-case */ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { withMiddleware } from 'universe/backend/middleware'; + +import { deleteElection, getElection, updateElection } from 'universe/backend'; +import { authorizationHeaderToOwnerAttribute } from 'universe/backend/api'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: '/v1/elections/:election_id' +}; + +export default withMiddleware( + async (req, res) => { + const election_id = req.query.election_id?.toString(); + const provenance = await authorizationHeaderToOwnerAttribute( + req.headers.authorization + ); + + switch (req.method) { + case 'GET': { + sendHttpOk(res, { election: await getElection({ election_id, provenance }) }); + break; + } + + case 'PATCH': { + await updateElection({ election_id, data: req.body, provenance }); + sendHttpOk(res); + break; + } + + case 'DELETE': { + await deleteElection({ election_id, provenance }); + sendHttpOk(res); + break; + } + } + }, + { + descriptor: metadata.descriptor, + options: { allowedMethods: ['GET', 'PATCH', 'DELETE'], apiVersion: '1' } + } +); diff --git a/src/pages/api/v1/elections/index.ts b/src/pages/api/v1/elections/index.ts new file mode 100644 index 0000000..68a29e8 --- /dev/null +++ b/src/pages/api/v1/elections/index.ts @@ -0,0 +1,45 @@ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { createElection, getAllElections } from 'universe/backend'; +import { authorizationHeaderToOwnerAttribute } from 'universe/backend/api'; +import { withMiddleware } from 'universe/backend/middleware'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: '/v1/elections' +}; + +export default withMiddleware( + async (req, res) => { + const provenance = await authorizationHeaderToOwnerAttribute( + req.headers.authorization + ); + + switch (req.method) { + case 'GET': { + sendHttpOk(res, { + elections: await getAllElections({ + after_id: req.query.after?.toString(), + provenance + }) + }); + break; + } + + case 'POST': { + sendHttpOk(res, { + election: await createElection({ + data: req.body, + provenance + }) + }); + break; + } + } + }, + { + descriptor: metadata.descriptor, + options: { allowedMethods: ['GET', 'POST'], apiVersion: '1' } + } +); diff --git a/src/pages/api/v1/info/index.ts b/src/pages/api/v1/info/index.ts new file mode 100644 index 0000000..6d3ca78 --- /dev/null +++ b/src/pages/api/v1/info/index.ts @@ -0,0 +1,25 @@ +import { sendHttpOk } from 'multiverse/next-api-respond'; +import { getInfo } from 'universe/backend'; +import { withMiddleware } from 'universe/backend/middleware'; + +// ? This is a NextJS special "config" export +export { defaultConfig as config } from 'universe/backend/api'; + +export const metadata = { + descriptor: '/v1/info' +}; + +export default withMiddleware( + async (req, res) => { + switch (req.method) { + case 'GET': { + sendHttpOk(res, { info: await getInfo() }); + break; + } + } + }, + { + descriptor: metadata.descriptor, + options: { allowedMethods: ['GET'], apiVersion: '1' } + } +); diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..b35b0e9 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,45 @@ +import { version as pkgVersion } from 'package'; +import * as React from 'react'; +import { getEnv } from 'universe/backend/env'; +//import { hydrateDb } from 'multiverse/mongo-test'; + +export async function getServerSideProps() { + const env = getEnv(); + //await hydrateDb({ name: 'app' }); + + return { + props: { + isInProduction: env.NODE_ENV === 'production', + nodeEnv: env.NODE_ENV, + nodeVersion: process.version + } + }; +} + +export default function Index({ + isInProduction, + nodeEnv, + nodeVersion +}: Awaited>['props']) { + return ( + +

+ Serverless node runtime: {nodeVersion}
+ elections_irv runtime: {`v${pkgVersion}`} +
+

+

+ Environment: {nodeEnv}
+ Production mode:{' '} + + {isInProduction ? ( + yes + ) : ( + no + )} + +
+

+
+ ); +} diff --git a/test/api/integration.test.ts b/test/api/integration.test.ts new file mode 100644 index 0000000..68ff0bd --- /dev/null +++ b/test/api/integration.test.ts @@ -0,0 +1,248 @@ +/* eslint-disable jest/require-hook */ +import { getProperty as dotPath } from 'dot-prop'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { toss } from 'toss-expression'; + +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { BANNED_BEARER_TOKEN, DUMMY_BEARER_TOKEN } from 'multiverse/next-auth'; +import { getFixtures } from 'testverse/integration'; +import { mockEnvFactory } from 'testverse/setup'; +import { api } from 'testverse/util'; +import { ValidationError } from 'universe/error'; + +import type { TestResult, TestResultset } from 'testverse/integration'; + +setupMemoryServerOverride({ + // ? Ensure all tests share the same database state + defer: true +}); + +const withMockedEnv = mockEnvFactory( + { OVERRIDE_EXPECT_ENV: 'force-check' }, + { replace: false } +); + +// ? Memory of the results of past fixture runs. +const memory: TestResultset = [ + { status: Number.POSITIVE_INFINITY, json: {} } +] as unknown as TestResultset; + +memory.latest = memory[0]; +memory.getResultAt = () => memory[0]; +memory.idMap = {}; + +// ? Fail fast and early +let lastRunSuccess = true; + +describe('> middleware correctness tests', () => { + Object.values(api) + .flatMap((v) => Object.values(v)) + .forEach((endpoint) => { + it(`${endpoint.uri} fails on bad authentication`, async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: endpoint, + test: async ({ fetch }) => { + await expect(fetch().then((r) => r.status)).resolves.toBe(401); + } + }); + }, + { + REQUESTS_PER_CONTRIVED_ERROR: '0', + IGNORE_RATE_LIMITS: 'true' + } + ); + }); + + it(`${endpoint.uri} fails if rate limited`, async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: endpoint, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${BANNED_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(429); + } + }); + }, + { + REQUESTS_PER_CONTRIVED_ERROR: '0', + IGNORE_RATE_LIMITS: 'false' + } + ); + }); + }); +}); + +describe('> fable integration tests', () => { + // ? Clear the request-log so contrived errors are counted properly + beforeAll(async () => { + await (await getDb({ name: 'root' })).collection('request-log').deleteMany({}); + }); + + let countSkippedTests = 0; + + afterAll(() => { + if (countSkippedTests) + // eslint-disable-next-line no-console + console.warn(`${countSkippedTests} tests were skipped!`); + }); + + getFixtures(api).forEach( + ({ + displayIndex, + subject, + handler, + method, + response, + body, + id, + params, + invisible + }) => { + if (!displayIndex) { + throw new ValidationError( + 'fixture is missing required property "displayIndex"' + ); + } + + const shouldSkip = + !subject || + !handler || + !method || + (!invisible && + (!response || !['number', 'function'].includes(typeof response.status))); + + // eslint-disable-next-line jest/prefer-expect-assertions + (process.env.RUN_ONLY ? it.only : it)( + `${shouldSkip ? ' ' : ''}${ + displayIndex <= 0 ? '###' : '#' + displayIndex + } ${method ? '[' + method + '] ' : ''}${ + handler?.uri ? handler.uri + ' ' : '' + }${subject || ''}`, + async () => { + if (shouldSkip || (!lastRunSuccess && process.env.FAIL_FAST)) { + countSkippedTests++; + return; + } + + // eslint-disable-next-line jest/no-standalone-expect + expect.hasAssertions(); + lastRunSuccess = false; + + memory.getResultAt = ( + index: number | string, + prop?: string + ): TestResult | T => { + const result: TestResult = + typeof index === 'string' + ? memory.idMap[index] + : memory[index + (index < 0 ? displayIndex : 1)]; + + if (!result) { + throw new ValidationError(`no result at index "${index}"`); + } + + const returnValue = prop + ? result.json === undefined + ? undefined + : (dotPath(result.json, prop) as T) + : result; + + if (returnValue === undefined) { + throw new ValidationError( + `${ + prop ? 'prop path "' + prop + '" ' : '' + }return value cannot be undefined` + ); + } + + return returnValue; + }; + + const requestParams = + typeof params === 'function' ? await params(memory) : params; + const requestBody = typeof body === 'function' ? await body(memory) : body; + + await withMockedEnv( + async () => { + await testApiHandler({ + pagesHandler: + handler || toss(new ValidationError('no handler provided')), + params: requestParams, + requestPatcher: (req) => { + req.headers.authorization = `bearer ${DUMMY_BEARER_TOKEN}`; + req.headers['content-type'] = 'application/json'; + }, + test: async ({ fetch }) => { + const res = await fetch({ + method: method, + ...(requestBody ? { body: JSON.stringify(requestBody) } : {}) + }); + + const expectedStatus = + typeof response?.status === 'function' + ? await response.status(res.status, memory) + : response?.status; + + let json: ReturnType; + + try { + const jsonText = await res.text(); + json = `${jsonText}`; + json = JSON.parse(jsonText); + } catch {} + + if (expectedStatus) { + if (res.status !== expectedStatus) { + // eslint-disable-next-line no-console + console.warn('unexpected status for result:', json); + } + + // eslint-disable-next-line jest/no-conditional-expect + expect(res.status).toBe(expectedStatus); + // eslint-disable-next-line jest/no-conditional-expect + expect(json.success)[ + res.status === 200 ? 'toBeTrue' : 'toBeFalsy' + ](); + delete json.success; + } + + const expectedJson = + typeof response?.json === 'function' + ? await response.json(json, memory) + : response?.json; + + if (expectedJson) { + // eslint-disable-next-line jest/no-conditional-expect + expect(json).toStrictEqual(expectedJson); + } + + const memorize = { status: res.status, json } as TestResult; + + if (id) memory.idMap[id] = memorize; + memory[displayIndex] = memorize; + memory.latest = memorize; + lastRunSuccess = true; + } + }); + }, + { + REQUESTS_PER_CONTRIVED_ERROR: '10', + IGNORE_RATE_LIMITS: 'true' + } + ); + } + ); + } + ); +}); diff --git a/test/api/unit-app-elections.test.ts b/test/api/unit-app-elections.test.ts new file mode 100644 index 0000000..1a9c248 --- /dev/null +++ b/test/api/unit-app-elections.test.ts @@ -0,0 +1,205 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { testApiHandler } from 'next-test-api-route-handler'; +import { api, setupMockBackend } from 'testverse/util'; + +jest.mock('universe/backend'); +jest.mock('universe/backend/api', (): typeof import('universe/backend/api') => { + return { + ...jest.requireActual('universe/backend/api'), + authorizationHeaderToOwnerAttribute: jest.fn(() => Promise.resolve('mock-owner')) + }; +}); + +jest.mock( + 'universe/backend/middleware', + (): typeof import('universe/backend/middleware') => { + const { middlewareFactory } = require('multiverse/next-api-glue'); + const { default: handleError } = require('multiverse/next-adhesive/handle-error'); + + return { + withMiddleware: jest + .fn() + .mockImplementation(middlewareFactory({ use: [], useOnError: [handleError] })) + } as unknown as typeof import('universe/backend/middleware'); + } +); + +setupMockBackend(); + +describe('api/v1/elections', () => { + describe('/ [GET]', () => { + it('accepts GET requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.elections, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'GET' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.elections).toBeArray(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); + + describe('/ [POST]', () => { + it('accepts POST requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.elections, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'POST' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.election).toBeObject(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); + + describe('/:election_id [GET]', () => { + it('accepts GET requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'GET' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.election).toBeObject(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); + + describe('/:election_id [PATCH]', () => { + it('accepts PATCH requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'PATCH' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(Object.keys(json)).toHaveLength(1); + } + }); + }); + }); + + describe('/:election_id [DELETE]', () => { + it('accepts DELETE requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'DELETE' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(Object.keys(json)).toHaveLength(1); + } + }); + }); + }); + + describe('/:election_id/ballots [GET]', () => { + it('accepts GET requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionIdBallots, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'GET' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.ballots).toBeArray(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); + + describe('/:election_id/ballots/:voter_id [GET]', () => { + it('accepts GET requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionIdBallotsVoterId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'GET' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.ballot).toBeObject(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); + + describe('/:election_id/ballots/:voter_id [PUT]', () => { + it('accepts PUT requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionIdBallotsVoterId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'PUT' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(Object.keys(json)).toHaveLength(1); + } + }); + }); + }); + + describe('/:election_id/ballots/:voter_id [DELETE]', () => { + it('accepts DELETE requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.electionsElectionIdBallotsVoterId, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'DELETE' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(Object.keys(json)).toHaveLength(1); + } + }); + }); + }); +}); diff --git a/test/api/unit-app-info.test.ts b/test/api/unit-app-info.test.ts new file mode 100644 index 0000000..796ccbf --- /dev/null +++ b/test/api/unit-app-info.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { testApiHandler } from 'next-test-api-route-handler'; +import { api, setupMockBackend } from 'testverse/util'; + +jest.mock('universe/backend'); +jest.mock('universe/backend/api', (): typeof import('universe/backend/api') => { + return { + ...jest.requireActual('universe/backend/api'), + authorizationHeaderToOwnerAttribute: jest.fn(() => Promise.resolve('mock-owner')) + }; +}); + +jest.mock( + 'universe/backend/middleware', + (): typeof import('universe/backend/middleware') => { + const { middlewareFactory } = require('multiverse/next-api-glue'); + const { default: handleError } = require('multiverse/next-adhesive/handle-error'); + + return { + withMiddleware: jest + .fn() + .mockImplementation(middlewareFactory({ use: [], useOnError: [handleError] })) + } as unknown as typeof import('universe/backend/middleware'); + } +); + +setupMockBackend(); + +describe('api/v1/info', () => { + describe('/ [GET]', () => { + it('accepts GET requests', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.v1.info, + test: async ({ fetch }) => { + const [status, json] = await fetch({ method: 'GET' }).then( + async (r) => [r.status, await r.json()] as [status: number, json: any] + ); + + expect(status).toBe(200); + expect(json.success).toBeTrue(); + expect(json.info).toBeObject(); + expect(Object.keys(json)).toHaveLength(2); + } + }); + }); + }); +}); diff --git a/test/api/unit-catchall.test.ts b/test/api/unit-catchall.test.ts new file mode 100644 index 0000000..3270048 --- /dev/null +++ b/test/api/unit-catchall.test.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { testApiHandler } from 'next-test-api-route-handler'; +import { api, setupMockBackend } from 'testverse/util'; + +jest.mock('universe/backend'); +jest.mock('universe/backend/api', (): typeof import('universe/backend/api') => { + return { + ...jest.requireActual('universe/backend/api'), + authorizationHeaderToOwnerAttribute: jest.fn(() => Promise.resolve('mock-owner')) + }; +}); + +jest.mock( + 'universe/backend/middleware', + (): typeof import('universe/backend/middleware') => { + const { middlewareFactory } = require('multiverse/next-api-glue'); + const { default: handleError } = require('multiverse/next-adhesive/handle-error'); + + return { + withMiddleware: jest + .fn() + .mockImplementation(middlewareFactory({ use: [], useOnError: [handleError] })) + } as unknown as typeof import('universe/backend/middleware'); + } +); + +setupMockBackend(); + +it('sends 404 regardless of method', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: api.catchAllForNotFound, + test: async ({ fetch }) => { + await Promise.all( + ['DELETE', 'GET', 'PATCH', 'POST', 'PUT'].map((method) => { + return fetch({ method }).then(async (r) => { + const status = r.status; + const json = await r.json().catch(() => ({})); + + expect([method, status]).toStrictEqual([method, 404]); + expect([method, json.success]).toStrictEqual([method, false]); + expect([method, json.error]).toStrictEqual([method, expect.any(String)]); + expect([method, Object.keys(json).length]).toStrictEqual([method, 2]); + }); + }) + ); + + await Promise.all( + ['HEAD', 'OPTIONS' /* , 'TRACE' */].map((method) => { + return fetch({ method }).then(async ({ status }) => { + expect([method, status]).toStrictEqual([method, 404]); + }); + }) + ); + } + }); +}); diff --git a/test/api/unit-sys-auth.test.ts b/test/api/unit-sys-auth.test.ts new file mode 100644 index 0000000..6669f36 --- /dev/null +++ b/test/api/unit-sys-auth.test.ts @@ -0,0 +1,179 @@ +/* eslint-disable no-global-assign */ +import { ObjectId, type Collection, type Db } from 'mongodb'; +import { testApiHandler } from 'next-test-api-route-handler'; +import { randomUUID } from 'node:crypto'; + +import { useMockDateNow } from 'multiverse/jest-mock-date'; +import { dummyRootData } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + BANNED_BEARER_TOKEN, + DEV_BEARER_TOKEN, + DUMMY_BEARER_TOKEN, + toPublicAuthEntry, + type InternalAuthBearerEntry, + type PublicAuthEntry +} from 'multiverse/next-auth'; + +import AuthEndpoint, { + config as AuthConfig +} from 'universe/pages/api/sys/auth/index'; + +import AuthAuthIdEndpoint, { + config as AuthAuthIdConfig +} from 'universe/pages/api/sys/auth/[auth_id]'; + +import AuthSearchEndpoint, { + config as AuthSearchConfig +} from 'universe/pages/api/sys/auth/search'; + +import type { InternalLimitedLogEntry } from 'multiverse/next-limit'; +import type { NextApiHandlerMixin } from 'testverse/util'; + +setupMemoryServerOverride(); +useMockDateNow(); + +// * This suite blurs the line between unit and integration tests for portability +// * reasons. +// TODO: replace with next-fable (formerly / in addition to: @xunnamius/fable). +// TODO: Also, split out into proper unit vs integration tests when in package +// TODO: form. + +const authHandler = AuthEndpoint as NextApiHandlerMixin; +const authAuthIdHandler = AuthAuthIdEndpoint as NextApiHandlerMixin; +const authSearchHandler = AuthSearchEndpoint as NextApiHandlerMixin; + +authHandler.config = AuthConfig; +authAuthIdHandler.config = AuthAuthIdConfig; +authSearchHandler.config = AuthSearchConfig; + +describe('middleware correctness tests', () => { + it('endpoints fail on req with bad authentication', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: authHandler, + test: async ({ fetch }) => { + await expect(fetch().then((r) => r.status)).resolves.toBe(401); + } + }); + + await testApiHandler({ + pagesHandler: authAuthIdHandler, + params: { auth_id: new ObjectId().toString() }, + test: async ({ fetch }) => { + await expect(fetch().then((r) => r.status)).resolves.toBe(401); + } + }); + + await testApiHandler({ + pagesHandler: authSearchHandler, + test: async ({ fetch }) => { + await expect(fetch().then((r) => r.status)).resolves.toBe(401); + } + }); + }); + + it('endpoints fail if authenticated req is not authorized', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler: authHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${DUMMY_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(403); + } + }); + + await testApiHandler({ + pagesHandler: authAuthIdHandler, + params: { auth_id: new ObjectId().toString() }, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${DUMMY_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(403); + } + }); + + await testApiHandler({ + pagesHandler: authSearchHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${DUMMY_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(403); + } + }); + }); + + it('endpoints fail if authed req is rate limited', async () => { + expect.hasAssertions(); + + await (await getDb({ name: 'root' })) + .collection('auth') + .updateOne( + { token: { bearer: BANNED_BEARER_TOKEN } }, + { $set: { 'attributes.isGlobalAdmin': true } } + ); + + await testApiHandler({ + pagesHandler: authHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${BANNED_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(429); + } + }); + + await testApiHandler({ + pagesHandler: authAuthIdHandler, + params: { auth_id: new ObjectId().toString() }, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${BANNED_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(429); + } + }); + + await testApiHandler({ + pagesHandler: authSearchHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${BANNED_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(429); + } + }); + }); +}); + +describe('api/sys/auth', () => { + describe('/ [GET]', () => { + it.todo('this'); + }); +}); + +describe('api/sys/auth/:auth_id', () => { + describe('/ [GET]', () => { + it.todo('this'); + }); +}); + +describe('api/sys/auth/search', () => { + describe('/ [GET]', () => { + it.todo('this'); + }); +}); diff --git a/test/api/unit-sys-limits.test.ts b/test/api/unit-sys-limits.test.ts new file mode 100644 index 0000000..0b30bba --- /dev/null +++ b/test/api/unit-sys-limits.test.ts @@ -0,0 +1 @@ +test.todo('this (see inbdpa)'); diff --git a/test/api/unit-sys-logs.test.ts b/test/api/unit-sys-logs.test.ts new file mode 100644 index 0000000..0b30bba --- /dev/null +++ b/test/api/unit-sys-logs.test.ts @@ -0,0 +1 @@ +test.todo('this (see inbdpa)'); diff --git a/test/api/unit-sys-ping.test.ts b/test/api/unit-sys-ping.test.ts new file mode 100644 index 0000000..cf60cc7 --- /dev/null +++ b/test/api/unit-sys-ping.test.ts @@ -0,0 +1,124 @@ +/* eslint-disable no-global-assign */ +import { useMockDateNow } from 'multiverse/jest-mock-date'; +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { BANNED_BEARER_TOKEN, DUMMY_BEARER_TOKEN } from 'multiverse/next-auth'; +import { testApiHandler } from 'next-test-api-route-handler'; + +import Endpoint, { config as Config } from 'universe/pages/api/sys/ping'; + +import type { InternalAuthBearerEntry } from 'multiverse/next-auth'; + +const pagesHandler = Endpoint as typeof Endpoint & { config?: typeof Config }; +pagesHandler.config = Config; + +setupMemoryServerOverride(); +useMockDateNow(); + +// * This suite blurs the line between unit and integration tests for portability +// * reasons. +// TODO: replace with next-fable (formerly / in addition to: @xunnamius/fable) + +describe('middleware correctness tests', () => { + it('endpoints is not authenticated', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + await expect(fetch().then((r) => r.status)).resolves.toBe(200); + } + }); + }); + + it('endpoints ignores authentication and authorization header', async () => { + expect.hasAssertions(); + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${DUMMY_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(200); + } + }); + }); + + it('endpoint fails if req is rate limited', async () => { + expect.hasAssertions(); + + await (await getDb({ name: 'root' })) + .collection('auth') + .updateOne( + { token: { bearer: BANNED_BEARER_TOKEN } }, + { $set: { 'attributes.isGlobalAdmin': true } } + ); + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + await expect( + fetch({ + headers: { Authorization: `bearer ${BANNED_BEARER_TOKEN}` } + }).then((r) => r.status) + ).resolves.toBe(429); + } + }); + }); +}); + +describe('api/sys/ping', () => { + it('pongs when we ping', async () => { + expect.hasAssertions(); + + const oldDate = Date; + + try { + // @ts-expect-error: overriding Date is tough stuff + Date = class extends Date { + constructor(...args: Parameters) { + super(...args); + } + + toLocaleString(): string; + toLocaleString( + locales?: string | string[], + options?: Intl.DateTimeFormatOptions + ): string; + toLocaleString(locales?: unknown, options?: unknown): string { + void locales, options; + return 'fake date, fake time'; + } + }; + + await testApiHandler({ + pagesHandler, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + await expect(res.json()).resolves.toStrictEqual({ + success: true, + message: 'Hello to Mr. World at fake date, fake time' + }); + } + }); + + await testApiHandler({ + pagesHandler, + params: { name: 'Ms. Universe' }, + test: async ({ fetch }) => { + const res = await fetch(); + expect(res.status).toBe(200); + await expect(res.json()).resolves.toStrictEqual({ + success: true, + message: 'Hello to Ms. Universe at fake date, fake time' + }); + } + }); + } finally { + Date = oldDate; + } + }); +}); diff --git a/test/backend/unit-backend.test.ts b/test/backend/unit-backend.test.ts new file mode 100644 index 0000000..3fdbb80 --- /dev/null +++ b/test/backend/unit-backend.test.ts @@ -0,0 +1,1549 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-await-in-loop */ +import { ObjectId } from 'mongodb'; +import assert from 'node:assert'; + +import * as Backend from 'universe/backend'; +import { getEnv } from 'universe/backend/env'; +import { ErrorMessage } from 'universe/error'; + +import { + getBallotsDb, + getElectionsDb, + toPublicBallot, + toPublicElection, + toPublicInfo, + type NewElection, + type NewOrPatchBallot, + type PatchElection, + type PublicElection +} from 'universe/backend/db'; + +import { expectExceptionsWithMatchingErrors } from 'multiverse/jest-expect-matching-errors'; +import { itemToObjectId, itemToStringId } from 'multiverse/mongo-item'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + dummyRootData, + mockDateNowMs, + useMockDateNow +} from 'multiverse/mongo-common'; + +import { dummyAppData } from 'testverse/db'; +import { mockEnvFactory } from 'testverse/setup'; + +import type { LiteralUnknownUnion } from 'types/global'; + +setupMemoryServerOverride(); +useMockDateNow(); + +const withMockedEnv = mockEnvFactory({ NODE_ENV: 'test' }); +const provenance = dummyRootData.auth[0].attributes.owner; + +describe('::getAllElections', () => { + it('returns all users in LIFO order', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance + }) + ).resolves.toStrictEqual( + dummyAppData.elections + .toReversed() + .map((internalElection) => + toPublicElection(internalElection, { owned: true }) + ) + ); + }); + + it('does not crash when database is empty', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance + }) + ).resolves.not.toStrictEqual([]); + + await (await getElectionsDb()).deleteMany({}); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance + }) + ).resolves.toStrictEqual([]); + }); + + it('supports pagination', async () => { + expect.hasAssertions(); + + await withMockedEnv( + async () => { + const expectedResults = dummyAppData.elections + .toReversed() + .map((election) => [toPublicElection(election, { owned: true })]); + + assert(expectedResults.length === 6); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance + }) + ).resolves.toStrictEqual(expectedResults[0]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[5]), + provenance + }) + ).resolves.toStrictEqual(expectedResults[1]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[4]), + provenance + }) + ).resolves.toStrictEqual(expectedResults[2]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[3]), + provenance + }) + ).resolves.toStrictEqual(expectedResults[3]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[2]), + provenance + }) + ).resolves.toStrictEqual(expectedResults[4]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[1]), + provenance + }) + ).resolves.toStrictEqual(expectedResults[5]); + + await expect( + Backend.getAllElections({ + after_id: itemToStringId(dummyAppData.elections[0]), + provenance + }) + ).resolves.toStrictEqual([]); + }, + { RESULTS_PER_PAGE: '1' } + ); + }); + + it('returns owner=false on provenance mismatch', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance: 'fake' + }) + ).resolves.toStrictEqual( + dummyAppData.elections + .toReversed() + .map((internalElection) => + toPublicElection(internalElection, { owned: false }) + ) + ); + }); + + it('rejects if after_id is invalid (undefined is okay)', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllElections({ after_id: 'fake-oid', provenance }) + ).rejects.toMatchObject({ message: ErrorMessage.InvalidObjectId('fake-oid') }); + }); + + it('rejects if after_id is invalid or not found', async () => { + expect.hasAssertions(); + + const after_id = new ObjectId().toString(); + + await expect( + Backend.getAllElections({ after_id, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(after_id, 'after_id') + }); + + await expect( + Backend.getAllElections({ after_id: 'invalid', provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllElections({ + after_id: undefined, + provenance: undefined as unknown as string + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); +}); + +describe('::getAllBallotsForElection', () => { + it('returns all ballots for the given election', async () => { + expect.hasAssertions(); + + { + const electionId = itemToObjectId(dummyAppData.elections[2]); + + await expect( + Backend.getAllBallotsForElection({ + election_id: itemToStringId(dummyAppData.elections[2]) + }) + ).resolves.toStrictEqual( + dummyAppData.ballots + .filter(({ election_id: _electionId }) => electionId.equals(_electionId)) + .map((ballot) => toPublicBallot(ballot)) + ); + } + + { + const electionId = itemToObjectId(dummyAppData.elections[3]); + + await expect( + Backend.getAllBallotsForElection({ + election_id: itemToStringId(dummyAppData.elections[3]) + }) + ).resolves.toStrictEqual( + dummyAppData.ballots + .filter(({ election_id: _electionId }) => electionId.equals(_electionId)) + .map((ballot) => toPublicBallot(ballot)) + ); + } + }); + + it('handles election with no votes', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllBallotsForElection({ + election_id: itemToStringId(dummyAppData.elections[0]) + }) + ).resolves.toStrictEqual([]); + }); + + it('does not crash when database is empty', async () => { + expect.hasAssertions(); + + await expect( + Backend.getAllBallotsForElection({ + election_id: itemToStringId(dummyAppData.elections[2]) + }) + ).resolves.not.toStrictEqual([]); + + await (await getBallotsDb()).deleteMany({}); + + await expect( + Backend.getAllBallotsForElection({ + election_id: itemToStringId(dummyAppData.elections[2]) + }) + ).resolves.toStrictEqual([]); + }); + + it('rejects if election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + + await expect( + Backend.getAllBallotsForElection({ election_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + + await expect( + Backend.getAllBallotsForElection({ election_id: undefined }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + await expect( + Backend.getAllBallotsForElection({ election_id: 'invalid' }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); +}); + +describe('::getElection', () => { + it('returns an election', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[0]); + + await expect( + Backend.getElection({ election_id, provenance }) + ).resolves.toStrictEqual( + toPublicElection(dummyAppData.elections[0], { owned: true }) + ); + }); + + it('returns owner=false on provenance mismatch', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[0]); + + await expect( + Backend.getElection({ + election_id, + provenance: 'fake' + }) + ).resolves.toStrictEqual( + toPublicElection(dummyAppData.elections[0], { owned: false }) + ); + }); + + it('rejects if election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + + await expect( + Backend.getElection({ election_id, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + + await expect( + Backend.getElection({ election_id: undefined, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + await expect( + Backend.getElection({ election_id: 'invalid', provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.getElection({ + election_id: itemToStringId(dummyAppData.elections[0]), + provenance: undefined as unknown as string + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); +}); + +describe('::getBallotForElection', () => { + it('returns a ballot from an election', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[1]); + const voter_id = dummyAppData.ballots[0].voter_id; + + await expect( + Backend.getBallotForElection({ election_id, voter_id }) + ).resolves.toStrictEqual(toPublicBallot(dummyAppData.ballots[0])); + }); + + it('rejects if election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + const voter_id = dummyAppData.ballots[0].voter_id; + + await expect( + Backend.getBallotForElection({ election_id, voter_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + + await expect( + Backend.getBallotForElection({ election_id: undefined, voter_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + await expect( + Backend.getBallotForElection({ election_id: 'invalid', voter_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); + + it('rejects if voter_id is undefined or not found', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[1]); + const voter_id = 'fake'; + + await expect( + Backend.getBallotForElection({ election_id, voter_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(voter_id, 'ballot') + }); + + await expect( + Backend.getBallotForElection({ election_id, voter_id: undefined }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidStringLength( + 'voter_id', + 1, + getEnv().MAX_VOTERID_LENGTH, + 'string' + ) + }); + }); + + it('rejects if election_id and voter_id exist but are not a composite key', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[0]); + const voter_id = dummyAppData.ballots[0].voter_id; + + await expect( + Backend.getBallotForElection({ election_id, voter_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(voter_id, 'ballot') + }); + }); +}); + +describe('::getInfo', () => { + it('returns system information wrt apiVersion and only counting active sessions', async () => { + expect.hasAssertions(); + + await expect(Backend.getInfo()).resolves.toStrictEqual( + toPublicInfo({ + closedElections: dummyAppData.elections.filter( + ({ deleted, closesAt }) => !deleted && closesAt < mockDateNowMs + ).length, + openElections: dummyAppData.elections.filter( + ({ deleted, opensAt, closesAt }) => + !deleted && opensAt < mockDateNowMs && closesAt > mockDateNowMs + ).length, + upcomingElections: dummyAppData.elections.filter( + ({ deleted, opensAt }) => !deleted && opensAt > mockDateNowMs + ).length + }) + ); + }); +}); + +describe('::createElection', () => { + it('creates and returns a new owned election', async () => { + expect.hasAssertions(); + + const newElection: NewElection = { + title: 'title', + description: 'description', + options: ['option-1', 'option-2'], + opensAt: 0, + closesAt: 1 + }; + + const { title, description, options, opensAt, closesAt } = newElection; + + await expect( + Backend.createElection({ data: newElection, provenance }) + ).resolves.toStrictEqual({ + election_id: expect.any(String), + title, + description, + options, + createdAt: Date.now(), + opensAt, + closesAt, + deleted: false, + owned: true + }); + + await expect((await getElectionsDb()).countDocuments(newElection)).resolves.toBe( + 1 + ); + }); + + it('supports creating elections with empty descriptions and options', async () => { + expect.hasAssertions(); + + const newElection: NewElection = { + title: 'title', + description: '', + options: [], + opensAt: 0, + closesAt: 1 + }; + + const { title, description, options, opensAt, closesAt } = newElection; + + await expect( + Backend.createElection({ data: newElection, provenance }) + ).resolves.toStrictEqual({ + election_id: expect.any(String), + title, + description, + options, + createdAt: Date.now(), + opensAt, + closesAt, + deleted: false, + owned: true + }); + + await expect((await getElectionsDb()).countDocuments(newElection)).resolves.toBe( + 1 + ); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.createElection({ + data: { + title: 'title', + description: '', + options: [], + opensAt: 0, + closesAt: 1 + }, + provenance: undefined as unknown as string + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); + + it('rejects if new election would violate the invariant: opensAt < closesAt', async () => { + expect.hasAssertions(); + + await expect( + Backend.createElection({ + provenance, + data: { + title: 'title', + description: '', + options: [], + opensAt: 0, + closesAt: 0 + } + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('opensAt < closesAt') + }); + + await expect( + Backend.createElection({ + provenance, + data: { + title: 'title', + description: '', + options: [], + opensAt: 2, + closesAt: 1 + } + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('opensAt < closesAt') + }); + }); + + it('rejects if new election would violate the invariant: options must be unique', async () => { + expect.hasAssertions(); + + await expect( + Backend.createElection({ + provenance, + data: { + title: 'title', + description: '', + options: ['1', '1'], + opensAt: 0, + closesAt: 1 + } + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('options must be unique') + }); + }); + + it('rejects if data is invalid or contains properties that violate limits', async () => { + expect.hasAssertions(); + + const { + MIN_ELECTION_TITLE_LENGTH: minTitle, + MAX_ELECTION_TITLE_LENGTH: maxTitle, + MAX_ELECTION_DESC_LENGTH: maxDesc, + MAX_ELECTION_OPTIONS_ITEMS: maxOptions, + MAX_ELECTION_OPTION_LENGTH: maxOption + } = getEnv(); + + const newElections: [LiteralUnknownUnion, string][] = [ + [undefined, ErrorMessage.InvalidJSON()], + ['string data', ErrorMessage.InvalidJSON()], + [{}, ErrorMessage.EmptyJSONBody()], + [ + { email: null }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 1 }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 'x'.repeat(minTitle - 1) }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 'x'.repeat(maxTitle + 1) }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 'x'.repeat(maxTitle) }, + ErrorMessage.InvalidStringLength('description', 0, maxDesc, 'string') + ], + [ + { title: 'valid title', description: 1 }, + ErrorMessage.InvalidStringLength('description', 0, maxDesc, 'string') + ], + [ + { title: 'valid title', description: 'x'.repeat(maxDesc + 1) }, + ErrorMessage.InvalidStringLength('description', 0, maxDesc, 'string') + ], + [ + { title: 'valid title', description: 'x'.repeat(maxDesc) }, + ErrorMessage.InvalidFieldValue('options') + ], + [ + { title: 'valid title', description: 'x'.repeat(maxDesc), options: 1 }, + ErrorMessage.InvalidFieldValue('options') + ], + [ + { + title: 'valid title', + description: '', + options: [''] + }, + ErrorMessage.InvalidArrayValue('options', '', 0, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['x'.repeat(maxOption + 1)] + }, + ErrorMessage.InvalidArrayValue('options', 'x'.repeat(maxOption + 1), 0, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option', ''] + }, + ErrorMessage.InvalidArrayValue('options', '', 1, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { + title: 'valid title', + description: 'x'.repeat(maxDesc), + options: ('x'.repeat(maxOption) + ',') + .repeat(maxOptions + 1) + .split(',') + .slice(0, -1) + }, + ErrorMessage.TooMany('options', maxOptions) + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'] + }, + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: '1' + }, + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: -1 + }, + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: 0 + }, + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: 0, + closesAt: '1' + }, + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: 0, + closesAt: -1 + }, + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: 0, + closesAt: 0 + }, + ErrorMessage.InvariantViolation('opensAt < closesAt') + ], + [ + { + title: 'valid title', + description: 'valid description', + options: ['valid option'], + opensAt: 0, + closesAt: 1, + extra: true + }, + ErrorMessage.UnknownField('extra') + ] + ]; + + await expectExceptionsWithMatchingErrors(newElections, (data) => + Backend.createElection({ data, provenance: 'fake-owner' }) + ); + }); +}); + +describe('::upsertBallot', () => { + it('creates a new ballot if voter_id does not exist', async () => { + expect.hasAssertions(); + + const newBallot: NewOrPatchBallot = { + ranking: { a: 1, b: 2 } + }; + + const election_id = itemToStringId(dummyAppData.elections[0]); + const voter_id = 'fake-id'; + + await expect( + Backend.upsertBallot({ election_id, data: newBallot, voter_id, provenance }) + ).resolves.toBeUndefined(); + + await expect((await getBallotsDb()).countDocuments(newBallot)).resolves.toBe(1); + }); + + it('updates an existing ballot if voter_id already exists', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[0]); + const voter_id = 'fake-id'; + + const newBallot: NewOrPatchBallot = { + ranking: { a: 1, b: 2 } + }; + + const updatedBallot: NewOrPatchBallot = { + ranking: { a: 50, b: 100 } + }; + + await expect( + Backend.upsertBallot({ election_id, data: newBallot, voter_id, provenance }) + ).resolves.toBeUndefined(); + + await expect((await getBallotsDb()).countDocuments(newBallot)).resolves.toBe(1); + + await expect( + Backend.upsertBallot({ election_id, data: updatedBallot, voter_id, provenance }) + ).resolves.toBeUndefined(); + + await expect((await getBallotsDb()).countDocuments(updatedBallot)).resolves.toBe( + 1 + ); + + await expect((await getBallotsDb()).countDocuments(newBallot)).resolves.toBe(0); + }); + + it('rejects on provenance mismatch', async () => { + expect.hasAssertions(); + + await expect( + Backend.upsertBallot({ + election_id: itemToStringId(dummyAppData.elections[0]), + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance: 'fake-provenance' + }) + ).rejects.toMatchObject({ + message: ErrorMessage.NotAuthorized() + }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.upsertBallot({ + election_id: itemToStringId(dummyAppData.elections[0]), + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance: undefined as unknown as string + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); + + it('rejects when attempting to add too many ballots', async () => { + expect.hasAssertions(); + + await expect( + Backend.upsertBallot({ + election_id: itemToStringId(dummyAppData.elections[3]), + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.TooMany('ballots', getEnv().MAX_BALLOTS_PER_ELECTION) + }); + }); + + it('rejects if election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + + await expect( + Backend.upsertBallot({ + election_id, + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + + await expect( + Backend.upsertBallot({ + election_id: undefined, + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + await expect( + Backend.upsertBallot({ + election_id: 'invalid', + data: { ranking: { a: 1 } }, + voter_id: 'fake-id', + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); + + it('rejects if voter_id is too long or too short', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + const { MAX_VOTERID_LENGTH } = getEnv(); + + await expect( + Backend.upsertBallot({ + election_id, + data: { ranking: { a: 1 } }, + voter_id: 'x'.repeat(MAX_VOTERID_LENGTH + 1), + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidStringLength( + 'voter_id', + 1, + MAX_VOTERID_LENGTH, + 'string' + ) + }); + + await expect( + Backend.upsertBallot({ + election_id, + data: { ranking: { a: 1 } }, + voter_id: '', + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidStringLength( + 'voter_id', + 1, + MAX_VOTERID_LENGTH, + 'string' + ) + }); + }); + + it('rejects if data is invalid or contains properties that violate limits', async () => { + expect.hasAssertions(); + + const { + MAX_ELECTION_OPTIONS_ITEMS: maxOptions, + MAX_ELECTION_OPTION_LENGTH: maxOption + } = getEnv(); + const election_id = itemToStringId(dummyAppData.elections[0]); + + const newOrPatchBallots: [LiteralUnknownUnion, string][] = [ + [undefined, ErrorMessage.InvalidJSON()], + ['string data', ErrorMessage.InvalidJSON()], + [{}, ErrorMessage.EmptyJSONBody()], + [{ contents: null }, ErrorMessage.InvalidJSON('ranking')], + [{ ranking: 1 }, ErrorMessage.InvalidJSON('ranking')], + [{ ranking: {} }, ErrorMessage.InvalidLength('ranking', 0, 1, maxOptions)], + [ + { + ranking: Object.fromEntries( + [...dummyAppData.elections[3].options, 'plus one'].map((option) => [ + option, + 1 + ]) + ) + }, + ErrorMessage.InvalidLength('ranking', maxOptions + 1, 1, maxOptions) + ], + [ + { ranking: { ['x'.repeat(maxOption + 1)]: 1 } }, + ErrorMessage.InvalidObjectKey('ranking', 'x'.repeat(maxOption + 1), [ + `strings between 1 and ${maxOption} characters` + ]) + ], + [ + { ranking: { '': 1 } }, + ErrorMessage.InvalidObjectKey('ranking', '', [ + `strings between 1 and ${maxOption} characters` + ]) + ], + [ + { ranking: { x: '1' } }, + ErrorMessage.InvalidObjectKeyValue('ranking', '1', [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: 1.5 } }, + ErrorMessage.InvalidObjectKeyValue('ranking', 1.5, [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: undefined } }, + ErrorMessage.InvalidObjectKeyValue('ranking', undefined, [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: -1 } }, + ErrorMessage.InvalidObjectKeyValue('ranking', -1, [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: Number.NaN } }, + ErrorMessage.InvalidObjectKeyValue('ranking', Number.NaN, [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: Number.POSITIVE_INFINITY } }, + ErrorMessage.InvalidObjectKeyValue('ranking', Number.POSITIVE_INFINITY, [ + `safe non-negative integers` + ]) + ], + [ + { ranking: { x: Number.NEGATIVE_INFINITY } }, + ErrorMessage.InvalidObjectKeyValue('ranking', Number.NEGATIVE_INFINITY, [ + `safe non-negative integers` + ]) + ], + [{ ranking: { x: 1 }, extra: true }, ErrorMessage.UnknownField('extra')] + ]; + + await expectExceptionsWithMatchingErrors(newOrPatchBallots, (data, index) => + Backend.upsertBallot({ + election_id, + voter_id: index.toString(), + data, + provenance: 'fake-owner' + }) + ); + }); +}); + +describe('::updateElection', () => { + it('updates an existing election', async () => { + expect.hasAssertions(); + + const electionsDb = await getElectionsDb(); + const electionId = itemToObjectId(dummyAppData.elections[2]); + const election_id = itemToStringId(dummyAppData.elections[2]); + + const patchElection: PatchElection = { + title: 'title', + description: 'description', + options: ['option-1', 'option-2'], + opensAt: 0, + closesAt: 1 + }; + + await expect( + electionsDb.countDocuments({ _id: electionId, ...patchElection }) + ).resolves.toBe(0); + + await expect( + Backend.updateElection({ election_id, data: patchElection, provenance }) + ).resolves.toBeUndefined(); + + await expect( + electionsDb.countDocuments({ _id: electionId, ...patchElection }) + ).resolves.toBe(1); + }); + + it('supports updating election to empty description and options', async () => { + expect.hasAssertions(); + + const electionsDb = await getElectionsDb(); + const electionId = itemToObjectId(dummyAppData.elections[2]); + const election_id = itemToStringId(dummyAppData.elections[2]); + + const patchElection: PatchElection = { + title: '1234', + description: '', + options: [], + opensAt: 0, + closesAt: 1 + }; + + await expect( + electionsDb.countDocuments({ _id: electionId, ...patchElection }) + ).resolves.toBe(0); + + await expect( + Backend.updateElection({ election_id, data: patchElection, provenance }) + ).resolves.toBeUndefined(); + + await expect( + electionsDb.countDocuments({ _id: electionId, ...patchElection }) + ).resolves.toBe(1); + }); + + it('rejects on provenance mismatch', async () => { + expect.hasAssertions(); + + await expect( + Backend.updateElection({ + provenance: 'fake', + election_id: itemToStringId(dummyAppData.elections[0]), + data: { title: 'updated election' } + }) + ).rejects.toMatchObject({ + message: ErrorMessage.NotAuthorized() + }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.updateElection({ + provenance: undefined as unknown as string, + election_id: itemToStringId(dummyAppData.elections[0]), + data: { title: 'updated election' } + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); + + it('rejects if update would violate the invariant: opensAt < closesAt', async () => { + expect.hasAssertions(); + + await expect( + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[2]), + data: { + opensAt: 1, + closesAt: 1 + }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('opensAt < closesAt') + }); + + await expect( + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[0]), + data: { opensAt: dummyAppData.elections[0].closesAt }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('opensAt < closesAt') + }); + + await expect( + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[1]), + data: { closesAt: dummyAppData.elections[1].opensAt }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('opensAt < closesAt') + }); + }); + + it('rejects if new election would violate the invariant: options must be unique', async () => { + expect.hasAssertions(); + + await expect( + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[1]), + data: { options: ['bad', 'bad'] }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvariantViolation('options must be unique') + }); + }); + + it('rejects if no data passed in', async () => { + expect.hasAssertions(); + + await expect( + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[1]), + data: {}, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.EmptyJSONBody() + }); + }); + + it('rejects if the election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + const election_id = new ObjectId().toString(); + + await expect( + Backend.updateElection({ + election_id, + data: { title: 'updated' }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + + await expect( + Backend.updateElection({ + election_id: undefined, + data: { title: 'updated' }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + await expect( + Backend.updateElection({ + election_id: 'invalid', + data: { title: 'updated' }, + provenance + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + }); + + it('rejects if data is invalid or contains properties that violate limits', async () => { + expect.hasAssertions(); + + const { + MIN_ELECTION_TITLE_LENGTH: minTitle, + MAX_ELECTION_TITLE_LENGTH: maxTitle, + MAX_ELECTION_DESC_LENGTH: maxDesc, + MAX_ELECTION_OPTIONS_ITEMS: maxOptions, + MAX_ELECTION_OPTION_LENGTH: maxOption + } = getEnv(); + + const patchElections: [LiteralUnknownUnion, string][] = [ + [undefined, ErrorMessage.InvalidJSON()], + ['string data', ErrorMessage.InvalidJSON()], + [{}, ErrorMessage.EmptyJSONBody()], + [{ email: null }, ErrorMessage.UnknownField('email')], + [ + { title: 1 }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 'x'.repeat(minTitle - 1) }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { title: 'x'.repeat(maxTitle + 1) }, + ErrorMessage.InvalidStringLength('title', minTitle, maxTitle, 'string') + ], + [ + { description: 1 }, + ErrorMessage.InvalidStringLength('description', 0, maxDesc, 'string') + ], + [ + { description: 'x'.repeat(maxDesc + 1) }, + ErrorMessage.InvalidStringLength('description', 0, maxDesc, 'string') + ], + [{ options: 1 }, ErrorMessage.InvalidFieldValue('options')], + [ + { description: '', options: [''] }, + ErrorMessage.InvalidArrayValue('options', '', 0, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { options: ['x'.repeat(maxOption + 1)] }, + ErrorMessage.InvalidArrayValue('options', 'x'.repeat(maxOption + 1), 0, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { options: ['valid option', ''] }, + ErrorMessage.InvalidArrayValue('options', '', 1, [ + `strings of length between 1 and ${maxOption}` + ]) + ], + [ + { + options: ('x'.repeat(maxOption) + ',') + .repeat(maxOptions + 1) + .split(',') + .slice(0, -1) + }, + ErrorMessage.TooMany('options', maxOptions) + ], + [ + { opensAt: '1' }, + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ], + [ + { opensAt: -1 }, + ErrorMessage.InvalidNumberValue('opensAt', 0, null, ' non-negative integer') + ], + [ + { closesAt: '1' }, + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ], + [ + { closesAt: -1 }, + ErrorMessage.InvalidNumberValue('closesAt', 0, null, ' non-negative integer') + ], + [ + { opensAt: 0, closesAt: 0 }, + ErrorMessage.InvariantViolation('opensAt < closesAt') + ] + ]; + + await expectExceptionsWithMatchingErrors(patchElections, (data) => + Backend.updateElection({ + election_id: itemToStringId(dummyAppData.elections[0]), + data, + provenance + }) + ); + }); +}); + +describe('::deleteElection', () => { + it('soft deletes an election', async () => { + expect.hasAssertions(); + + const electionsDb = await getElectionsDb(); + const electionId = itemToObjectId(dummyAppData.elections[0]); + + await expect( + electionsDb.countDocuments({ _id: electionId, deleted: true }) + ).resolves.toBe(0); + + await expect( + Backend.deleteElection({ election_id: electionId.toString(), provenance }) + ).resolves.toBeUndefined(); + + await expect( + electionsDb.countDocuments({ _id: electionId, deleted: true }) + ).resolves.toBe(1); + }); + + it('rejects on provenance mismatch', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteElection({ + election_id: itemToStringId(dummyAppData.elections[0]), + provenance: 'fake' + }) + ).rejects.toMatchObject({ message: ErrorMessage.NotAuthorized() }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteElection({ + election_id: itemToStringId(dummyAppData.elections[0]), + provenance: undefined as unknown as string + }) + ).rejects.toMatchObject({ message: ErrorMessage.BadProvenanceToken() }); + }); + + it('rejects if the election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteElection({ election_id: 'invalid', provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + + await expect( + Backend.deleteElection({ election_id: undefined, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + const election_id = itemToStringId(new ObjectId()); + await expect( + Backend.deleteElection({ election_id, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + }); +}); + +describe('::deleteBallotFromElection', () => { + it('deletes a ballot', async () => { + expect.hasAssertions(); + + const ballotsDb = await getBallotsDb(); + + await expect( + ballotsDb.countDocuments({ + _id: itemToObjectId(dummyAppData.ballots[0]) + }) + ).resolves.toBe(1); + + await expect( + Backend.deleteBallotFromElection({ + provenance, + election_id: itemToStringId(dummyAppData.ballots[0].election_id), + voter_id: dummyAppData.ballots[0].voter_id + }) + ).resolves.toBeUndefined(); + + await expect( + ballotsDb.countDocuments({ + _id: itemToObjectId(dummyAppData.ballots[0]) + }) + ).resolves.toBe(0); + }); + + it('rejects on provenance mismatch', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteBallotFromElection({ + provenance: 'fake', + election_id: itemToStringId(dummyAppData.ballots[0].election_id), + voter_id: dummyAppData.ballots[0].voter_id + }) + ).rejects.toMatchObject({ + message: ErrorMessage.NotAuthorized() + }); + }); + + it('rejects if provenance is not a string', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteBallotFromElection({ + provenance: undefined as unknown as string, + election_id: itemToStringId(dummyAppData.ballots[0].election_id), + voter_id: dummyAppData.ballots[0].voter_id + }) + ).rejects.toMatchObject({ + message: ErrorMessage.BadProvenanceToken() + }); + }); + + it('rejects if the election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + await expect( + Backend.deleteBallotFromElection({ + provenance, + election_id: 'does-not-exist', + voter_id: 'fake' + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('does-not-exist') + }); + + await expect( + Backend.deleteBallotFromElection({ + provenance, + election_id: undefined, + voter_id: 'fake' + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + const election_id = itemToStringId(new ObjectId()); + await expect( + Backend.deleteBallotFromElection({ provenance, election_id, voter_id: 'fake' }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + }); + + it('rejects if the voter_id is undefined or not found', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.ballots[0].election_id); + + await expect( + Backend.deleteBallotFromElection({ + provenance, + election_id, + voter_id: 'fake' + }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound('fake', 'ballot') + }); + + await expect( + Backend.deleteBallotFromElection({ + provenance, + election_id, + voter_id: undefined + }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidStringLength( + 'voter_id', + 1, + getEnv().MAX_VOTERID_LENGTH, + 'string' + ) + }); + }); + + it('rejects if the voter_id and election_id are defined but do not form a valid composite key', async () => { + expect.hasAssertions(); + + const election_id = itemToStringId(dummyAppData.elections[0]); + const voter_id = dummyAppData.ballots[0].voter_id; + + await expect( + Backend.deleteBallotFromElection({ election_id, voter_id, provenance }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(voter_id, 'ballot') + }); + }); +}); + +describe('::superDeleteElectionAndRelatedBallots', () => { + it('permanently deletes an election and all its ballots', async () => { + expect.hasAssertions(); + + const electionsDb = await getElectionsDb(); + const electionId = itemToObjectId(dummyAppData.elections[0]); + + await expect(electionsDb.countDocuments({ _id: electionId })).resolves.toBe(1); + + await expect( + Backend.superDeleteElectionAndRelatedBallots({ + election_id: electionId.toString() + }) + ).resolves.toBeUndefined(); + + await expect(electionsDb.countDocuments({ _id: electionId })).resolves.toBe(0); + }); + + it('rejects if the election_id is undefined, invalid, or not found', async () => { + expect.hasAssertions(); + + await expect( + Backend.superDeleteElectionAndRelatedBallots({ election_id: 'invalid' }) + ).rejects.toMatchObject({ + message: ErrorMessage.InvalidObjectId('invalid') + }); + + await expect( + Backend.superDeleteElectionAndRelatedBallots({ election_id: undefined }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(undefined, 'election') + }); + + const election_id = itemToStringId(new ObjectId()); + await expect( + Backend.superDeleteElectionAndRelatedBallots({ election_id }) + ).rejects.toMatchObject({ + message: ErrorMessage.ItemNotFound(election_id, 'election') + }); + }); +}); diff --git a/test/db.ts b/test/db.ts new file mode 100644 index 0000000..b22fdfe --- /dev/null +++ b/test/db.ts @@ -0,0 +1,254 @@ +import { getEnv } from 'universe/backend/env'; + +import { + dummyRootData, + generateMockSensitiveObjectId, + getCommonDummyData, + mockDateNowMs +} from 'multiverse/mongo-common'; + +import type { DummyData } from 'multiverse/mongo-test'; + +import type { InternalBallot, InternalElection } from 'universe/backend/db'; + +/** + * Returns data used to hydrate databases and their collections. + */ +export function getDummyData(): DummyData { + return getCommonDummyData({ app: dummyAppData }); +} + +/** + * The shape of the application database's test data. + */ +export type DummyAppData = { + _generatedAt: number; + elections: InternalElection[]; + ballots: InternalBallot[]; +}; + +// ! Tests are relying on the XValue variables remaining what they are ! \\ + +export const createdAtHighValue = mockDateNowMs; +export const createdAtMidValue = mockDateNowMs - 10 ** 3; +export const createdAtLowValue = mockDateNowMs - 10 ** 5; +export const opensAtHighValue = mockDateNowMs + 10 ** 3; +export const opensAtMidValue = mockDateNowMs - 10 ** 3; +export const opensAtLowValue = mockDateNowMs - 10 ** 5; +export const closesAtHighValue = mockDateNowMs + 10 ** 4; +export const closesAtMidValue = mockDateNowMs - 10 ** 4; +export const closesAtLowValue = mockDateNowMs - 10 ** 6; + +// ! Order matters in unit and integration tests, so APPEND ONLY ! \\ + +const { + MAX_ELECTION_DESC_LENGTH, + MAX_ELECTION_OPTIONS_ITEMS, + MAX_ELECTION_OPTION_LENGTH, + MAX_ELECTION_TITLE_LENGTH, + MAX_VOTERID_LENGTH, + MAX_BALLOTS_PER_ELECTION +} = getEnv(); + +const elections: InternalElection[] = [ + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + // ? Election #1 (empty) demonstrates minimum title length (4 characters) + title: '#1 E', + description: '', + options: [], + createdAt: createdAtHighValue, + opensAt: opensAtHighValue, + closesAt: closesAtHighValue, + deleted: false + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + title: 'election #2 (deleted)', + description: 'deleted', + options: ['deleted-1', 'deleted-2'], + createdAt: createdAtHighValue, + opensAt: opensAtHighValue, + closesAt: closesAtHighValue, + deleted: true + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + title: 'election #3 (average)', + description: 'election 3 description', + options: ['1', '2', '3'], + createdAt: createdAtHighValue, + opensAt: opensAtHighValue, + closesAt: closesAtHighValue, + deleted: false + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + title: 'election #4 (full) ' + '#'.repeat(MAX_ELECTION_TITLE_LENGTH - 19), + description: 'x'.repeat(MAX_ELECTION_DESC_LENGTH), + options: Array.from({ length: MAX_ELECTION_OPTIONS_ITEMS }).map((_, index) => + index.toString().repeat(MAX_ELECTION_OPTION_LENGTH) + ), + createdAt: createdAtHighValue, + opensAt: opensAtHighValue, + closesAt: closesAtHighValue, + deleted: false + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + title: 'election #5 (open but not closed)', + description: 'election 5 description', + options: ['a', 'b', 'c'], + createdAt: createdAtLowValue, + opensAt: opensAtMidValue, + closesAt: closesAtHighValue, + deleted: false + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + title: 'election #6 (closed)', + description: 'election 6 description', + options: ['z-0', 'y-1', 'x-2', 'w-3', 'v-4', 'u-5'], + createdAt: createdAtHighValue, + opensAt: opensAtLowValue, + closesAt: closesAtMidValue, + deleted: false + } +]; + +const ballots: InternalBallot[] = [ + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[1]._id, + voter_id: 'voter_1', + ranking: { [elections[1].options[0]]: 1, [elections[1].options[1]]: 2 } + }, + + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[2]._id, + voter_id: 'voter_a', + ranking: { + [elections[2].options[0]]: 1, + [elections[2].options[1]]: 2, + [elections[2].options[1]]: 3 + } + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[2]._id, + voter_id: 'voter_b', + ranking: { + [elections[2].options[0]]: 2, + [elections[2].options[1]]: 3, + [elections[2].options[1]]: 1 + } + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[2]._id, + voter_id: 'voter_c', + ranking: { + [elections[2].options[0]]: 2, + [elections[2].options[1]]: 1, + [elections[2].options[2]]: 3 + } + }, + + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[4]._id, + voter_id: 'voter_x', + ranking: { + [elections[4].options[0]]: 1, + [elections[4].options[1]]: 2, + [elections[4].options[2]]: 3 + } + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[4]._id, + voter_id: 'voter_y', + ranking: { + [elections[4].options[0]]: 2, + [elections[4].options[1]]: 3, + [elections[4].options[2]]: 1 + } + }, + + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[5]._id, + voter_id: 'voter_x', + ranking: { + [elections[5].options[0]]: 1, + [elections[5].options[1]]: 2, + [elections[5].options[2]]: 3, + [elections[5].options[3]]: 4, + [elections[5].options[4]]: 5 + } + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[5]._id, + voter_id: 'voter_b', + ranking: { + [elections[5].options[0]]: 2, + [elections[5].options[1]]: 3, + [elections[5].options[2]]: 1, + [elections[5].options[3]]: 4, + [elections[5].options[4]]: 5 + } + }, + { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[5]._id, + voter_id: 'voter_3', + ranking: { + [elections[5].options[2]]: 10, + [elections[5].options[0]]: 20, + fake: 50, + [elections[5].options[1]]: 30 + } + }, + + ...Array.from({ length: MAX_BALLOTS_PER_ELECTION }).map((_, index) => { + const voterId = `generated_${index}_`; + return { + _id: generateMockSensitiveObjectId(), + __provenance: dummyRootData.auth[0].attributes.owner, + election_id: elections[3]._id, + voter_id: `${voterId}${'x'.repeat(MAX_VOTERID_LENGTH - voterId.length)}`, + ranking: Object.fromEntries( + elections[3].options.map((option, optionIndex, options) => [ + option, + index % 2 === 0 ? optionIndex + 1 : options.length - optionIndex + ]) + ) + } satisfies InternalBallot; + }) +]; + +/** + * Test data for the application database. + */ +export const dummyAppData: DummyAppData = { + _generatedAt: mockDateNowMs, + elections, + ballots +}; diff --git a/test/externals/unit-ban.test.ts b/test/externals/unit-ban.test.ts new file mode 100644 index 0000000..828790e --- /dev/null +++ b/test/externals/unit-ban.test.ts @@ -0,0 +1,329 @@ +import { mockDateNowMs, useMockDateNow } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { BANNED_BEARER_TOKEN } from 'multiverse/next-auth'; +import { TrialError } from 'universe/error'; + +import { + mockEnvFactory, + protectedImportFactory, + withMockedOutput +} from 'testverse/setup'; + +import { ObjectId } from 'mongodb'; +import type { InternalLimitedLogEntry } from 'multiverse/next-limit'; +import type { InternalRequestLogEntry } from 'multiverse/next-log'; + +// ? Ensure the isolated external picks up the memory server override +jest.mock('multiverse/mongo-schema', (): typeof import('multiverse/mongo-schema') => { + return jest.requireActual('multiverse/mongo-schema'); +}); + +const TEST_MARGIN_MS = 1000; +const TEN_MINUTES_MS = 10 * 60 * 1000; + +const withMockedEnv = mockEnvFactory({ + NODE_ENV: 'test', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '60', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '100', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '1', + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '5', + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '4' +}); + +const importBanHammer = protectedImportFactory< + typeof import('externals/ban-hammer').default +>({ + path: 'externals/ban-hammer', + useDefault: true +}); + +setupMemoryServerOverride(); +useMockDateNow(); + +const getRequestLogCollection = async () => { + return (await getDb({ name: 'root' })).collection( + 'request-log' + ); +}; + +const getRateLimitsCollection = async () => { + return (await getDb({ name: 'root' })).collection( + 'limited-log' + ); +}; + +const getRateLimits = async () => { + return (await getRateLimitsCollection()) + .find() + .project({ _id: 0, ip: 1, header: 1 }) + .toArray(); +}; + +const getRateLimitUntils = async () => { + return (await getRateLimitsCollection()) + .find() + .project({ _id: 0, until: 1 }) + .toArray(); +}; + +it('is verbose when no DEBUG environment variable set and compiled NODE_ENV is not test', async () => { + expect.hasAssertions(); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + DEBUG: undefined, + NODE_ENV: 'something-else', + OVERRIDE_EXPECT_ENV: 'force-no-check' + }); + + expect(infoSpy).toBeCalledWith(expect.stringContaining('execution complete')); + }); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 })); + expect(infoSpy).not.toHaveBeenCalled(); + }); +}); + +it('rejects on bad environment', async () => { + expect.hasAssertions(); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 2 }), { + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '' + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 2 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '' + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 2 }), { + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '' + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 2 }), { + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '' + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 2 }), { + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '' + }); +}); + +it('rate limits only those ips and auth headers that exceed limits', async () => { + expect.hasAssertions(); + + const now = ((n: number) => n - (n % 5000) - 1000)(mockDateNowMs); + + await (await getRateLimitsCollection()).deleteMany({}); + await ( + await getRequestLogCollection() + ).updateMany({}, { $set: { createdAt: now } }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '1' + }); + + await expect(getRateLimits()).resolves.toIncludeSameMembers([ + { ip: '1.2.3.4' }, + { header: `bearer ${BANNED_BEARER_TOKEN}` } + ]); + + await (await getRateLimitsCollection()).deleteMany({}); + await ( + await getRequestLogCollection() + ).updateMany( + { header: `bearer ${BANNED_BEARER_TOKEN}` }, + { $set: { ip: '9.8.7.6' } } + ); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '1' + }); + + await expect(getRateLimits()).resolves.toIncludeSameMembers([ + { ip: '1.2.3.4' }, + { ip: '9.8.7.6' }, + { header: `bearer ${BANNED_BEARER_TOKEN}` } + ]); + + await (await getRateLimitsCollection()).deleteMany({}); + await ( + await getRequestLogCollection() + ).insertOne({ + _id: new ObjectId(), + ip: '1.2.3.4', + header: `bearer ${BANNED_BEARER_TOKEN}`, + method: 'PUT', + resStatusCode: 200, + route: 'jest/test', + endpoint: '/fake/:jest', + createdAt: now - 1000, + durationMs: 1234 + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '11', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '1' + }); + + await expect(getRateLimits()).resolves.toBeArrayOfSize(0); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '11', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '5' + }); + + await expect(getRateLimits()).resolves.toIncludeSameMembers([ + { ip: '1.2.3.4' }, + { header: `bearer ${BANNED_BEARER_TOKEN}` } + ]); + + await (await getRateLimitsCollection()).deleteMany({}); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '11', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '1' + }); + + await expect(getRateLimits()).resolves.toBeArrayOfSize(0); +}); + +it('rate limits with respect to invocation interval', async () => { + expect.hasAssertions(); + + await (await getRateLimitsCollection()).deleteMany(); + const requestLogDb = await getRequestLogCollection(); + + if ((await requestLogDb.countDocuments()) === 0) { + throw new TrialError('no request-log entries found'); + } + + const now = ((_now: number) => _now - (_now % 5000) - 2000)(mockDateNowMs); + + await requestLogDb.updateMany( + { header: `bearer ${BANNED_BEARER_TOKEN}` }, + { $set: { ip: '9.8.7.6' } } + ); + + await requestLogDb.updateMany({}, { $set: { createdAt: now } }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '5', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '1' + }); + + await expect(getRateLimits()).resolves.toBeArrayOfSize(0); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10', + BAN_HAMMER_RESOLUTION_WINDOW_SECONDS: '5', + BAN_HAMMER_WILL_BE_CALLED_EVERY_SECONDS: '8' + }); + + await expect(getRateLimits()).resolves.toIncludeSameMembers([ + { header: `bearer ${BANNED_BEARER_TOKEN}` }, + { ip: '9.8.7.6' }, + { ip: '1.2.3.4' } + ]); +}); + +it('repeat offenders are punished to the maximum extent', async () => { + expect.hasAssertions(); + + await (await getRateLimitsCollection()).deleteMany({}); + await ( + await getRequestLogCollection() + ).updateMany( + { header: `bearer ${BANNED_BEARER_TOKEN}` }, + { $set: { ip: '9.8.7.6' } } + ); + + const now = mockDateNowMs; + let untils; + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '10', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10' + }); + + untils = await getRateLimitUntils(); + expect(untils).toBeArrayOfSize(3); + + untils.forEach((u) => { + expect(u.until).toBeWithin( + now + TEN_MINUTES_MS - TEST_MARGIN_MS, + now + TEN_MINUTES_MS + TEST_MARGIN_MS + ); + }); + + await (await getRateLimitsCollection()).deleteMany({}); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '20', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10' + }); + + untils = await getRateLimitUntils(); + expect(untils).toBeArrayOfSize(3); + + untils.forEach((u) => { + expect(u.until).toBeWithin( + now + 2 * TEN_MINUTES_MS - TEST_MARGIN_MS, + now + 2 * TEN_MINUTES_MS + TEST_MARGIN_MS + ); + }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 }), { + BAN_HAMMER_DEFAULT_BAN_TIME_MINUTES: '20', + BAN_HAMMER_MAX_REQUESTS_PER_WINDOW: '10', + BAN_HAMMER_RECIDIVISM_PUNISH_MULTIPLIER: '5' + }); + + untils = await getRateLimitUntils(); + expect(untils).toBeArrayOfSize(3); + + untils.forEach((u) => { + expect(u.until).toBeWithin( + now + 10 * TEN_MINUTES_MS - TEST_MARGIN_MS, + now + 10 * TEN_MINUTES_MS + TEST_MARGIN_MS + ); + }); +}); + +it('does not replace longer bans with shorter bans', async () => { + expect.hasAssertions(); + + await expect(getRateLimits()).resolves.toBeArrayOfSize(3); + + await ( + await getRateLimitsCollection() + ).updateMany({ ip: { $ne: '5.6.7.8' } }, { $set: { until: 9_998_784_552_826 } }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 })); + + let saw = 0; + (await getRateLimitUntils()).forEach((u) => u.until === 9_998_784_552_826 && saw++); + + expect(saw).toBe(2); +}); + +it('deletes outdated entries outside the punishment period', async () => { + expect.hasAssertions(); + + await expect(getRateLimits()).resolves.toBeArrayOfSize(3); + + await ( + await getRateLimitsCollection() + ).updateMany({ ip: '5.6.7.8' }, { $set: { until: 0 } }); + + await withMockedEnv(() => importBanHammer({ expectedExitCode: 0 })); + + await expect(getRateLimits()).resolves.toIncludeSameMembers([ + { ip: '1.2.3.4' }, + { header: `bearer ${BANNED_BEARER_TOKEN}` } + ]); +}); diff --git a/test/externals/unit-initialize.test.ts b/test/externals/unit-initialize.test.ts new file mode 100644 index 0000000..a1038d2 --- /dev/null +++ b/test/externals/unit-initialize.test.ts @@ -0,0 +1,68 @@ +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; +import { debugNamespace as namespace } from 'universe/constants'; + +import { + mockEnvFactory, + protectedImportFactory, + withMockedOutput +} from 'testverse/setup'; + +void namespace; + +// ? Ensure the isolated external picks up the memory server override +jest.mock('multiverse/mongo-schema', (): typeof import('multiverse/mongo-schema') => { + return jest.requireActual('multiverse/mongo-schema'); +}); + +const withMockedEnv = mockEnvFactory({ + // ! For max test perf, ensure this next line is commented out unless needed + //DEBUG: `${namespace}:initialize-data,${namespace}:initialize-data:*`, + + // ? Use these to control the options auto-selected for inquirer. Note that + // ? these values must either be empty/undefined or a valid URL query string. + TEST_PROMPTER_INITIALIZER: 'action=commit', + TEST_PROMPTER_FINALIZER: 'action=exit', + + NODE_ENV: 'test', + MONGODB_URI: 'fake' +}); + +const importInitializeData = protectedImportFactory< + typeof import('externals/initialize-data').default +>({ + path: 'externals/initialize-data', + useDefault: true +}); + +setupMemoryServerOverride(); + +it('is verbose when no DEBUG environment variable set and compiled NODE_ENV is not test', async () => { + expect.hasAssertions(); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importInitializeData({ expectedExitCode: 0 }), { + DEBUG: undefined, + NODE_ENV: 'something-else', + OVERRIDE_EXPECT_ENV: 'force-no-check' + }); + + expect(infoSpy.mock.calls.at(-1)?.[0]).toStrictEqual( + expect.stringContaining('execution complete') + ); + }); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importInitializeData({ expectedExitCode: 0 })); + expect(infoSpy).not.toHaveBeenCalled(); + }); +}); + +it('initializes the database as expected', async () => { + expect.hasAssertions(); + + await withMockedOutput(async () => { + await withMockedEnv(() => importInitializeData({ expectedExitCode: 0 }), { + TEST_PROMPTER_INITIALIZER: 'action=commit-force' + }); + }); +}); diff --git a/test/externals/unit-prune.test.ts b/test/externals/unit-prune.test.ts new file mode 100644 index 0000000..d5985f4 --- /dev/null +++ b/test/externals/unit-prune.test.ts @@ -0,0 +1,245 @@ +import { useMockDateNow } from 'multiverse/mongo-common'; +import { getDb } from 'multiverse/mongo-schema'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + mockEnvFactory, + protectedImportFactory, + withMockedOutput +} from 'testverse/setup'; + +import { TrialError, ValidationError } from 'universe/error'; + +// * Follow the steps (below) to tailor these tests to this specific project πŸ˜‰ + +// ? Ensure the isolated external picks up the memory server override +jest.mock('multiverse/mongo-schema', (): typeof import('multiverse/mongo-schema') => { + return jest.requireActual('multiverse/mongo-schema'); +}); + +// // * Step 1: Add new collections here w/ keys of form: database.collection +// ! Only do step 1 if you're using count-based limits and NOT byte-based! +// const testCollectionsMap = { +// 'root.request-log': dummyRootData['request-log'].length, +// 'root.limited-log': dummyRootData['limited-log'].length, +// ... +// }; + +const testCollections = [ + 'root.request-log', + 'root.limited-log', + 'app.elections', + 'app.ballots' +] as const; + +const withMockedEnv = mockEnvFactory({ + NODE_ENV: 'test', + PRUNE_DATA_MAX_LOGS_BYTES: '100mb', + PRUNE_DATA_MAX_BANNED_BYTES: '10mb', + // * Step 2: Add new env var default values here + PRUNE_DATA_MAX_ELECTIONS_BYTES: '200mb', + PRUNE_DATA_MAX_BALLOTS_BYTES: '120mb' +}); + +const importPruneData = protectedImportFactory< + typeof import('externals/prune-data').default +>({ + path: 'externals/prune-data', + useDefault: true +}); + +setupMemoryServerOverride(); +useMockDateNow(); + +/** + * Accepts one or more database and collection names in the form + * `database.collection` and returns the number of documents contained in each + * collection or the size of each collection in bytes depending on the value of + * `metric`. + */ +async function getCollectionSize( + collection: string, + { metric }: { metric: 'count' | 'bytes' } +): Promise; +async function getCollectionSize( + collections: readonly string[], + { metric }: { metric: 'count' | 'bytes' } +): Promise>; +async function getCollectionSize( + collections: string | readonly string[], + { metric }: { metric: 'count' | 'bytes' } +): Promise> { + const targetCollections = [collections].flat(); + const result = Object.assign( + {}, + ...(await Promise.all( + targetCollections.map(async (dbCollection) => { + const [dbName, ...rawCollectionName] = dbCollection.split('.'); + + if (!dbName || rawCollectionName.length !== 1) { + throw new TrialError(`invalid input "${dbCollection}" to countCollection`); + } + + const colDb = (await getDb({ name: dbName })).collection( + rawCollectionName[0] + ); + + if (metric === 'count') { + return colDb.countDocuments().then((count) => ({ [dbCollection]: count })); + } else if (metric === 'bytes') { + return colDb + .aggregate<{ size: number }>([ + { + $group: { + _id: null, + size: { $sum: { $bsonSize: '$$ROOT' } } + } + } + ]) + .next() + .then((r) => ({ + [dbCollection]: r?.size ?? 0 + })); + } else { + throw new ValidationError(`unknown metric "${metric}"`); + } + }) + )) + ); + + const resultLength = Object.keys(result).length; + + if (resultLength !== targetCollections.length) { + throw new TrialError('invalid output from countCollection'); + } + + return resultLength === 1 ? result[collections.toString()] : result; +} + +it('is verbose when no DEBUG environment variable set and compiled NODE_ENV is not test', async () => { + expect.hasAssertions(); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importPruneData({ expectedExitCode: 0 }), { + DEBUG: undefined, + NODE_ENV: 'something-else', + OVERRIDE_EXPECT_ENV: 'force-no-check' + }); + + expect(infoSpy.mock.calls.at(-1)?.[0]).toStrictEqual( + expect.stringContaining('execution complete') + ); + }); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importPruneData({ expectedExitCode: 0 })); + expect(infoSpy).not.toHaveBeenCalled(); + }); +}); + +it('rejects on bad environment', async () => { + expect.hasAssertions(); + + // * Step 3: Add new env vars emptiness tests below + + // ? Remember that withMockedEnv is the result of calling a factory function + // ? with all the PRUNE_DATA_MAX_X env vars already defined. + + await withMockedEnv(() => importPruneData({ expectedExitCode: 2 }), { + PRUNE_DATA_MAX_LOGS_BYTES: '', + PRUNE_DATA_MAX_BANNED_BYTES: '', + PRUNE_DATA_MAX_BALLOTS_BYTES: '', + PRUNE_DATA_MAX_ELECTIONS_BYTES: '' + }); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 2 }), { + PRUNE_DATA_MAX_LOGS_BYTES: '' + }); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 2 }), { + PRUNE_DATA_MAX_BANNED_BYTES: '' + }); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 2 }), { + PRUNE_DATA_MAX_BALLOTS_BYTES: '' + }); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 2 }), { + PRUNE_DATA_MAX_ELECTIONS_BYTES: '' + }); +}); + +// ? This is a bytes-based test. Look elsewhere for the old count-based tests! +it('respects the limits imposed by PRUNE_DATA_MAX_X environment variables', async () => { + expect.hasAssertions(); + + const initialSizes = await getCollectionSize(testCollections, { metric: 'bytes' }); + + // * Step 4: Add new env vars low-prune-threshold tests below. + + const expectedSizes = { + 'root.request-log': initialSizes['root.request-log'] / 2, + 'root.limited-log': initialSizes['root.limited-log'] / 2, + 'app.elections': initialSizes['app.elections'] / 2, + 'app.ballots': initialSizes['app.ballots'] / 2 + }; + + await withMockedEnv(() => importPruneData({ expectedExitCode: 0 }), { + PRUNE_DATA_MAX_LOGS_BYTES: String(expectedSizes['root.request-log']), + PRUNE_DATA_MAX_BANNED_BYTES: String(expectedSizes['root.limited-log']), + PRUNE_DATA_MAX_ELECTIONS_BYTES: String(expectedSizes['app.elections']), + PRUNE_DATA_MAX_BALLOTS_BYTES: String(expectedSizes['app.ballots']) + }); + + const newSizes = await getCollectionSize(testCollections, { metric: 'bytes' }); + + expect(newSizes['root.request-log']).toBeLessThanOrEqual( + expectedSizes['root.request-log'] + ); + + expect(newSizes['root.limited-log']).toBeLessThanOrEqual( + expectedSizes['root.limited-log'] + ); + + expect(newSizes['app.elections']).toBeLessThanOrEqual( + expectedSizes['app.elections'] + ); + expect(newSizes['app.ballots']).toBeLessThanOrEqual(expectedSizes['app.ballots']); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 0 }), { + PRUNE_DATA_MAX_LOGS_BYTES: '1', + PRUNE_DATA_MAX_BANNED_BYTES: '1', + PRUNE_DATA_MAX_ELECTIONS_BYTES: '1', + PRUNE_DATA_MAX_BALLOTS_BYTES: '1' + }); + + const latestSizes = await getCollectionSize(testCollections, { metric: 'bytes' }); + + expect(latestSizes['root.request-log']).toBe(0); + expect(latestSizes['root.limited-log']).toBe(0); + expect(latestSizes['app.elections']).toBe(0); + expect(latestSizes['app.ballots']).toBe(0); +}); + +// ? This is a bytes-based test. Look elsewhere for the old count-based tests! +it('only deletes entries if necessary', async () => { + expect.hasAssertions(); + + const initialSizes = await getCollectionSize(testCollections, { metric: 'bytes' }); + + await withMockedEnv(() => importPruneData({ expectedExitCode: 0 }), { + PRUNE_DATA_MAX_LOGS_BYTES: '100gb', + PRUNE_DATA_MAX_BANNED_BYTES: '100gb', + // * Step 5: Add new env vars high-prune-threshold values here + PRUNE_DATA_MAX_ELECTIONS_BYTES: '100gb', + PRUNE_DATA_MAX_BALLOTS_BYTES: '100gb' + }); + + const newSizes = await getCollectionSize(testCollections, { metric: 'bytes' }); + + expect(newSizes['root.request-log']).toBe(initialSizes['root.request-log']); + expect(newSizes['root.limited-log']).toBe(initialSizes['root.limited-log']); + + expect(newSizes['app.elections']).toBe(initialSizes['app.elections']); + expect(newSizes['app.ballots']).toBe(initialSizes['app.ballots']); +}); diff --git a/test/externals/unit-stats.test.ts b/test/externals/unit-stats.test.ts new file mode 100644 index 0000000..5ecac8f --- /dev/null +++ b/test/externals/unit-stats.test.ts @@ -0,0 +1,64 @@ +import { useMockDateNow } from 'multiverse/mongo-common'; +import { setupMemoryServerOverride } from 'multiverse/mongo-test'; + +import { + mockEnvFactory, + protectedImportFactory, + withMockedOutput +} from 'testverse/setup'; + +// ? Ensure the isolated external picks up the memory server override +jest.mock('multiverse/mongo-schema', (): typeof import('multiverse/mongo-schema') => { + return jest.requireActual('multiverse/mongo-schema'); +}); + +jest.mock( + 'jsonfile', + (): typeof import('jsonfile') => + ({ + readFile: jest.fn().mockReturnValue({}), + writeFile: jest.fn() + }) as unknown as typeof import('jsonfile') +); + +const withMockedEnv = mockEnvFactory({ + NODE_ENV: 'test', + MONGODB_URI: 'fake' +}); + +const importLogStats = protectedImportFactory< + typeof import('externals/log-stats').default +>({ + path: 'externals/log-stats', + useDefault: true +}); + +setupMemoryServerOverride(); +useMockDateNow(); + +it('is verbose when no DEBUG environment variable set and compiled NODE_ENV is not test', async () => { + expect.hasAssertions(); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importLogStats({ expectedExitCode: 0 }), { + DEBUG: undefined, + NODE_ENV: 'something-else', + OVERRIDE_EXPECT_ENV: 'force-no-check' + }); + + expect(infoSpy).toBeCalledWith(expect.stringContaining('execution complete')); + }); + + await withMockedOutput(async ({ infoSpy }) => { + await withMockedEnv(() => importLogStats({ expectedExitCode: 0 })); + expect(infoSpy).not.toHaveBeenCalled(); + }); +}); + +it('rejects on bad environment', async () => { + expect.hasAssertions(); + + await withMockedEnv(() => importLogStats({ expectedExitCode: 2 }), { + MONGODB_URI: '' + }); +}); diff --git a/test/integration.ts b/test/integration.ts new file mode 100644 index 0000000..87213a0 --- /dev/null +++ b/test/integration.ts @@ -0,0 +1,1480 @@ +import debugFactory from 'debug'; +import { ObjectId } from 'mongodb'; +import assert from 'node:assert'; +import { name as pkgName } from 'package'; +import { toss } from 'toss-expression'; + +import { defaultNavLinks } from 'universe/backend'; +import { getEnv } from 'universe/backend/env'; +import { GuruMeditationError, ValidationError } from 'universe/error'; + +// TODO: integration tests are not finished for this repo (sowwy) + +import { + toPublicUser, + type NewPage, + type NewUser, + type PatchBlog, + type PatchPage, + type PatchUser, + type PublicBlog, + type PublicInfo, + type PublicPage, + type PublicPageMetadata, + type PublicUser +} from 'universe/backend/db'; + +import { dummyAppData } from 'testverse/db'; + +import type { NextApiHandlerMixin } from 'testverse/util'; +import type { Promisable } from 'type-fest'; + +// TODO: XXX: turn a lot of this into some kind of package; needs to be generic +// TODO: XXX: enough to handle various use cases though :) Maybe +// TODO: XXX: @xunnamius/fable for the generic version, along with +// TODO: XXX: @xunnamius/fable-next, @xunnamius/fable-next-api (below), +// TODO: XXX: @xunnamius/fable-X plugins. Initial version of @xunnamius/fable +// TODO: XXX: would just be the next API version. + +// TODO: XXX: add an `id` param that allows getResultAt using that `id` (along +// TODO: XXX: with index) + +// TODO: XXX: document functionality: RUN_ONLY='#, ##,###,...' +// TODO: XXX: "fail fast" should be optional + +const debug = debugFactory(`${pkgName}:integration-fixtures`); + +/** + * A single test result stored in `memory`. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type TestResult = { + status: number; + json: T | undefined; +}; + +/** + * Stored results from past fixtures runs made available for future fixtures + * runs via `memory`. + */ +export type TestResultset = TestResult[] & { + /** + * A property containing a mapping between optional test ids and their + * results. + */ + idMap: Record; + /** + * A property containing the most previous resultset. + */ + latest: TestResult; + /** + * Get the HTTP response status and json result from previously run tests by + * index. You can pass a negative index to begin counting backwards from the + * current test. Tests are zero-indexed, i.e. use `getResultAt(0)` to refer to + * the very first resultset. `getResultAt(1)` will return the second + * resultset. `getResultAt(-1)` will return the immediately previous resultset + * (same as what the `latest` property returns). + * + * @param index Specify a previous test result index starting at 1 (not zero!) + */ + getResultAt(index: number): TestResult; + getResultAt(index: number, prop: string): T; + getResultAt(index: string): TestResult; + getResultAt(index: string, prop: string): T; +}; + +/** + * Represents a test that executes an HTTP request and evaluate the response + * for correctness. + */ +export type TestFixture = { + /** + * An optional id that can be used to reference the result from this fixture + * directly as opposed to by index. + * + * @example getResultAt('my-id') === getResultAt(22) + */ + id?: string; + /** + * If `invisible === true`, the test is not counted when generating positional + * fixtures. + * + * @default false + */ + invisible?: boolean; + /** + * The test index X (as in "#X") that is reported to the user when a test + * fails. + */ + displayIndex: number; + /** + * A very brief couple of words added to the end of the test title. + */ + subject?: string; + /** + * The handler under test. + */ + handler?: NextApiHandlerMixin; + /** + * The method of the mock request. + */ + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + /** + * Represents mock "processed" dynamic route components and query params. + */ + params?: + | Record + | (( + previousResults: TestResultset + ) => Promisable>); + /** + * The body of the mock request. Automatically stringified. + */ + body?: + | Record + | ((previousResults: TestResultset) => Promisable>); + /** + * The expected shape of the HTTP response. + */ + response?: { + /** + * The expected response status. If status !== 200, we expect `json.success` + * to be `false`. Otherwise, we expect it to be `true`. All status-related + * checks are skipped if a callback is provided that returns `undefined`. + */ + status?: + | number + | (( + status: number, + previousResults: TestResultset + ) => Promisable); + /** + * The expected JSON response body. No need to test for `success` as that is + * handled automatically (unless a status callback was used and it returned + * `undefined`). Jest async matchers are also supported. All json-related + * checks are skipped if a callback is provided that returns `undefined` or + * `json` itself is `undefined`. + */ + json?: + | Record + | jest.AsymmetricMatcher + | (( + json: Record | undefined, + previousResults: TestResultset + ) => Promisable< + Record | jest.AsymmetricMatcher | undefined + >); + }; +}; + +export function getFixtures(api: typeof import('testverse/util').api): TestFixture[] { + // TODO: delete next line in real implementation + assert(dummyAppData.users[2].username); + + const runOnly = process.env.RUN_ONLY?.split(',') + .flatMap((n) => { + const range = n + .split('-') + .map((m) => Number.parseInt(m)) + .filter((m) => !Number.isNaN(m)); + + const min = Math.min(...range); + const max = Math.max(...range); + + debug(`min: ${min}`); + debug(`max: ${max}`); + debug(`range: ${range}`); + + if (!(0 < min && min <= max && max < Number.POSITIVE_INFINITY)) { + throw new ValidationError(`invalid RUN_ONLY range "${min}-${max}"`); + } else { + const finalRange = Array.from({ length: max - min + 1 }).map( + (_, ndx) => min + ndx + ); + debug(`final range: ${finalRange}`); + return finalRange; + } + }) + .sort((a, b) => a - b); + + // TODO: add headers property to TestFixture + + // * Note: user passwords are their usernames + const fixtures: Omit[] = [ + // * Creating, retrieving, authenticating, updating, and deleting users + { + id: 'user-hillary', + subject: 'create new blogger user "the-hill" (h@hillaryclinton.com)', + handler: api.v1.users, + method: 'POST', + body: { + username: 'the-hill', + email: 'h@hillaryclinton.com', + key: '3ffd270e595ef1e485437d90e788d2965acb602a7412f50760140304f4b1f039998ee471de8ddb7c3115f3dee86ba487a213be9604db0ef23ccb99414e47d452', + salt: 'd63a897a76ece8b9a503913db68c95af', + blogName: 'the-hill', + type: 'blogger' + } satisfies NewUser, + response: { + status: 200, + json: { + user: { + user_id: expect.any(String), + username: 'the-hill', + email: 'h@hillaryclinton.com', + salt: 'd63a897a76ece8b9a503913db68c95af', + type: 'blogger', + banned: false, + blogName: 'the-hill' + } satisfies PublicUser + } + } + }, + { + subject: 'verify user the-hill can be fetched', + handler: api.v1.usersUsernameorid, + params: { usernameOrEmail: 'the-hill' }, + method: 'GET', + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { user: getResultAt('user-hillary', 'user') }; + } + } + }, + { + subject: 'verify the-hill appears in LIFO list of all users', + handler: api.v1.users, + method: 'GET', + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { + users: [ + getResultAt('user-hillary', 'user'), + ...dummyAppData.users + .slice() + .reverse() + .map((u) => toPublicUser(u)) + ] + }; + } + } + }, + { + subject: 'update the-hill (salt, key, and banned)', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'the-hill' }, + body: { + salt: '2a9e8128c6641c2fe7642abd14b09e14', + key: '8df1042284e5cc64ff722e473bba9deebb7ef06927c96a004faa1f4dc60f3b1c01fc42612f495cd91ac7041060860b4626e6a5af04b6e31104e6f896b4e3d153', + banned: true + } satisfies PatchUser, + response: { status: 200 } + }, + { + id: 'updated-user-hillary', + subject: 'verify the-hill was updated', + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: 'the-hill' }, + method: 'GET', + response: { + status: 200, + json: { + user: expect.objectContaining({ + salt: '2a9e8128c6641c2fe7642abd14b09e14', + banned: true + }) + } + } + }, + { + subject: 'authenticate the-hill', + handler: api.v1.usersUsernameoremailAuth, + method: 'POST', + params: { usernameOrEmail: 'the-hill' }, + body: { + key: '8df1042284e5cc64ff722e473bba9deebb7ef06927c96a004faa1f4dc60f3b1c01fc42612f495cd91ac7041060860b4626e6a5af04b6e31104e6f896b4e3d153' + }, + response: { status: 200 } + }, + { + subject: 'authenticate the-hill case-insensitively', + handler: api.v1.usersUsernameoremailAuth, + method: 'POST', + params: { usernameOrEmail: 'the-hill' }, + body: { + key: '8DF1042284E5CC64FF722E473BBA9DEEBB7EF06927C96A004FAA1F4DC60F3B1C01FC42612F495CD91AC7041060860B4626E6A5AF04B6E31104E6F896B4E3D153' + }, + response: { status: 200 } + }, + { + subject: 'attempt to authenticate the-hill with bad key', + handler: api.v1.usersUsernameoremailAuth, + method: 'POST', + params: { usernameOrEmail: 'the-hill' }, + body: { key: 'x' }, + response: { status: 403 } + }, + { + subject: 'attempt to update a non-existent user', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'does-not-exist' }, + body: { username: 'should-not-be-seen' }, + response: { status: 404 } + }, + { + subject: 'attempt to delete a non-existent user', + handler: api.v1.usersUsernameoremail, + method: 'DELETE', + params: { usernameOrEmail: 'does-not-exist' }, + response: { status: 404 } + }, + { + subject: `delete ${dummyAppData.users[0].email}`, + handler: api.v1.usersUsernameoremail, + method: 'DELETE', + params: { usernameOrEmail: dummyAppData.users[0].email }, + response: { status: 200 } + }, + { + subject: `verify ${dummyAppData.users[0].email} is not present in LIFO list of all users`, + handler: api.v1.users, + method: 'GET', + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { + users: [ + getResultAt('updated-user-hillary', 'user'), + ...dummyAppData.users + .slice(1) + .reverse() + .map((u) => toPublicUser(u)) + ] + }; + } + } + }, + { + subject: `verify ${dummyAppData.users[0].email} cannot be fetched`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[0].email }, + method: 'GET', + response: { status: 404 } + }, + { + id: 'user-obama', + subject: 'create new administrator user "baracko" (o@barackobama.com)', + handler: api.v1.users, + method: 'POST', + body: { + username: 'baracko', + email: 'o@barackobama.com', + key: 'ac4ab7f9f19fb198a0e1ec3c3970d8b8a2a47e19127a988c02299807210927dfb915d66af69f4a8b53c7610b31604eed6ebe0273a9dc73831892a86250082ebf', + salt: 'e1a3593dbf0ff964292398251f3b47ad', + type: 'administrator' + } satisfies NewUser, + response: { + status: 200, + json: { + user: { + user_id: expect.any(String), + username: 'baracko', + email: 'o@barackobama.com', + salt: 'e1a3593dbf0ff964292398251f3b47ad', + type: 'administrator' + } satisfies PublicUser + } + } + }, + { + subject: 'attempt to create another user named "baracko"', + handler: api.v1.users, + method: 'POST', + body: { + username: 'baracko', + email: 'xyz@abc.def', + key: 'ac4ab7f9f19fb198a0e1ec3c3970d8b8a2a47e19127a988c02299807210927dfb915d66af69f4a8b53c7610b31604eed6ebe0273a9dc73831892a86250082ebf', + salt: 'e1a3593dbf0ff964292398251f3b47ad', + type: 'blogger', + blogName: 'the-o-blog' + } satisfies NewUser, + response: { status: 400 } + }, + { + subject: 'attempt to create a user with a duplicate email', + handler: api.v1.users, + method: 'POST', + body: { + username: 'xyz-abc', + email: 'o@barackobama.com', + key: 'ac4ab7f9f19fb198a0e1ec3c3970d8b8a2a47e19127a988c02299807210927dfb915d66af69f4a8b53c7610b31604eed6ebe0273a9dc73831892a86250082ebf', + salt: 'e1a3593dbf0ff964292398251f3b47ad', + type: 'administrator' + } satisfies NewUser, + response: { status: 400 } + }, + { + subject: 'attempt to create a user with a duplicate blogName', + handler: api.v1.users, + method: 'POST', + body: ({ getResultAt }) => + ({ + username: 'xyz-abc', + email: 'xyz@abc.com', + key: 'ac4ab7f9f19fb198a0e1ec3c3970d8b8a2a47e19127a988c02299807210927dfb915d66af69f4a8b53c7610b31604eed6ebe0273a9dc73831892a86250082ebf', + salt: 'e1a3593dbf0ff964292398251f3b47ad', + type: 'blogger', + blogName: getResultAt('updated-user-hillary', 'user.blogName') + }) satisfies NewUser, + response: { status: 400 } + }, + { + subject: 'verify baracko can be fetched', + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: 'baracko' }, + method: 'GET', + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { user: getResultAt('user-obama', 'user') }; + } + } + }, + { + subject: 'verify baracko appears in LIFO list of all users', + handler: api.v1.users, + method: 'GET', + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { + users: [ + getResultAt('user-obama', 'user'), + getResultAt('updated-user-hillary', 'user'), + ...dummyAppData.users + .slice(1) + .reverse() + .map((u) => toPublicUser(u)) + ] + }; + } + } + }, + { + subject: 'attempt to update baracko with bad salt', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'baracko' }, + body: { + salt: '2', + key: 'dfb915d66af69f4a8b53c7610b31604eed6e2a47e19127a988c022998072109273831892a86250082ebfbe0273a9dc7ac4ab7f9f19fb198a0e1ec3c3970d8b8a' + } satisfies PatchUser, + response: { status: 400 } + }, + { + subject: 'attempt to update baracko with bad key', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'baracko' }, + body: { + salt: '2a9e8128c6641c2fe7642abd14b09e14', + key: 'a' + } satisfies PatchUser, + response: { status: 400 } + }, + { + subject: 'attempt to update baracko salt without key', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'baracko' }, + body: { salt: '2a9e8128c6641c2fe7642abd14b09e14' } satisfies PatchUser, + response: { status: 400 } + }, + { + subject: 'attempt to update baracko key without salt', + handler: api.v1.usersUsernameoremail, + method: 'PATCH', + params: { usernameOrEmail: 'baracko' }, + body: { + key: 'dfb915d66af69f4a8b53c7610b31604eed6e2a47e19127a988c022998072109273831892a86250082ebfbe0273a9dc7ac4ab7f9f19fb198a0e1ec3c3970d8b8a' + } satisfies PatchUser, + response: { status: 400 } + }, + { + subject: 'verify baracko was not updated', + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: 'baracko' }, + method: 'GET', + response: { + status: 200, + json(_json, { getResultAt }) { + return { user: getResultAt('user-obama', 'user') }; + } + } + }, + { + subject: 'authenticate baracko', + handler: api.v1.usersUsernameoremailAuth, + method: 'POST', + params: { usernameOrEmail: 'baracko' }, + body: { + key: 'ac4ab7f9f19fb198a0e1ec3c3970d8b8a2a47e19127a988c02299807210927dfb915d66af69f4a8b53c7610b31604eed6ebe0273a9dc73831892a86250082ebf' + }, + response: { status: 200 } + }, + { + subject: 'attempt to authenticate baracko with no key', + handler: api.v1.usersUsernameoremailAuth, + method: 'POST', + params: { usernameOrEmail: 'baracko' }, + body: { key: undefined }, + response: { status: 403 } + }, + { + subject: 'attempt to fetch all users in LIFO order using bad after_id', + handler: api.v1.users, + method: 'GET', + params: { after: 'bad-id' }, + response: { status: 400 } + }, + { + subject: 'attempt to fetch all users in LIFO order using non-existent after_id', + handler: api.v1.users, + method: 'GET', + params: { after: new ObjectId().toString() }, + response: { status: 404 } + }, + { + subject: `attempt to update deleted ${dummyAppData.users[0].email}`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[0].email }, + method: 'PATCH', + body: { email: 'some@new.email' }, + response: { status: 404 } + }, + { + subject: `attempt to update ${dummyAppData.users[2].username} using a bad email`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[2].username }, + method: 'PATCH', + body: { email: 'bad email address' }, + response: { status: 400 } + }, + { + subject: `attempt to update ${dummyAppData.users[2].username} using a too-long email`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[2].username }, + method: 'PATCH', + body: { email: 'x'.repeat(getEnv().MAX_USER_EMAIL_LENGTH) + '@aol.com' }, + response: { status: 400 } + }, + { + subject: `attempt to update ${dummyAppData.users[2].username} using a short non-hex salt`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[2].username }, + method: 'PATCH', + body: { salt: 'xyz' }, + response: { status: 400 } + }, + { + subject: `attempt to update ${dummyAppData.users[2].username} using a short non-hex key`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[2].username }, + method: 'PATCH', + body: { key: 'xyz' }, + response: { status: 400 } + }, + { + subject: `attempt to update ${dummyAppData.users[2].username} using an empty request body`, + handler: api.v1.usersUsernameoremail, + params: { usernameOrEmail: dummyAppData.users[2].username }, + method: 'PATCH', + body: {}, + response: { status: 400 } + }, + { + subject: 'fetch all users in LIFO order using pagination', + handler: api.v1.users, + method: 'GET', + params: ({ getResultAt }) => { + return { after: getResultAt('updated-user-hillary', 'user.user_id') }; + }, + response: { + status: 200, + json: { + users: dummyAppData.users + .slice(1) + .reverse() + .map((u) => toPublicUser(u)) + } + } + }, + + // * Retrieving and patching a blog + { + subject: 'fetch the-hill blog', + handler: api.v1.blogsBlogname, + method: 'GET', + params: { blogName: 'the-hill' }, + response: { + status: 200, + json: { + blog: { + name: 'the-hill', + rootPage: 'home', + navLinks: defaultNavLinks, + createdAt: expect.any(Number) + } satisfies PublicBlog + } + } + }, + { + subject: 'update the-hill blog name', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-hill' }, + body: { + name: 'the-new-hill' + } satisfies PatchBlog, + response: { status: 200 } + }, + { + subject: 'update the-new-hill blog rootPage to non-existent page name', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + rootPage: 'dne' + } satisfies PatchBlog, + response: { status: 200 } + }, + { + subject: 'update the-new-hill blog navLinks', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + navLinks: [] + } satisfies PatchBlog, + response: { status: 200 } + }, + { + subject: 'verify the-new-hill blog has been updated', + id: 'updated-blog-hill', + handler: api.v1.blogsBlogname, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + blog: { + name: 'the-new-hill', + rootPage: 'dne', + navLinks: [], + createdAt: expect.any(Number) + } satisfies PublicBlog + } + } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with bad href', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { navLinks: [{ href: null, text: 'illegal' }] }, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with empty href', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + navLinks: [{ href: '', text: 'illegal' }] + } satisfies PatchBlog, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with malformed href', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + navLinks: [{ href: '#illegal', text: 'illegal' }] + } satisfies PatchBlog, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with bad text', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { navLinks: [{ href: 'legal', text: 5 }] }, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with empty text', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + navLinks: [{ href: 'legal', text: '' }] + } satisfies PatchBlog, + response: { status: 400 } + }, + { + subject: + 'attempt to update the-new-hill blog navLinks with empty object element', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { navLinks: [{}] }, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog navLinks with null element', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { navLinks: [null] }, + response: { status: 400 } + }, + { + subject: 'attempt to update the-new-hill blog rootPage to an empty string', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { rootPage: '' }, + response: { status: 400 } + }, + { + subject: 'verify the-new-hill blog has not been updated', + handler: api.v1.blogsBlogname, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: (_json, { getResultAt }) => { + return { blog: getResultAt('updated-blog-hill', 'blog') }; + } + } + }, + { + subject: + 'update the-new-hill blog navLinks with both protocol-relative and page name hrefs with query strings and fragments', + handler: api.v1.blogsBlogname, + method: 'PATCH', + params: { blogName: 'the-new-hill' }, + body: { + navLinks: [ + { href: '//google.com', text: 'google-1' }, + { href: '//google.com#fragment', text: 'google-2' }, + { href: '//google.com?query-string', text: 'google-3' }, + { href: 'page-name?query=string#fragment', text: 'page-1' }, + { href: 'page-name', text: 'page-2' } + ] + } satisfies PatchBlog, + response: { status: 200 } + }, + { + subject: 'verify the-new-hill blog has been updated', + id: 'updated-blog-hill', + handler: api.v1.blogsBlogname, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + blog: { + name: 'the-new-hill', + rootPage: 'dne', + navLinks: [ + { href: '//google.com', text: 'google-1' }, + { href: '//google.com#fragment', text: 'google-2' }, + { href: '//google.com?query-string', text: 'google-3' }, + { href: 'page-name?query=string#fragment', text: 'page-1' }, + { href: 'page-name', text: 'page-2' } + ], + createdAt: expect.any(Number) + } satisfies PublicBlog + } + } + }, + + // * Creating, retrieving, updating, and deleting pages; retrieving the page + // * metadata of a blog + { + subject: 'verify the-new-hill blog page metadata', + id: 'updated-blog-hill', + handler: api.v1.blogsBlogname, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + blog: { + name: 'the-new-hill', + rootPage: 'dne', + navLinks: [ + { href: '//google.com', text: 'google-1' }, + { href: '//google.com#fragment', text: 'google-2' }, + { href: '//google.com?query-string', text: 'google-3' }, + { href: 'page-name?query=string#fragment', text: 'page-1' }, + { href: 'page-name', text: 'page-2' } + ], + createdAt: expect.any(Number) + } satisfies PublicBlog + } + } + }, + { + subject: 'create new the-new-hill blog page "new-page"', + handler: api.v1.blogsBlognamePages, + method: 'POST', + params: { blogName: 'the-new-hill' }, + body: { + name: 'new-page', + contents: '' + } satisfies NewPage, + response: { + status: 200, + json: { + page: { + name: 'new-page', + contents: '', + totalViews: 0, + createdAt: expect.any(Number) + } satisfies PublicPage + } + } + }, + { + subject: + 'create new the-new-hill blog page "new-page-2" with the same contents', + handler: api.v1.blogsBlognamePages, + method: 'POST', + params: { blogName: 'the-new-hill' }, + body: { + name: 'new-page-2', + contents: '' + } satisfies NewPage, + response: { + status: 200, + json: { + page: { + name: 'new-page-2', + contents: '', + totalViews: 0, + createdAt: expect.any(Number) + } satisfies PublicPage + } + } + }, + { + subject: 'attempt to create a new the-new-hill blog page with the same name', + handler: api.v1.blogsBlognamePages, + method: 'POST', + params: { blogName: 'the-new-hill' }, + body: { + name: 'new-page-2', + contents: '# Another New Page' + } satisfies NewPage, + response: { status: 400 } + }, + { + subject: 'verify blog metadata #1', + handler: api.v1.blogsBlognamePages, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + pages: [ + { name: 'new-page-2', totalViews: 0, createdAt: expect.any(Number) }, + { name: 'new-page', totalViews: 0, createdAt: expect.any(Number) }, + { name: 'home', totalViews: 0, createdAt: expect.any(Number) } + ] satisfies PublicPageMetadata[] + } + } + }, + { + subject: 'retrieve page new-page', + handler: api.v1.blogsBlognamePagesPagename, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { + status: 200, + json: { + page: { + name: 'new-page', + createdAt: expect.any(Number), + contents: '', + totalViews: 0 + } satisfies PublicPage + } + } + }, + { + subject: 'attempt to update new-page totalViews with an invalid value', + handler: api.v1.blogsBlognamePagesPagename, + method: 'PATCH', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + body: { totalViews: 1 }, + response: { status: 400 } + }, + { + subject: 'attempt to update new-page contents with a value that is too large', + handler: api.v1.blogsBlognamePagesPagename, + method: 'PATCH', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + body: { + contents: 'x'.repeat(getEnv().MAX_BLOG_PAGE_CONTENTS_LENGTH_BYTES + 1) + } satisfies PatchPage, + response: { status: 400 } + }, + { + subject: 'verify page new-page was not updated', + handler: api.v1.blogsBlognamePagesPagename, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { + status: 200, + json: { + page: { + name: 'new-page', + createdAt: expect.any(Number), + contents: '', + totalViews: 0 + } satisfies PublicPage + } + } + }, + { + subject: 'update new-page contents and totalViews', + handler: api.v1.blogsBlognamePagesPagename, + method: 'PATCH', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + body: { totalViews: 'increment' } satisfies PatchPage, + response: { status: 200 } + }, + { + subject: + 'update new-page contents with a value pushing up against the length limit', + handler: api.v1.blogsBlognamePagesPagename, + method: 'PATCH', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + body: { contents: 'x'.repeat(getEnv().MAX_BLOG_PAGE_CONTENTS_LENGTH_BYTES) }, + response: { status: 200 } + }, + { + subject: 'verify page new-page was updated', + handler: api.v1.blogsBlognamePagesPagename, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { + status: 200, + json: { + page: { + name: 'new-page', + createdAt: expect.any(Number), + contents: 'x'.repeat(getEnv().MAX_BLOG_PAGE_CONTENTS_LENGTH_BYTES), + totalViews: 1 + } satisfies PublicPage + } + } + }, + { + subject: 'verify blog metadata #2', + handler: api.v1.blogsBlognamePages, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + pages: [ + { name: 'new-page-2', totalViews: 0, createdAt: expect.any(Number) }, + { name: 'new-page', totalViews: 1, createdAt: expect.any(Number) }, + { name: 'home', totalViews: 0, createdAt: expect.any(Number) } + ] satisfies PublicPageMetadata[] + } + } + }, + { + subject: 'delete new-page', + handler: api.v1.blogsBlognamePagesPagename, + method: 'DELETE', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { status: 200 } + }, + { + subject: 'verify new-page was deleted', + handler: api.v1.blogsBlognamePagesPagename, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { status: 404 } + }, + { + subject: 'attempt to delete non-existent page', + handler: api.v1.blogsBlognamePagesPagename, + method: 'DELETE', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + response: { status: 404 } + }, + { + subject: 'attempt to update non-existent page', + handler: api.v1.blogsBlognamePagesPagename, + method: 'PATCH', + params: { blogName: 'the-new-hill', pageName: 'new-page' }, + body: { totalViews: 'increment' } satisfies PatchPage, + response: { status: 404 } + }, + { + subject: 'verify blog metadata #3', + handler: api.v1.blogsBlognamePages, + method: 'GET', + params: { blogName: 'the-new-hill' }, + response: { + status: 200, + json: { + pages: [ + { name: 'new-page-2', totalViews: 0, createdAt: expect.any(Number) }, + { name: 'home', totalViews: 0, createdAt: expect.any(Number) } + ] satisfies PublicPageMetadata[] + } + } + }, + + // * Creating, updating (renewing), and deleting (manually expiring) + // * sessions; retrieving a page's active session count + { + subject: 'verify no sessions associated with new-page', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { active: 0 } + } + }, + { + id: 'session-new', + subject: 'create new session associated with new-page-2 #1', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'POST', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { session_id: expect.any(String) } + } + }, + { + subject: 'verify sessions associated with new-page-2', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { active: 1 } + } + }, + { + subject: 'renew the new session', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'PUT', + params: ({ getResultAt }) => ({ + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: getResultAt('session-new', 'session_id') + }), + response: { status: 200 } + }, + { + subject: 'create new session associated with new-page-2 #2', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'POST', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { session_id: expect.any(String) } + } + }, + { + subject: 'create new session associated with new-page-2 #3', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'POST', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { session_id: expect.any(String) } + } + }, + { + subject: 'verify sessions associated with new-page-2', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { active: 3 } + } + }, + { + subject: 'delete the first new session', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'DELETE', + params: ({ getResultAt }) => ({ + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: getResultAt('session-new', 'session_id') + }), + response: { status: 200 } + }, + { + subject: 'verify sessions associated with new-page-2', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { active: 2 } + } + }, + { + subject: 'attempt to delete non-existent session', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'DELETE', + params: ({ getResultAt }) => ({ + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: getResultAt('session-new', 'session_id') + }), + response: { status: 404 } + }, + { + subject: 'attempt to renew non-existent session', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'PUT', + params: ({ getResultAt }) => ({ + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: getResultAt('session-new', 'session_id') + }), + response: { status: 404 } + }, + { + subject: 'attempt to delete session using bad session_id', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'DELETE', + params: { + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: 'bad' + }, + response: { status: 400 } + }, + { + subject: 'attempt to renew session using bad session_id', + handler: api.v1.blogsBlognamePagesPagenameSessionsSessionid, + method: 'PUT', + params: { + blogName: 'the-new-hill', + pageName: 'new-page-2', + session_id: 'bad' + }, + response: { status: 400 } + }, + { + subject: 'verify sessions associated with new-page-2', + handler: api.v1.blogsBlognamePagesPagenameSessions, + method: 'GET', + params: { blogName: 'the-new-hill', pageName: 'new-page-2' }, + response: { + status: 200, + json: { active: 2 } + } + }, + + // * Reported system information actively updates in response to API events + { + id: 'initial-info', + subject: 'verify reported system information #1', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 1, + pages: dummyAppData.info[0].pages + 2, + users: dummyAppData.info[0].users + 1 + } satisfies PublicInfo + } + } + }, + { + subject: 'add administrator user "test-admin"', + handler: api.v1.users, + method: 'POST', + body: { + username: 'test-admin', + email: 'test-admin@email.com', + key: '60140304f4b1f039998ee471de8ddb7c3102a7412f50787a213be9604db0ef23ccb99414e47d45215f3dee86ba43ffd270e595ef1e485437d90e788d2965acb6', + salt: 'e8b9a503913db68c95afd63a897a76ec', + type: 'administrator' + } satisfies NewUser, + response: { status: 200 } + }, + { + subject: 'verify reported system information #2', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 1, + pages: dummyAppData.info[0].pages + 2, + users: dummyAppData.info[0].users + 2 + } satisfies PublicInfo + } + } + }, + { + subject: 'add blogger user "test-blogger"', + handler: api.v1.users, + method: 'POST', + body: { + username: 'test-blogger', + email: 'test-blogger@email.com', + key: '60140304f4b1f039998ee471de8ddb7c3102a7412f50787a213be9604db0ef23ccb99414e47d45215f3dee86ba43ffd270e595ef1e485437d90e788d2965acb6', + salt: 'e8b9a503913db68c95afd63a897a76ec', + type: 'blogger', + blogName: 'test-blogger' + } satisfies NewUser, + response: { status: 200 } + }, + { + subject: 'verify reported system information #3', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 2, + pages: dummyAppData.info[0].pages + 3, + users: dummyAppData.info[0].users + 3 + } satisfies PublicInfo + } + } + }, + { + subject: 'add page "test-page" to test-blogger', + handler: api.v1.blogsBlognamePages, + method: 'POST', + params: { blogName: 'test-blogger' }, + body: { name: 'test-page', contents: '# Test Blog' } satisfies NewPage, + response: { status: 200 } + }, + { + subject: 'verify reported system information #4', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 2, + pages: dummyAppData.info[0].pages + 4, + users: dummyAppData.info[0].users + 3 + } satisfies PublicInfo + } + } + }, + { + subject: 'attempt to add duplicate user', + handler: api.v1.users, + method: 'POST', + params: { blogName: 'test-blogger' }, + body: { + username: 'test-blogger', + email: 'test-blogger@email.com', + key: '60140304f4b1f039998ee471de8ddb7c3102a7412f50787a213be9604db0ef23ccb99414e47d45215f3dee86ba43ffd270e595ef1e485437d90e788d2965acb6', + salt: 'e8b9a503913db68c95afd63a897a76ec', + type: 'blogger', + blogName: 'test-blogger' + } satisfies NewUser, + response: { status: 400 } + }, + { + subject: 'attempt to add duplicate page', + handler: api.v1.blogsBlognamePages, + method: 'POST', + params: { blogName: 'test-blogger' }, + body: { name: 'test-page', contents: '# Test Blog' } satisfies NewPage, + response: { status: 400 } + }, + { + subject: 'verify reported system information #5', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 2, + pages: dummyAppData.info[0].pages + 4, + users: dummyAppData.info[0].users + 3 + } satisfies PublicInfo + } + } + }, + { + subject: 'attempt to remove non-existent page', + handler: api.v1.blogsBlognamePagesPagename, + method: 'DELETE', + params: { blogName: 'test-blogger', pageName: 'non-existent-page' }, + response: { status: 404 } + }, + { + subject: 'attempt to remove non-existent user', + handler: api.v1.usersUsernameoremail, + method: 'DELETE', + params: { usernameOrEmail: 'does-not-exist' }, + response: { status: 404 } + }, + { + subject: 'verify reported system information #6', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 2, + pages: dummyAppData.info[0].pages + 4, + users: dummyAppData.info[0].users + 3 + } satisfies PublicInfo + } + } + }, + { + subject: 'remove test-page from test-blogger', + handler: api.v1.blogsBlognamePagesPagename, + method: 'DELETE', + params: { blogName: 'test-blogger', pageName: 'test-page' }, + response: { status: 200 } + }, + { + subject: 'verify reported system information #7', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 2, + pages: dummyAppData.info[0].pages + 3, + users: dummyAppData.info[0].users + 3 + } satisfies PublicInfo + } + } + }, + { + subject: 'remove test-blogger user', + handler: api.v1.usersUsernameoremail, + method: 'DELETE', + params: { usernameOrEmail: 'test-blogger' }, + response: { status: 200 } + }, + { + subject: 'remove test-admin user', + handler: api.v1.usersUsernameoremail, + method: 'DELETE', + params: { usernameOrEmail: 'test-admin' }, + response: { status: 200 } + }, + { + subject: 'verify reported system information has returned to initial values', + handler: api.v1.info, + method: 'GET', + response: { + status: 200, + json: { + info: { + blogs: dummyAppData.info[0].blogs + 1, + pages: dummyAppData.info[0].pages + 2, + users: dummyAppData.info[0].users + 1 + } satisfies PublicInfo + } + } + } + ]; + + // TODO: XXX: ability to specify "depends" via index or name/id vv + // TODO: XXX: ability to specify "dependents" key to reverse-map the above ^^ + + const willSkipFixture = (fixture: (typeof fixtures)[number]) => { + const shouldSkip = + !fixture.subject || + !fixture.handler || + !fixture.method || + (!fixture.invisible && + (!fixture.response || + !['number', 'function'].includes(typeof fixture.response.status))); + + return shouldSkip; + }; + + const filteredFixtures = fixtures.filter( + (fixture, ndx): fixture is TestFixture => { + const displayIndex = ndx + 1; + + if (runOnly && !runOnly.includes(displayIndex)) { + return false; + } + + (fixture as TestFixture).displayIndex = !runOnly + ? displayIndex + : runOnly.shift() ?? + toss(new GuruMeditationError('ran out of RUN_ONLY indices')); + + return true; + } + ); + + // TODO: XXX: add ability to capture/suppress output via fixture option (even better: selectively use mock plugins like withMockEnv and withMockOutput via config options) + + // TODO: XXX: with @xunnamius/fable, have an "every X" type construct (the below is "every reqPerContrived") + // TODO: XXX: also allow middleware + // TODO: XXX: also custom props for fixtures + + const reqPerContrived = getEnv().REQUESTS_PER_CONTRIVED_ERROR; + + if (reqPerContrived) { + for (let index = 0, noSkipCount = 0; index < filteredFixtures.length; index++) { + if ( + !willSkipFixture(filteredFixtures[index]) && + noSkipCount++ % reqPerContrived === 0 + ) { + filteredFixtures.splice(index, 0, { + displayIndex: -1, + subject: 'handle contrived', + handler: api.v1.users, + method: 'POST', + body: {}, + response: { + status: 555, + json: { error: expect.stringContaining('contrived') } + } + }); + } + } + } + + return filteredFixtures; +} diff --git a/test/pages/unit-app.test.tsx b/test/pages/unit-app.test.tsx new file mode 100644 index 0000000..fb48712 --- /dev/null +++ b/test/pages/unit-app.test.tsx @@ -0,0 +1,20 @@ +/** + * @jest-environment jsdom + */ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from 'universe/pages/_app'; + +import type { AppProps } from 'next/app'; + +it('renders without crashing', async () => { + expect.hasAssertions(); + + render( +
Hello, world!
} as unknown as AppProps)} + /> + ); + + expect(screen.getByText('Hello, world!')).toBeInTheDocument(); +}); diff --git a/test/pages/unit-index.test.tsx b/test/pages/unit-index.test.tsx new file mode 100644 index 0000000..ae8e6e3 --- /dev/null +++ b/test/pages/unit-index.test.tsx @@ -0,0 +1,18 @@ +/** + * @jest-environment jsdom + */ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import IndexPage, { getServerSideProps } from 'universe/pages/index'; + +it('renders without crashing', async () => { + expect.hasAssertions(); + + const serverSideProps = (await getServerSideProps()).props; + + render(); + expect(screen.getByText('no')).toBeInTheDocument(); + + render(); + expect(screen.getByText('yes')).toBeInTheDocument(); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..fdd8435 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,1111 @@ +import execa from 'execa'; +import { debugFactory } from 'multiverse/debug-extended'; +import { promises as fs } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { resolve } from 'node:path'; +import { name as pkgName, version as pkgVersion } from 'package'; +import gitFactory from 'simple-git'; +import { toss } from 'toss-expression'; +import uniqueFilename from 'unique-filename'; +import { defaultConfig } from 'universe/backend/api'; +import { debugNamespace } from 'universe/constants'; +import { TrialError, ValidationError, makeNamedError } from 'universe/error'; +import { verifyEnvironment } from '../expect-env'; + +import '@testing-library/jest-dom'; +// ? See: https://github.com/jest-community/jest-extended#setup +import 'jest-extended'; +import 'jest-extended/all'; + +import type { + NextApiHandler, + NextApiRequest, + NextApiResponse, + PageConfig +} from 'next'; + +import type { Debugger } from 'multiverse/debug-extended'; +import type { SimpleGit } from 'simple-git'; +import type { Promisable } from 'type-fest'; + +const { writeFile, access: accessFile } = fs; +const debug = debugFactory(`${debugNamespace}:jest-setup`); + +debug(`pkgName: "${pkgName}"`); +debug(`pkgVersion: "${pkgVersion}"`); + +let env = {}; + +try { + require('node:fs').accessSync('.env'); + env = require('dotenv').config().parsed; + debug('.env vars: %O', env); +} catch (error) { + debug(`.env support disabled; reason: ${error}`); +} + +verifyEnvironment(); + +/** + * A mock Next.js API handler that sends an empty object Reponse with a 200 + * status code. + */ +export const noopHandler = async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).send({}); +}; + +/** + * This function wraps mock Next.js API handler functions so that they provide + * the default (or a custom) API configuration object. + */ +export const wrapHandler = (pagesHandler: NextApiHandler, config?: PageConfig) => { + const api = async (req: NextApiRequest, res: NextApiResponse) => + pagesHandler(req, res); + api.config = config || defaultConfig; + return api; +}; + +// TODO: XXX: add these brand new tools to where they're supposed to be! + +export class FactoryExhaustionError extends TrialError {} +export function itemFactory(testItems: T[]) { + const nextItem = Object.assign( + () => { + const next = nextItem['$iter'].next() as IteratorResult; + if (next.done) { + throw new FactoryExhaustionError( + 'item factory iterator exhausted unexpectedly' + ); + } else return next.value; + }, + { + items: testItems, + count: testItems.length, + $iter: testItems.values(), + *[Symbol.iterator]() { + while (true) { + try { + yield nextItem(); + } catch (error) { + if (error instanceof FactoryExhaustionError) return; + else throw error; + } + } + }, + async *[Symbol.asyncIterator]() { + while (true) { + try { + // eslint-disable-next-line no-await-in-loop + yield await nextItem(); + } catch (error) { + if (error instanceof FactoryExhaustionError) return; + else throw error; + } + } + } + } + ); + + Object.defineProperty(nextItem, 'length', { + configurable: false, + enumerable: false, + set: () => + toss(new SyntaxError('did you mean to use ::count instead of ::length?')), + get: () => + toss(new SyntaxError('did you mean to use ::count instead of ::length?')) + }); + + return nextItem; +} + +// TODO: XXX: make this into a separate (mock-argv) package (along w/ the below) +export type MockArgvOptions = { + /** + * By default, the first two elements in `process.argv` are preserved. Setting + * `replace` to `true` will cause the entire process.argv array to be replaced + * @default false + */ + replace?: boolean; +}; + +// TODO: XXX: make this into a separate (mock-env) package (along w/ the below) +export type MockEnvOptions = { + /** + * By default, the `process.env` object is emptied and re-hydrated with + * `newEnv`. Setting `replace` to `false` will cause `newEnv` to be appended + * instead + * @default true + */ + replace?: boolean; +}; + +// TODO: XXX: make this into a separate (mock-argv) package +export async function withMockedArgv( + fn: () => unknown, + newArgv: string[], + options?: MockArgvOptions +) { + const { replace = false } = options || {}; + + // ? Take care to preserve the original argv array reference in memory + const previousArgv = process.argv.splice(replace ? 0 : 2, process.argv.length); + process.argv.push(...newArgv); + + try { + await fn(); + } finally { + process.argv.splice(replace ? 0 : 2, process.argv.length); + process.argv.push(...previousArgv); + } +} + +// TODO: XXX: make this into a separate (mock-argv) package (along w/ the above) +export function mockArgvFactory( + newArgv: typeof process.argv, + options?: MockArgvOptions +) { + const factoryNewArgv = newArgv; + const factoryOptions = { replace: false, ...options }; + + return (fn: () => unknown, newArgv?: string[], options?: MockArgvOptions) => { + return withMockedArgv( + fn, + [...factoryNewArgv, ...(newArgv || [])], + options || factoryOptions + ); + }; +} + +// TODO: XXX: make this into a separate (mock-env) package +export async function withMockedEnv( + fn: () => unknown, + newEnv: Record, + options?: MockEnvOptions +) { + const { replace = true } = options || {}; + const previousEnv = { ...process.env }; + const clearEnv = () => + Object.getOwnPropertyNames(process.env).forEach( + (prop) => delete process.env[prop] + ); + + // ? Take care to preserve the original env object reference in memory + if (replace) clearEnv(); + Object.assign(process.env, newEnv); + + try { + await fn(); + } finally { + clearEnv(); + Object.assign(process.env, previousEnv); + } +} + +// TODO: XXX: make this into a separate (mock-env) package (along w/ the above) +export function mockEnvFactory( + newEnv: Record, + options?: MockEnvOptions +) { + const factoryNewEnv = newEnv; + const factoryOptions = options; + + return ( + fn: () => unknown, + newEnv?: Record, + options?: MockEnvOptions + ) => { + const env = { ...factoryNewEnv, ...newEnv }; + const options_ = { ...options, ...factoryOptions }; + + // ? The process.env proxy casts undefineds to strings, so we'll delete them + Object.keys(env).forEach((key) => env[key] === undefined && delete env[key]); + return withMockedEnv(fn, env as Record, options_); + }; +} + +// TODO: XXX: make this into a separate (jest-isolated-import) package +export async function withDebugEnabled(fn: () => Promisable) { + const namespaces = debugFactory.disable(); + debugFactory.enable('*'); + + try { + await fn(); + } finally { + debugFactory.disable(); + debugFactory.enable(namespaces); + } +} + +// TODO: XXX: make this into a separate (jest-isolated-import) package + +/** + * Performs a module import as if it were being imported for the first time. + * + * Note that this function breaks the "require caching" expectation of Node.js + * modules. Problems can arise, for example, when closing an app-wide database + * connection in your test cleanup phase and expecting it to close for the + * isolated module too. In this case, the isolated module has its own isolated + * "app-wide" connection that would not actually be closed and could cause your + * test to hang unexpectedly, even when all tests pass. + */ +export function isolatedImport({ + path, + useDefault +}: { + /** + * Path to the module to import. Module resolution is handled by `require`. + */ + path: string; + /** + * By default, only if `module.__esModule === true`, the default export will + * be returned instead. Use `useDefault` to override this behavior in either + * direction. + */ + useDefault?: boolean; +}) { + let pkg: T | undefined; + + // ? Cache-busting + jest.isolateModules(() => { + debug( + `performing isolated import of ${path}${ + useDefault ? ' (returning default by force)' : '' + }` + ); + + pkg = ((r) => { + return r.default && + (useDefault === true || + (useDefault !== false && r.__esModule && Object.keys(r).length === 1)) + ? r.default + : r; + })(require(path)); + }); + + return pkg as T; +} + +// TODO: XXX: make this into a separate package (along with the above) +export function isolatedImportFactory({ + path, + useDefault +}: { + /** + * Path to the module to import. Module resolution is handled by `require`. + */ + path: string; + /** + * By default, only if `module.__esModule === true`, the default export will + * be returned instead. Use `useDefault` to override this behavior in either + * direction. + */ + useDefault?: boolean; +}) { + return () => isolatedImport({ path: path, useDefault: useDefault }); +} + +// TODO: XXX: make this into a separate package (along with the above) +/** + * While `isolatedImport` performs a module import as if it were being + * imported for the first time, `protectedImport` wraps `isolatedImport` + * with `withMockedExit`. This makes `protectedImport` useful for testing + * IIFE modules such as CLI entry points an externals. + */ +export async function protectedImport({ + path, + useDefault, + expectedExitCode +}: { + /** + * Path to the module to import. Module resolution is handled by `require`. + */ + path: string; + /** + * By default, only if `module.__esModule === true`, the default export will + * be returned instead. Use `useDefault` to override this behavior in either + * direction. + */ + useDefault?: boolean; + /** + * The code that must be passed to process.exit by the imported module. If + * `undefined` (default), then process.exit must not be called. + * + * @default undefined + */ + expectedExitCode?: number | 'non-zero' | undefined; +}) { + let pkg: unknown = undefined; + + await withMockedExit(async ({ exitSpy }) => { + try { + pkg = await isolatedImport({ path: path, useDefault: useDefault }); + } catch (error) { + if (!(error instanceof MockedProcessExit)) { + throw error; + } + } + + if (expect) { + expectedExitCode === 'non-zero' + ? expect(exitSpy).not.toBeCalledWith(0) + : expectedExitCode === undefined + ? expect(exitSpy).not.toHaveBeenCalled() + : expect(exitSpy).toBeCalledWith(expectedExitCode); + } else { + debug.warn('"expect" object not found, so exit check was skipped'); + } + }); + + return pkg as T; +} + +// TODO: XXX: make this into a separate package (along with the above) +export function protectedImportFactory({ + path, + useDefault +}: { + /** + * Path to the module to import. Module resolution is handled by `require`. + */ + path: string; + /** + * By default, only if `module.__esModule === true`, the default export will + * be returned instead. Use `useDefault` to override this behavior in either + * direction. + */ + useDefault?: boolean; + /** + * The code that must be passed to process.exit by the imported module. If + * `undefined` (default), then process.exit must not be called. + * + * @default undefined + */ + expectedExitCode?: number | 'non-zero' | undefined; +}) { + return async (params?: { + /** + * The code that must be passed to process.exit by the imported module. If + * `undefined` (default), then process.exit must not be called. + * + * @default undefined + */ + expectedExitCode?: number | 'non-zero' | undefined; + }) => { + return protectedImport({ + path: path, + useDefault: useDefault, + expectedExitCode: params?.expectedExitCode + }); + }; +} + +// TODO: XXX: make this into a separate (mock-exit) package +/** + * Represents a call to process.exit that has been mocked by `withMockedExit`. + */ +export class MockedProcessExit extends Error { + /** + * Represents a call to process.exit that has been mocked by `withMockedExit`. + */ + constructor(public readonly code?: number) { + super(`process.exit was called with code ${code}`); + } +} + +makeNamedError(MockedProcessExit, 'MockedProcessExit'); + +// TODO: XXX: make this into a separate (mock-exit) package +export async function withMockedExit( + fn: (spies: { exitSpy: jest.SpyInstance }) => unknown +) { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation((code) => { + // ? Give a helping hand when debugging, just in case + if (process.env.VSCODE_INSPECTOR_OPTIONS) { + process.emitWarning( + `process.exit(${code}) was called, but the function is mocked. A MockedProcessExit error will be thrown instead.`, + 'MockedProcessExitWarning' + ); + } + + throw new MockedProcessExit((code as number) || undefined); + }); + + try { + await fn({ exitSpy }); + } catch (error) { + if (!(error instanceof MockedProcessExit)) { + throw error; + } + } finally { + exitSpy.mockRestore(); + } +} + +// TODO: XXX: make this into a separate (mock-output) package +/** + * Any output generated within `fn` will be captured by an output spy instead of + * emitting to the console (stdout/stderr). + * + * However, not that `stdErrSpy` is set to passthrough mode by default. If + * desired, use the `passthrough` option to prevent this. + */ +export async function withMockedOutput( + fn: (spies: { + logSpy: jest.SpyInstance; + warnSpy: jest.SpyInstance; + errorSpy: jest.SpyInstance; + infoSpy: jest.SpyInstance; + stdoutSpy: jest.SpyInstance; + stdErrSpy: jest.SpyInstance; + }) => unknown, + options?: { + /** + * Determine if spies provide mock implementations for output functions, + * thus preventing any output to the terminal, or if spies should + * passthrough output as normal. + * + * Passthrough is disabled for all spies by default (except `stdErrSpy`). + * Pass `true` to enable passthrough for a specific spy. + */ + passthrough?: { + /** + * @default false + */ + logSpy?: boolean; + /** + * @default false + */ + warnSpy?: boolean; + /** + * @default false + */ + errorSpy?: boolean; + /** + * @default false + */ + infoSpy?: boolean; + /** + * @default false + */ + stdoutSpy?: boolean; + /** + * @default true + */ + stdErrSpy?: boolean; + }; + } +) { + const logSpy = jest.spyOn(console, 'log'); + const warnSpy = jest.spyOn(console, 'warn'); + const errorSpy = jest.spyOn(console, 'error'); + const infoSpy = jest.spyOn(console, 'info'); + const stdoutSpy = jest.spyOn(process.stdout, 'write'); + const stdErrSpy = jest.spyOn(process.stderr, 'write'); + + !options?.passthrough?.logSpy && logSpy.mockImplementation(() => undefined); + !options?.passthrough?.warnSpy && warnSpy.mockImplementation(() => undefined); + !options?.passthrough?.errorSpy && errorSpy.mockImplementation(() => undefined); + !options?.passthrough?.infoSpy && infoSpy.mockImplementation(() => undefined); + !options?.passthrough?.stdoutSpy && stdoutSpy.mockImplementation(() => true); + options?.passthrough?.stdErrSpy === false && + stdErrSpy.mockImplementation(() => true); + + try { + await fn({ + logSpy, + warnSpy, + errorSpy, + infoSpy, + stdoutSpy, + stdErrSpy + }); + } finally { + logSpy.mockRestore(); + warnSpy.mockRestore(); + errorSpy.mockRestore(); + infoSpy.mockRestore(); + stdoutSpy.mockRestore(); + stdErrSpy.mockRestore(); + } +} + +export function mockOutputFactory(options: { + /** + * Determine if spies provide mock implementations for output functions, + * thus preventing any output to the terminal, or if spies should + * passthrough output as normal. + * + * Passthrough is disabled for all spies by default (except `stdErrSpy`). + * Pass `true` to enable passthrough for a specific spy. + */ + passthrough?: { + /** + * @default false + */ + logSpy?: boolean; + /** + * @default false + */ + warnSpy?: boolean; + /** + * @default false + */ + errorSpy?: boolean; + /** + * @default false + */ + infoSpy?: boolean; + /** + * @default false + */ + stdoutSpy?: boolean; + /** + * @default true + */ + stdErrSpy?: boolean; + }; +}) { + const factoryOptions = options; + + return async ( + fn: (spies: { + logSpy: jest.SpyInstance; + warnSpy: jest.SpyInstance; + errorSpy: jest.SpyInstance; + infoSpy: jest.SpyInstance; + stdoutSpy: jest.SpyInstance; + stdErrSpy: jest.SpyInstance; + }) => unknown, + options?: { + /** + * Determine if spies provide mock implementations for output functions, + * thus preventing any output to the terminal, or if spies should + * passthrough output as normal. + * + * Passthrough is disabled for all spies by default (except `stdErrSpy`). + * Pass `true` to enable passthrough for a specific spy. + */ + passthrough?: { + /** + * @default false + */ + logSpy?: boolean; + /** + * @default false + */ + warnSpy?: boolean; + /** + * @default false + */ + errorSpy?: boolean; + /** + * @default false + */ + infoSpy?: boolean; + /** + * @default false + */ + stdoutSpy?: boolean; + /** + * @default true + */ + stdErrSpy?: boolean; + }; + } + ) => { + return withMockedOutput(fn, { ...factoryOptions, ...options }); + }; +} + +// TODO: XXX: make this into a separate (run) package (along w/ below) +export interface RunOptions extends execa.Options { + /** + * Setting this to `true` rejects the promise instead of resolving it with the error. + * @default false + */ + reject?: boolean; +} + +// TODO: XXX: make this into a separate (run) package +// ! By default, does NOT reject on bad exit code (set reject: true to override) +export async function run(file: string, args?: string[], options?: RunOptions) { + debug(`executing "${file}" with:`); + debug(` args: %O`, args); + debug(` runner options %O`, options); + + const result = await execa(file, args, { reject: false, ...options }); + debug('execution result: %O', result); + + return result; +} + +// TODO: XXX: make this into a separate (run) package (along w/ above) +export function runnerFactory(file: string, args?: string[], options?: RunOptions) { + const factoryArgs = args; + const factoryOptions = options; + + return (args?: string[], options?: RunOptions) => + run(file, args || factoryArgs, { ...factoryOptions, ...options }); +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface FixtureOptions + extends Partial, + Partial, + Partial { + performCleanup: boolean; + use: MockFixture[]; + initialFileContents: { [filePath: string]: string }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface WebpackTestFixtureOptions { + webpackVersion: string; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface GitRepositoryFixtureOptions { + setupGit: (git: SimpleGit) => unknown; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface DummyDirectoriesFixtureOptions { + directoryPaths: string[]; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +// eslint-disable-next-line @typescript-eslint/ban-types +export interface FixtureContext = {}> + extends Partial, + Partial, + Partial { + root: string; + testIdentifier: string; + options: FixtureOptions & CustomOptions; + using: MockFixture[]; + fileContents: { [filePath: string]: string }; + debug: Debugger; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface TestResultProvider { + testResult: { exitCode: number; stdout: string; stderr: string }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface TreeOutputProvider { + treeOutput: string; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface GitProvider { + git: SimpleGit; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +// eslint-disable-next-line @typescript-eslint/ban-types +export type FixtureAction = ( + context_: Context +) => Promise; + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export type ReturnsString = ( + context_: Context +) => Promise | string; + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export interface MockFixture { + name: 'root' | 'describe-root' | string | ReturnsString | symbol; + description: string | ReturnsString; + setup?: FixtureAction; + teardown?: FixtureAction; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function rootFixture(): MockFixture { + return { + name: 'root', // ? If first isn't named root, root used automatically + description: (context_) => + `creating a unique root directory${ + context_.options.performCleanup && + ' (will be deleted after all tests complete)' + }`, + setup: async (context_) => { + context_.root = uniqueFilename(tmpdir(), context_.testIdentifier); + + await run('mkdir', ['-p', context_.root], { reject: true }); + await run('mkdir', ['-p', 'src'], { cwd: context_.root, reject: true }); + }, + teardown: async (context_) => + context_.options.performCleanup && + run('rm', ['-rf', context_.root], { reject: true }) + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function dummyNpmPackageFixture(): MockFixture { + return { + name: 'dummy-npm-package', + description: 'creating package.json file and node_modules subdirectory', + setup: async (context_) => { + await Promise.all([ + writeFile( + `${context_.root}/package.json`, + (context_.fileContents['package.json'] = + context_.fileContents['package.json'] || '{"name":"dummy-pkg"}') + ), + run('mkdir', ['-p', 'node_modules'], { cwd: context_.root, reject: true }) + ]); + + if (pkgName.includes('/')) { + await run('mkdir', ['-p', pkgName.split('/')[0]], { + cwd: `${context_.root}/node_modules`, + reject: true + }); + } + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function npmLinkSelfFixture(): MockFixture { + return { + name: 'npm-link-self', + description: + 'soft-linking project repo into node_modules to emulate package installation', + setup: async (context_) => { + await run('ln', ['-s', resolve(`${__dirname}/..`), pkgName], { + cwd: `${context_.root}/node_modules`, + reject: true + }); + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function webpackTestFixture(): MockFixture { + return { + name: 'webpack-test', + description: 'setting up webpack jest integration test', + setup: async (context_) => { + if (typeof context_.options.webpackVersion !== 'string') { + throw new ValidationError( + 'invalid or missing options.webpackVersion, expected string' + ); + } + + const indexPath = Object.keys(context_.fileContents).find((path) => + /^src\/index\.(((c|m)?js)|ts)x?$/.test(path) + ); + + if (!indexPath) + throw new ValidationError( + 'could not find initial contents for src/index file' + ); + + if (!context_.fileContents['webpack.config.js']) + throw new ValidationError( + 'could not find initial contents for webpack.config.js file' + ); + + await Promise.all([ + writeFile(`${context_.root}/${indexPath}`, context_.fileContents[indexPath]), + writeFile( + `${context_.root}/webpack.config.js`, + context_.fileContents['webpack.config.js'] + ) + ]); + + context_.treeOutput = await getTreeOutput(context_); + + await run( + 'npm', + ['install', `webpack@${context_.options.webpackVersion}`, 'webpack-cli'], + { + cwd: context_.root, + reject: true + } + ); + + await run('npx', ['webpack'], { cwd: context_.root, reject: true }); + + const { exitCode, stdout, stderr } = await run('node', [ + `${context_.root}/dist/index.js` + ]); + + context_.testResult = { + exitCode, + stdout, + stderr + }; + } + }; +} + +async function getTreeOutput(context_: FixtureContext) { + return (await execa('tree', ['-a'], { cwd: context_.root })).stdout; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function nodeImportTestFixture(): MockFixture { + return { + name: 'node-import-test', + description: 'setting up node import jest integration test', + setup: async (context_) => { + const indexPath = Object.keys(context_.fileContents).find((path) => + /^src\/index\.(((c|m)?js)|ts)x?$/.test(path) + ); + + if (!indexPath) + throw new ValidationError( + 'could not find initial contents for src/index file' + ); + + await writeFile( + `${context_.root}/${indexPath}`, + context_.fileContents[indexPath] + ); + + context_.treeOutput = await getTreeOutput(context_); + + const { exitCode, stdout, stderr } = await run( + 'node', + ['--experimental-json-modules', indexPath], + { cwd: context_.root } + ); + + context_.testResult = { + exitCode, + stdout, + stderr + }; + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function gitRepositoryFixture(): MockFixture { + return { + name: 'git-repository', + description: 'configuring fixture root to be a git repository', + setup: async (context_) => { + if ( + context_.options.setupGit && + typeof context_.options.setupGit !== 'function' + ) { + throw new ValidationError( + 'invalid or missing options.setupGit, expected function' + ); + } + + context_.git = gitFactory({ baseDir: context_.root }); + + await (context_.options.setupGit + ? context_.options.setupGit(context_.git) + : context_.git + .init() + .addConfig('user.name', 'fake-user') + .addConfig('user.email', 'fake@email')); + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function dummyDirectoriesFixture(): MockFixture { + return { + name: 'dummy-directories', + description: 'creating dummy directories under fixture root', + setup: async (context_) => { + if (!Array.isArray(context_.options.directoryPaths)) { + throw new ValidationError( + 'invalid or missing options.directoryPaths, expected array' + ); + } + + await Promise.all( + context_.options.directoryPaths.map((path) => + run('mkdir', ['-p', path], { cwd: context_.root, reject: true }) + ) + ); + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +export function dummyFilesFixture(): MockFixture { + return { + name: 'dummy-files', + description: 'creating dummy files under fixture root', + setup: async (context_) => { + await Promise.all( + Object.entries(context_.fileContents).map(async ([path, contents]) => { + const fullPath = `${context_.root}/${path}`; + await accessFile(fullPath).then( + () => + debug(`skipped creating dummy file: file already exists at ${path}`), + async () => { + debug(`creating dummy file "${path}" with contents:`); + debug.extend('contents >')(contents); + await writeFile(fullPath, (context_.fileContents[path] = contents)); + } + ); + }) + ); + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ below) +// ? If a fixture w/ this name isn't included, it's appended +// ! This fixture, when included, is always run even when errors occur! +export function describeRootFixture(): MockFixture { + return { + name: 'describe-root', + description: 'outputting debug information about environment', + setup: async (context_) => { + context_.debug('test identifier: %O', context_.testIdentifier); + context_.debug('root: %O', context_.root); + context_.debug(context_.treeOutput || (await getTreeOutput(context_))); + context_.debug('per-file contents: %O', context_.fileContents); + } + }; +} + +// TODO: XXX: make this into a separate (mock-fixture) package +export async function withMockedFixture< + // eslint-disable-next-line @typescript-eslint/ban-types + CustomOptions extends Record = {}, + // eslint-disable-next-line @typescript-eslint/ban-types + CustomContext extends Record = {} +>({ + fn, + testIdentifier, + options +}: { + fn: FixtureAction< + FixtureContext< + FixtureOptions & Partial & CustomOptions> + > & + CustomContext + >; + testIdentifier: string; + options?: Partial; +}) { + type CustomizedFixtureOptions = FixtureOptions & + Partial & CustomOptions>; + type CustomizedFixtureContext = FixtureContext & + CustomContext; + type CustomizedMockFixture = MockFixture; + + const testSymbol = Symbol('test'); + const finalOptions = { + performCleanup: true, + use: [] as MockFixture[], + initialFileContents: {}, + ...options + } as CustomizedFixtureOptions & { use: CustomizedMockFixture[] }; + + // TODO: + // @ts-expect-error: TODO: fix this + const context_ = { + root: '', + testIdentifier, + debug, + using: [] as MockFixture[], + options: finalOptions, + fileContents: { ...finalOptions.initialFileContents } + } as CustomizedFixtureContext & { using: CustomizedMockFixture[] }; + + if (finalOptions.use) { + if (finalOptions.use?.[0]?.name !== 'root') context_.using.push(rootFixture()); + context_.using = [...context_.using, ...finalOptions.use]; + // ? `describe-root` fixture doesn't have to be the last one, but a fixture + // ? with that name must be included at least once + if (!finalOptions.use.some((f) => f.name === 'describe-root')) + context_.using.push(describeRootFixture()); + } else context_.using = [rootFixture(), describeRootFixture()]; + + context_.using.push({ + name: testSymbol, + description: '', + setup: fn + }); + + let ranDescribe = false; + const cleanupFunctions: NonNullable[] = []; + + const setupDebugger = async (fixture: CustomizedMockFixture, error = false) => { + const toString = async ( + p: CustomizedMockFixture['name'] | CustomizedMockFixture['description'] + ) => + typeof p === 'function' + ? p(context_) + : typeof p === 'string' + ? p + : ':impossible:'; + const name = await toString(fixture.name.toString()); + const desc = await toString(fixture.description); + const dbg = debug.extend(error ? `${name}:` : name); + context_.debug = dbg; + dbg(desc); + }; + + /*eslint-disable no-await-in-loop */ + try { + for (const mockFixture of context_.using) { + if (mockFixture.name === testSymbol) { + context_.debug = debug; + debug('executing test callback'); + } else { + await setupDebugger(mockFixture); + if (mockFixture.teardown) cleanupFunctions.push(mockFixture.teardown); + } + + mockFixture.setup + ? await mockFixture.setup(context_) + : context_.debug('(warning: mock fixture has no setup function)'); + + if (mockFixture.name === 'describe-root') ranDescribe = true; + } + } catch (error) { + context_.debug.extend('')('exception occurred: %O', error); + throw error; + } finally { + if (!ranDescribe) { + const fixture = describeRootFixture(); + await setupDebugger(fixture, true); + await fixture.setup?.(context_); + } + + context_.debug = debug.extend(''); + + for (const cfn of cleanupFunctions.reverse()) { + await cfn(context_).catch((error) => + context_.debug( + `ignored exception in teardown function: ${ + error?.message || error.toString() || '' + }` + ) + ); + } + } + /*eslint-enable no-await-in-loop */ +} + +// TODO: XXX: make this into a separate (mock-fixture) package (along w/ above) +export function mockFixtureFactory< + // eslint-disable-next-line @typescript-eslint/ban-types + CustomOptions extends Record = {}, + // eslint-disable-next-line @typescript-eslint/ban-types + CustomContext extends Record = {} +>(testIdentifier: string, options?: Partial) { + return ( + fn: FixtureAction< + FixtureContext< + FixtureOptions & Partial & CustomOptions> + > & + CustomContext + > + ) => + withMockedFixture({ fn, testIdentifier, options }); +} diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 0000000..34c6782 --- /dev/null +++ b/test/util.ts @@ -0,0 +1,152 @@ +import { asMockedFunction } from '@xunnamius/jest-types'; + +import { + createElection, + deleteBallotFromElection, + deleteElection, + getAllBallotsForElection, + getAllElections, + getBallotForElection, + getElection, + getInfo, + superDeleteElectionAndRelatedBallots, + updateElection, + upsertBallot +} from 'universe/backend'; + +import CatchAllForNotFoundEndpoint, { + config as CatchAllForNotFoundConfig, + metadata as CatchAllForNotFoundMetadata +} from 'universe/pages/api/[[...catchAllForNotFound]]'; + +import V1EndpointElections, { + config as V1ConfigElections, + metadata as V1MetadataElections +} from 'universe/pages/api/v1/elections'; + +import V1EndpointElectionsElectionId, { + config as V1ConfigElectionsElectionId, + metadata as V1MetadataElectionsElectionId +} from 'universe/pages/api/v1/elections/[election_id]'; + +import V1EndpointElectionsElectionIdBallots, { + config as V1ConfigElectionsElectionIdBallots, + metadata as V1MetadataElectionsElectionIdBallots +} from 'universe/pages/api/v1/elections/[election_id]/ballots'; + +import V1EndpointElectionsElectionIdBallotsVoterId, { + config as V1ConfigElectionsElectionIdBallotsVoterId, + metadata as V1MetadataElectionsElectionIdBallotsVoterId +} from 'universe/pages/api/v1/elections/[election_id]/ballots/[voter_id]'; + +import V1EndpointInfo, { + config as V1ConfigInfo, + metadata as V1MetadataInfo +} from 'universe/pages/api/v1/info'; + +import type { NextApiHandler, PageConfig } from 'next'; +import type { PublicBallot, PublicElection, PublicInfo } from 'universe/backend/db'; + +export type NextApiHandlerMixin = NextApiHandler & { + config?: PageConfig; + uri: string; +}; + +// TODO: make a package that automatically generates/regenerates this file !!! + +/** + * The entire live API topology gathered together into one convenient object. + */ +export const api = { + catchAllForNotFound: CatchAllForNotFoundEndpoint as NextApiHandlerMixin, + v1: { + elections: V1EndpointElections as NextApiHandlerMixin, + electionsElectionId: V1EndpointElectionsElectionId as NextApiHandlerMixin, + electionsElectionIdBallots: + V1EndpointElectionsElectionIdBallots as NextApiHandlerMixin, + electionsElectionIdBallotsVoterId: + V1EndpointElectionsElectionIdBallotsVoterId as NextApiHandlerMixin, + + info: V1EndpointInfo as NextApiHandlerMixin + } +}; + +// ** ** +// ** Add configuration objects ** +// ** ** + +api.catchAllForNotFound.config = CatchAllForNotFoundConfig; + +api.v1.elections.config = V1ConfigElections; +api.v1.electionsElectionId.config = V1ConfigElectionsElectionId; +api.v1.electionsElectionIdBallots.config = V1ConfigElectionsElectionIdBallots; +api.v1.electionsElectionIdBallotsVoterId.config = + V1ConfigElectionsElectionIdBallotsVoterId; + +api.v1.info.config = V1ConfigInfo; + +// ** ** +// ** Add metadata descriptors ** +// ** ** + +api.catchAllForNotFound.uri = CatchAllForNotFoundMetadata.descriptor; + +api.v1.elections.uri = V1MetadataElections.descriptor; +api.v1.electionsElectionId.uri = V1MetadataElectionsElectionId.descriptor; +api.v1.electionsElectionIdBallots.uri = + V1MetadataElectionsElectionIdBallots.descriptor; +api.v1.electionsElectionIdBallotsVoterId.uri = + V1MetadataElectionsElectionIdBallotsVoterId.descriptor; + +api.v1.info.uri = V1MetadataInfo.descriptor; + +/** + * A convenience function that mocks the entire backend and returns the mock + * functions. Uses `beforeEach` under the hood. + * + * **WARNING: YOU MUST CALL `jest.mock('universe/backend')` before calling this + * function!** + */ +export function setupMockBackend() { + const mockedCreateElection = asMockedFunction(createElection); + const mockedDeleteBallotFromElection = asMockedFunction(deleteBallotFromElection); + const mockedDeleteElection = asMockedFunction(deleteElection); + const mockedGetAllBallotsForElection = asMockedFunction(getAllBallotsForElection); + const mockedGetAllElections = asMockedFunction(getAllElections); + const mockedGetBallotForElection = asMockedFunction(getBallotForElection); + const mockedGetElection = asMockedFunction(getElection); + const mockedGetInfo = asMockedFunction(getInfo); + const mockedSuperDeleteElectionAndRelatedBallots = asMockedFunction( + superDeleteElectionAndRelatedBallots + ); + const mockedUpdateElection = asMockedFunction(updateElection); + const mockedUpsertBallot = asMockedFunction(upsertBallot); + + beforeEach(() => { + mockedCreateElection.mockReturnValue(Promise.resolve({} as PublicElection)); + mockedDeleteBallotFromElection.mockReturnValue(Promise.resolve()); + mockedDeleteElection.mockReturnValue(Promise.resolve()); + mockedGetAllBallotsForElection.mockReturnValue(Promise.resolve([])); + mockedGetAllElections.mockReturnValue(Promise.resolve([])); + mockedGetBallotForElection.mockReturnValue(Promise.resolve({} as PublicBallot)); + mockedGetElection.mockReturnValue(Promise.resolve({} as PublicElection)); + mockedGetInfo.mockReturnValue(Promise.resolve({} as PublicInfo)); + mockedSuperDeleteElectionAndRelatedBallots.mockReturnValue(Promise.resolve()); + mockedUpdateElection.mockReturnValue(Promise.resolve()); + mockedUpsertBallot.mockReturnValue(Promise.resolve()); + }); + + return { + mockedCreateElection, + mockedDeleteBallotFromElection, + mockedDeleteElection, + mockedGetAllBallotsForElection, + mockedGetAllElections, + mockedGetBallotForElection, + mockedGetElection, + mockedGetInfo, + mockedSuperDeleteElectionAndRelatedBallots, + mockedUpdateElection, + mockedUpsertBallot + }; +} diff --git a/tsconfig.docs.json b/tsconfig.docs.json new file mode 100644 index 0000000..119b8be --- /dev/null +++ b/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/**/*", "lib/**/*", "src/**/*", "packages/**/*"] +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..e1c9902 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*", "**/.*"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4dd2cf0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,58 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "baseUrl": ".", + "checkJs": false, + // ? Only for Next.js + "jsx": "preserve", + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "inlineSourceMap": true, + "isolatedModules": true, + "lib": [ + "ESNext", + "DOM", + //"WebWorker.ImportScripts", + "DOM.Iterable" + ], + "module": "esnext", + "moduleResolution": "node", + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "paths": { + // ! If changed, also update these aliases in jest.config.js, + // ! webpack.config.js, and .eslintrc.js + "externals/*": ["external-scripts/*"], + "multiverse/*": ["lib/*"], + "package": ["package.json"], + "pkgverse/*": ["packages/*"], + "testverse/*": ["test/*"], + "types/*": ["types/*"], + "universe/*": ["src/*"], + // ? These are used at various points (including at compile time by + // ? Next.js) to get mongo schema configuration and/or test dummy data. + // ! Must be defined if using @xunnamius/mongo-schema + "configverse/get-schema-config": ["src/backend/db.ts"], + // ! Must be defined if using @xunnamius/mongo-test + "configverse/get-dummy-data": ["test/db.ts"] + }, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext" + }, + "exclude": ["node_modules"], + "include": [ + "types/**/*", + "lib/**/*", + "src/**/*", + "test/**/*", + "external-scripts/**/*", + "packages/**/*" + ] +} diff --git a/tsconfig.lint.json b/tsconfig.lint.json new file mode 100644 index 0000000..569f104 --- /dev/null +++ b/tsconfig.lint.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "types/**/*", + "lib/**/*", + "src/**/*", + "test/**/*", + "external-scripts/**/*", + "packages/**/*" + ] +} diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000..0763785 --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "declaration": true, + "emitDeclarationOnly": true, + "isolatedModules": false, + "noEmit": false, + "outDir": "dist/types", + "rootDir": "./" + }, + "extends": "./tsconfig.json", + "include": ["types/**/*", "lib/**/*", "src/**/*"] +} diff --git a/types/global.ts b/types/global.ts new file mode 100644 index 0000000..3f7bc11 --- /dev/null +++ b/types/global.ts @@ -0,0 +1,20 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { LiteralUnion } from 'type-fest'; + +// TODO: add this to @xunnamius/types + +/** + * An extension of type-fest's {@link LiteralUnion} that functions with + * `unknown` as `BaseType`. + * + * The point of this function is to make intellisense suggestions available for + * parameters that expect a certain shape but initially accept any (i.e. + * `unknown`) shape. For example, a function that accepts user input where said + * function asserts its input parameter is of a specific type, even though the + * argument passed through that parameter could technically be of any type. + */ +export type LiteralUnknownUnion = + | LiteralType + | (unknown & Record) + | null + | undefined; diff --git a/types/next__bundle-analyzer.d.ts b/types/next__bundle-analyzer.d.ts new file mode 100644 index 0000000..f13ba32 --- /dev/null +++ b/types/next__bundle-analyzer.d.ts @@ -0,0 +1 @@ +declare module '@next/bundle-analyzer'; diff --git a/types/random-case.d.ts b/types/random-case.d.ts new file mode 100644 index 0000000..54c2dc7 --- /dev/null +++ b/types/random-case.d.ts @@ -0,0 +1,3 @@ +declare module 'random-case' { + export default function (originalString: string): string; +} diff --git a/types/unique-filename.d.ts b/types/unique-filename.d.ts new file mode 100644 index 0000000..5b77ba2 --- /dev/null +++ b/types/unique-filename.d.ts @@ -0,0 +1,7 @@ +declare module 'unique-filename' { + export default function ( + dir: string, + filePrefix?: string, + uniqStr?: string + ): string; +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..bff83cb --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "trailingSlash": false +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..8055892 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,198 @@ +'use strict'; + +// This webpack config is used to transpile src to dist, compile externals, +// compile executables, etc + +const { EnvironmentPlugin, DefinePlugin, BannerPlugin } = require('webpack'); +const { verifyEnvironment } = require('./expect-env'); +const nodeExternals = require('webpack-node-externals'); +const debug = require('debug')(`${require('./package.json').name}:webpack-config`); + +const IMPORT_ALIASES = { + universe: `${__dirname}/src/`, + multiverse: `${__dirname}/lib/`, + testverse: `${__dirname}/test/`, + externals: `${__dirname}/external-scripts/`, + types: `${__dirname}/types/`, + package: `${__dirname}/package.json`, + // ? These are used at various points (including at compile time by + // ? Next.js) to get mongo schema configuration and/or test dummy data. + // ! Must be defined if using @xunnamius/mongo-schema + 'configverse/get-schema-config': `${__dirname}/src/backend/db.ts`, + // ! Must be defined if using @xunnamius/mongo-test + 'configverse/get-dummy-data': `${__dirname}/test/db.ts` +}; + +let sanitizedEnv = {}; +let { NODE_ENV: nodeEnv, ...sanitizedProcessEnv } = { + ...process.env, + NODE_ENV: 'production' +}; + +try { + require('node:fs').accessSync('.env'); + const { NODE_ENV: forceEnv, ...parsedEnv } = require('dotenv').config().parsed; + nodeEnv = forceEnv || nodeEnv; + sanitizedEnv = parsedEnv; + debug(`NODE_ENV: ${nodeEnv}`); + debug('sanitized .env vars: %O', sanitizedEnv); +} catch (error) { + debug(`.env support disabled; reason: ${error}`); +} + +debug('sanitized process env: %O', sanitizedProcessEnv); +verifyEnvironment(); + +const envPlugins = [ + // ? NODE_ENV is not a "default" (unlike below) but an explicit overwrite + new DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(nodeEnv) + }), + // ? Load our .env results as the defaults (overridden by process.env) + new EnvironmentPlugin({ ...sanitizedEnv, ...sanitizedProcessEnv }), + // ? Create shim process.env for undefined vars + // ! The above already replaces all process.env.X occurrences in the code + // ! first, so plugin order is important here + new DefinePlugin({ 'process.env': '{}' }) +]; + +const externals = [ + nodeExternals(), + ({ request }, callback) => + // ? Externalize all .json imports (required as commonjs modules) + /\.json$/.test(request) ? callback(null, `commonjs ${request}`) : callback() +]; + +/* const libConfig = { + name: 'lib', + mode: 'production', + target: 'node', + node: false, + + entry: `${__dirname}/src/index.ts`, + + output: { + filename: 'index.js', + path: `${__dirname}/dist`, + // ! β–Ό Only required for libraries + // ! β–Ό Note: ESM outputs are handled by Babel ONLY! + libraryTarget: 'commonjs2' + }, + + externals, + externalsPresets: { node: true }, + + stats: { + orphanModules: true, + providedExports: true, + usedExports: true, + errorDetails: true + }, + + resolve: { + extensions: ['.ts', '.wasm', '.mjs', '.cjs', '.js', '.json'], + // ! If changed, also update these aliases in tsconfig.json, + // ! jest.config.js, next.config.ts, and .eslintrc.js + alias: IMPORT_ALIASES + }, + module: { + rules: [{ test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: /node_modules/ }] + }, + optimization: { usedExports: true }, + plugins: [...envPlugins] +}; */ + +const externalsConfig = { + name: 'externals', + mode: 'production', + target: 'node', + node: false, + + entry: { + 'ban-hammer': `${__dirname}/external-scripts/ban-hammer.ts`, + 'prune-data': `${__dirname}/external-scripts/prune-data.ts`, + 'log-stats': `${__dirname}/external-scripts/log-stats.ts`, + 'initialize-data': `${__dirname}/external-scripts/initialize-data/index.ts` + // 'simulate-activity': `${__dirname}/external-scripts/simulate-activity.ts` + }, + + output: { + filename: '[name].js', + path: `${__dirname}/external-scripts/bin` + }, + + externals, + externalsPresets: { node: true }, + + stats: { + orphanModules: true, + providedExports: true, + usedExports: true, + errorDetails: true + }, + + resolve: { + extensions: ['.ts', '.wasm', '.mjs', '.cjs', '.js', '.json'], + // ! If changed, also update these aliases in tsconfig.json, + // ! jest.config.js, next.config.ts, and .eslintrc.js + alias: IMPORT_ALIASES + }, + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: 'babel-loader' + } + ] + }, + optimization: { usedExports: true }, + plugins: [ + ...envPlugins, + // * β–Ό For non-bundled externals, make entry file executable w/ shebang + new BannerPlugin({ banner: '#!/usr/bin/env node', raw: true, entryOnly: true }) + ] +}; + +/* const cliConfig = { + name: 'cli', + mode: 'production', + target: 'node', + node: false, + + entry: `${__dirname}/src/cli.ts`, + + output: { + filename: 'cli.js', + path: `${__dirname}/dist` + }, + + externals, + externalsPresets: { node: true }, + + stats: { + orphanModules: true, + providedExports: true, + usedExports: true, + errorDetails: true + }, + + resolve: { + extensions: ['.ts', '.wasm', '.mjs', '.cjs', '.js', '.json'], + // ! If changed, also update these aliases in tsconfig.json, + // ! jest.config.js, next.config.ts, and .eslintrc.js + alias: IMPORT_ALIASES + }, + module: { + rules: [{ test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: /node_modules/ }] + }, + optimization: { usedExports: true }, + plugins: [ + ...envPlugins, + // * β–Ό For bundled CLI applications, make entry file executable w/ shebang + new BannerPlugin({ banner: '#!/usr/bin/env node', raw: true, entryOnly: true }) + ] +}; */ + +module.exports = [/*libConfig,*/ externalsConfig /*, cliConfig*/]; +debug('exports: %O', module.exports);