diff --git a/.eslintrc.js b/.eslintrc.js index 53aed49..c186191 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,21 +1,21 @@ module.exports = { - "extends": ["airbnb-base"], - "env": { - "node": true, - "mocha": true, - "es6": true, - "mongo": true + extends: ["airbnb-base"], + env: { + node: true, + mocha: true, + es6: true, + mongo: true, }, - "rules": { + rules: { "no-unused-vars": "warn", - "no-var" : "off", + "no-var": "off", "func-names": "off", "consistent-return": "off", "prefer-rest-params": "off", - "radix": "off", + radix: "off", "prefer-spread": "off", "no-plusplus": "off", - "camelcase": "off", + camelcase: "off", "no-use-before-define": "off", "no-lonely-if": "off", "no-restricted-syntax": "off", @@ -27,192 +27,192 @@ module.exports = { "no-bitwise": "off", "no-mixed-operators": "off", "object-shorthand": "off", - //reassess till here ==== + // reassess till here ==== "block-spacing": [ - "error", - "always" - ], - "brace-style": [ - "error", - "stroustrup" - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "comma-style": [ - "error", - "last" - ], - "computed-property-spacing": [ - "error", - "never" - ], - "curly": [ - "error", - "all" - ], - "eol-last": "off", - "func-call-spacing": [ - "error", - "never" - ], - "indent": [ - "error", - 4 - ], - "key-spacing": [ - "error", - { - "beforeColon": false, - "afterColon": true - } - ], - "keyword-spacing": [ - "error", - { - "before": true, - "after": true - } - ], - "lines-between-class-members": [ - "error", - "always" - ], - "no-multi-spaces": [ - "warn" - ], - "no-trailing-spaces": [ - "warn", - { - "ignoreComments": true - } - ], - "no-whitespace-before-property": [ - "error" - ], - "object-curly-newline": [ - "error", - { - "multiline": true, - "consistent": true - } - ], - "object-property-newline": [ - "error", - { - "allowAllPropertiesOnSameLine": true - } - ], - "semi": [ - "error", - "always" - ], - "semi-style": [ - "error", - "last" - ], - "space-before-blocks": [ - "error", - "always" - ], - "space-before-function-paren": [ - "error", - "never" - ], - "space-in-parens": [ - "error", - "never" - ], - "space-infix-ops": [ - "error" - ], - "space-unary-ops": [ - "error", - { - "words": true, - "nonwords": false - } - ], - "switch-colon-spacing": [ - "error" - ], - "unicode-bom": [ - "error", - "never" - ], - "linebreak-style": [ - "error", - "unix" - ], - "no-useless-escape": "off", - "quotes": "off", - "valid-jsdoc": [ - "error", - { - "requireReturn": false - } - ], - "require-jsdoc": [ - "error", - { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - "ClassDeclaration": true, - "ArrowFunctionExpression": true, - "FunctionExpression": false - } - } - ], - "no-console": [ - "error" - ], - "dot-notation": [ - "error" - ], - "eqeqeq": [ - "error", - "always" - ], - "no-alert": [ - "error" - ], - "no-caller": [ - "error" - ], - "no-eval": [ - "error" - ], - "no-extend-native": [ - "error" - ], - "no-iterator": [ - "error" - ], - "no-loop-func": [ - "error" - ], - "no-shadow": [ - "error" - ] + "error", + "always", + ], + "brace-style": [ + "error", + "stroustrup", + ], + "comma-spacing": [ + "error", + { + before: false, + after: true, + }, + ], + "comma-style": [ + "error", + "last", + ], + "computed-property-spacing": [ + "error", + "never", + ], + curly: [ + "error", + "all", + ], + "eol-last": "off", + "func-call-spacing": [ + "error", + "never", + ], + indent: [ + "error", + 4, + ], + "key-spacing": [ + "error", + { + beforeColon: false, + afterColon: true, + }, + ], + "keyword-spacing": [ + "error", + { + before: true, + after: true, + }, + ], + "lines-between-class-members": [ + "error", + "always", + ], + "no-multi-spaces": [ + "warn", + ], + "no-trailing-spaces": [ + "warn", + { + ignoreComments: true, + }, + ], + "no-whitespace-before-property": [ + "error", + ], + "object-curly-newline": [ + "error", + { + multiline: true, + consistent: true, + }, + ], + "object-property-newline": [ + "error", + { + allowAllPropertiesOnSameLine: true, + }, + ], + semi: [ + "error", + "always", + ], + "semi-style": [ + "error", + "last", + ], + "space-before-blocks": [ + "error", + "always", + ], + "space-before-function-paren": [ + "error", + "never", + ], + "space-in-parens": [ + "error", + "never", + ], + "space-infix-ops": [ + "error", + ], + "space-unary-ops": [ + "error", + { + words: true, + nonwords: false, + }, + ], + "switch-colon-spacing": [ + "error", + ], + "unicode-bom": [ + "error", + "never", + ], + "linebreak-style": [ + "error", + "unix", + ], + "no-useless-escape": "off", + quotes: "off", + "valid-jsdoc": [ + "error", + { + requireReturn: false, + }, + ], + "require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: true, + FunctionExpression: false, + }, + }, + ], + "no-console": [ + "error", + ], + "dot-notation": [ + "error", + ], + eqeqeq: [ + "error", + "always", + ], + "no-alert": [ + "error", + ], + "no-caller": [ + "error", + ], + "no-eval": [ + "error", + ], + "no-extend-native": [ + "error", + ], + "no-iterator": [ + "error", + ], + "no-loop-func": [ + "error", + ], + "no-shadow": [ + "error", + ], }, - "overrides": [ + overrides: [ { - "files": [ + files: [ "test/**/*.js", - "webpack.config.js" + "webpack.config.js", ], - "env": { - "es6": true, - "node": true + env: { + es6: true, + node: true, }, rules: { "require-jsdoc": "off", - } - } - ] - }; \ No newline at end of file + }, + }, + ], +}; \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c5fe6e..d2b10bb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,7 +2,26 @@ version: 2 updates: - package-ecosystem: npm directory: "/" + target-branch: staging schedule: interval: monthly open-pull-requests-limit: 10 versioning-strategy: increase-if-necessary + groups: + dev-dependencies: + dependency-type: development + update-types: [ "minor", "patch" ] + ignore: + # mocha 12 needs Node 20.19+ while CI still tests Node 18, and eslint 9+ needs a new config format + - dependency-name: "mocha" + update-types: [ "version-update:semver-major" ] + - dependency-name: "eslint" + update-types: [ "version-update:semver-major" ] + - package-ecosystem: github-actions + directory: "/" + target-branch: staging + schedule: + interval: monthly + groups: + github-actions: + patterns: [ "*" ] diff --git a/.github/scripts/check_release.js b/.github/scripts/check_release.js new file mode 100644 index 0000000..5a5aa0b --- /dev/null +++ b/.github/scripts/check_release.js @@ -0,0 +1,58 @@ +/** + * Release check run by CI before publishing: the version in package.json, the SDK_VERSION constants in lib and the + * first CHANGELOG heading must all name the same version. With "--notes" it prints that version's CHANGELOG section + * instead, which becomes the GitHub release notes. + * usage: node .github/scripts/check_release.js [--notes] + */ +var fs = require("fs"); +var path = require("path"); + +var root = path.resolve(__dirname, "../.."); + +/** + * Reads a file of the repository + * @param {String} file - path relative to the repository root + * @returns {String} file content + */ +function read(file) { + return fs.readFileSync(path.join(root, file), "utf8"); +} + +/** + * Splits the CHANGELOG into its "## " sections + * @returns {Array<{heading: String, body: String}>} sections in file order + */ +function changelogSections() { + return read("CHANGELOG.md").split(/^## /m).slice(1).map((section) => { + var lines = section.split("\n"); + return { heading: lines[0].trim(), body: lines.slice(1).join("\n").trim() }; + }); +} + +var { version } = JSON.parse(read("package.json")); +var sections = changelogSections(); + +if (process.argv.indexOf("--notes") !== -1) { + process.stdout.write(`${sections.length ? sections[0].body : ""}\n`); +} +else { + var problems = []; + var heading = sections.length ? sections[0].heading : "(none)"; + if (heading !== version) { + problems.push(`the first CHANGELOG heading is "${heading}", rename it to "${version}"`); + } + ["lib/countly.js", "lib/countly-bulk.js"].forEach((file) => { + var match = /SDK_VERSION = "([^"]+)"/.exec(read(file)); + var found = match ? match[1] : "(none)"; + if (found !== version) { + problems.push(`${file} has SDK_VERSION "${found}", set it to "${version}"`); + } + }); + if (problems.length) { + process.stderr.write(`Release check failed for ${version}:\n- ${problems.join("\n- ")}\n`); + process.exitCode = 1; + } + else { + process.stdout.write(`Release check passed for ${version}\n`); + } +} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b9bab18..35497af 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,33 +39,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 with: languages: ${{ matrix.language }} + build-mode: none # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # πŸ“š https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5a729d8..56d275f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,37 +9,173 @@ on: pull_request: branches: [ master, staging ] +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # the test suite needs Node 18.18+ (mocha 11), the compat job covers the older supported versions + os: [ ubuntu-latest ] + node: [ 18, 20, 22, 24, 26 ] + include: + - os: windows-latest + node: 24 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + - run: npm ci + - name: Lint and test + if: matrix.os == 'ubuntu-latest' && matrix.node != 24 + run: npm test + - name: Lint and test with coverage + if: matrix.os == 'ubuntu-latest' && matrix.node == 24 + run: npm run coverage + - name: Upload the coverage report + if: matrix.os == 'ubuntu-latest' && matrix.node == 24 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-report + path: coverage/ + # lint runs on Linux only, as Windows checkouts can have CRLF line endings that the lint rules reject + - name: Test + if: matrix.os == 'windows-latest' + run: npm test --ignore-scripts + compat: runs-on: ubuntu-latest + timeout-minutes: 5 strategy: - matrix: - node: [14, 16, 18, 20, 22, 23] + fail-fast: false + matrix: + node: [ 10, 12, 14, 16 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v4 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node }} - cache: 'npm' - - run: npm install - - run: npm run build --if-present - - run: npm test - + # the SDK has no dependencies, so the check runs without installing packages + - run: node test/compat/compat_check.js + deploy: runs-on: ubuntu-latest - needs: build + needs: [ build, compat ] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + permissions: + contents: write steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Use Node.js 24.x + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: 20.x + node-version: 24.x cache: 'npm' - - run: npm install + - run: npm ci - name: Deploy documentation shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bash generateDocs.sh + + version: + runs-on: ubuntu-latest + needs: [ build, compat ] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + outputs: + version: ${{ steps.version.outputs.version }} + new: ${{ steps.version.outputs.new }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Use Node.js 24 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + package-manager-cache: false + - name: Check whether this version is on npm already + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + STATUS=0 + PUBLISHED=$(npm view "countly-sdk-nodejs@$VERSION" version 2> npm-view-error.txt) || STATUS=$? + if [ "$STATUS" -eq 0 ] && [ "$PUBLISHED" = "$VERSION" ]; then + echo "new=false" >> "$GITHUB_OUTPUT" + echo "::notice::$VERSION is on npm already, nothing to release" + elif [ "$STATUS" -eq 0 ] || grep -q E404 npm-view-error.txt; then + echo "new=true" >> "$GITHUB_OUTPUT" + else + cat npm-view-error.txt + echo "::error::Could not check whether $VERSION is on npm" + exit 1 + fi + - name: Check that package.json, the code and the CHANGELOG name the same version + if: steps.version.outputs.new == 'true' + run: node .github/scripts/check_release.js + + stage: + runs-on: ubuntu-latest + needs: version + if: needs.version.outputs.new == 'true' + # a maintainer approves this job through the npm environment, which npm's trusted publisher setting also names + environment: npm + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Use Node.js 24 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + # npm stage needs npm 11.15.0 or newer, which only Node 24.18.0 and newer ship + check-latest: true + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Stage the package on npm + env: + VERSION: ${{ needs.version.outputs.version }} + run: | + if [ "$(npm view "countly-sdk-nodejs@$VERSION" version 2> /dev/null)" = "$VERSION" ]; then + echo "::notice::$VERSION is on npm already, staging is skipped" + elif ! npm stage publish; then + echo "::error::Staging $VERSION failed. If $VERSION is already in the Staged Packages tab on npmjs.com, approve or reject it there" + exit 1 + fi + + # a draft release sends no notice; publishing it after the npm approval starts the Release Notice workflow + draft: + runs-on: ubuntu-latest + needs: [ version, stage ] + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Create the draft GitHub release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.version.outputs.version }} + run: | + if gh release view "$VERSION" > /dev/null 2>&1; then + echo "::notice::The GitHub release $VERSION exists already" + else + node .github/scripts/check_release.js --notes > release-notes.md + gh release create "$VERSION" --draft --title "Countly NodeJS SDK $VERSION" --notes-file release-notes.md --target "$GITHUB_SHA" + fi + URL=$(gh release view "$VERSION" --json url --jq .url) + { + echo "### Next steps for $VERSION" + echo "1. Approve $VERSION in the Staged Packages tab of https://www.npmjs.com/package/countly-sdk-nodejs" + echo "2. Publish the draft release, which sends the release notice: $URL" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml index e17315a..f3b09fe 100644 --- a/.github/workflows/release_notice.yml +++ b/.github/workflows/release_notice.yml @@ -3,19 +3,22 @@ on: release: types: [published] workflow_dispatch: +permissions: + contents: read jobs: build: runs-on: ubuntu-latest steps: - # To check the github context - - name: Dump Github context - env: - GITHUB_CONTEXT: ${{ toJSON(github) }} - run: echo "$GITHUB_CONTEXT" - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@v1.23.0 + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 with: + webhook: ${{ secrets.SLACK_RELEASE }} + webhook-type: webhook-trigger + # errors, retries and payload-delimiter keep the behaviour of v1: the step fails on an error, posts once and sends every value as text + errors: true + retries: 0 + payload-delimiter: "." # This data can be any valid JSON from a previous step in the GitHub Action payload: | { @@ -25,15 +28,14 @@ jobs: "body": ${{ toJSON(github.event.release.body) }}, "html_url": "${{ github.event.release.html_url }}" } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE }} - name: Send custom JSON data to Discord - uses: sarisia/actions-status-discord@v1.13.0 + if: ${{ !cancelled() }} + uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0 with: webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} nodetail: true title: New ${{ github.repository }} version ${{ github.event.release.tag_name }} published by ${{ github.actor }} description: | Release URL: ${{ github.event.release.html_url }} - Click [here](https://github.com/Countly/countly-server/blob/master/CHANGELOG.md) to view the change log. + Click [here](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md) to view the change log. `${{ github.event.release.body }}` diff --git a/.gitignore b/.gitignore index 7fd7636..4d9fea3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ bulk_data/** **/data .nyc_output coverage -coverage-report.txt \ No newline at end of file +coverage-report.txt +.countly-storage/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b6a01..9fc3230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +## 26.8.0 +- ! Minor breaking change ! SDK behavior settings are now enabled by default. Changes made in SDK Manager on your server affect the SDK's behavior directly and take precedence over the matching init options: + - The SDK fetches the settings at start, every 4 hours by default and after a device ID change, also before consent is given, and stores them for the next start +- ! Minor breaking change ! The SDK now stores its data in a `.countly-storage` folder in the app's working directory: + - Before, it used a `data` folder inside the SDK package, which every reinstall or upgrade of the package deleted, so the next start counted as a new user + - Data at the old location is moved to the new folder once, at the first start, and the old files are kept with a `.migrated` suffix. If the new folder is not writable, the old location stays in use + - A relative `storage_path` is now resolved against the app's working directory, and the files go inside the given folder also when the path has no trailing slash + - CountlyBulk without `storage_path` now stores its data in `.countly-storage/bulk/` + - Add `.countly-storage/` to your `.gitignore`. Command line tools started from different folders and apps in containers should set an absolute `storage_path` to keep one device ID + - The SDK prints a warning, also when logging is off, when the storage folder is not writable, for example on AWS Lambda, or is shared by two instances +- ! Minor breaking change ! The SDK now keeps the process running only while it has data to send and the server answers, so a script ends by itself once its data is delivered. Data that cannot be sent yet stays stored and is sent at the next start +- ! Minor breaking change ! With `track_errors`, unhandled promise rejections are now recorded as non-fatal errors, as the process keeps running after them. Uncaught exceptions are still recorded as fatal crashes +- ! Minor breaking change ! In a cluster, the workers and the main process now exchange messages that have a `cly` property. Your own `message` handlers receive them as well + +- Added the init option `standalone_worker` that lets a cluster worker send its own data when the main process does not run the SDK, for example in PM2 cluster mode: + - Each worker keeps its data in its own `worker-` folder, numbered by PM2's instance number or else by the cluster worker ID + - The cluster worker ID changes when a worker restarts, so pass `device_id` there to keep one device ID +- Added the init option `behavior_settings` for providing SDK behavior settings, in the format SDK Manager exports, until the first successful fetch +- Added the init option `disable_sdk_behavior_settings_updates` that stops fetching SDK behavior settings from the server. Stored and provided settings still apply +- Added SDK health check requests after init, also sent before consent is given. They can be turned off with the new init option `disable_health_check` +- Added an automatic backoff mechanism that by default pauses sending for 60 seconds after the server needed 10 seconds or more to answer. It can be turned off with the new init option `disable_backoff_mechanism` +- Added the init option `request_timeout` for setting, in seconds, how long a request waits for the server before it is sent again later (default 30, also for CountlyBulk) +- Added the method `flush` that sends all recorded data right away and resolves once it is delivered, for example before `process.exit()` +- A cluster worker whose main process does not run the SDK now prints a warning, also when logging is off, that its data is not sent +- CountlyBulk now prints a warning when `salt` is set +- Raw requests made with `request` now count towards the request queue size +- Queued requests are now sent one after another without waiting for the next heartbeat, also in CountlyBulk +- File storage now optimized for repeated writes +- The SDK now removes its process and cluster event listeners when `halt` is called + +- Mitigated an issue where a missing storage file, which is normal at the first start, was logged as a warning +- Mitigated an issue where a request could be lost when the process ended while the request was being sent +- Mitigated an issue where calling `process.exit()` right after an SDK call could empty the storage file and lose the stored data and device ID +- Mitigated an issue where a BigInt value or a self-referencing object in recorded data crashed the app. BigInt values are now sent as text +- Mitigated an issue where a request that got no answer, or only part of one, stopped all further sending until the app restarted, also in CountlyBulk +- Mitigated an issue where CountlyBulk instances, and CountlyBulk next to the SDK, shared their storage, so data could be lost, sent twice or sent under the wrong app +- Mitigated an issue where removing consent was never sent to the server +- Mitigated an issue where a consent change made right before a device ID change, or before `flush` and `process.exit()`, was sent under the new device ID or lost +- Mitigated an issue where, after `change_id` without merge, the previous user's open view, unsaved custom properties, breadcrumbs and remote config values carried over to the wrong user +- Mitigated an issue where headers set in `http_options` made every GET request fail +- Mitigated an issue where internal limits were used wrongly +- Mitigated an issue where requests containing an apostrophe failed the checksum check of Countly 26.01 and newer servers when `salt` is set +- Mitigated an issue where a value containing half of an emoji could never be sent and stopped all further sending. Such characters are now sent as "οΏ½" +- Mitigated an issue where a CountlyBulk user created with `ip_address` threw an error on most calls +- Mitigated an issue where giving consent to a CountlyBulk user crashed the app when a call was waiting for that consent +- Mitigated an issue where the CountlyBulk `start` callback reported an empty queue while data was still unsent +- Mitigated an issue where calling `track_errors` repeatedly recorded each error several times +- Mitigated an issue where CountlyBulk replaced explicitly provided `hour`, `dow` and `tz` values of 0 with the current time in `add_request` and `add_bulk_request` +- Mitigated an issue where memory storage wrote files when a fatal crash was recorded + ## 24.10.4 - Added a new init time flag `salt` for request tampering protection (should be used in tandem with server options) diff --git a/bulk_data/.gitignore b/bulk_data/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/bulk_data/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/data/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/lib/countly-behavior.js b/lib/countly-behavior.js new file mode 100644 index 0000000..5cb5c3e --- /dev/null +++ b/lib/countly-behavior.js @@ -0,0 +1,222 @@ +/** + * SDK behavior settings: checks the settings the server sends (from SDK Manager) or the developer provides, + * and decides the value of each setting the SDK uses. + * @module lib/countly-behavior + */ + +/** + * Settings the Node.js SDK follows, with the kind of value each one takes: + * "switch" true or false; "count" a whole number of at least 1; "zeroOrMore" a whole number of at least 0; + * "fraction" a number above 0 and at most 1 + */ +var SUPPORTED = { + tracking: "switch", + networking: "switch", + st: "switch", + vt: "switch", + cet: "switch", + crt: "switch", + lt: "switch", + cr: "switch", + bom: "switch", + rqs: "count", + eqs: "count", + sui: "count", + lkl: "count", + lvs: "count", + lsv: "count", + lbc: "count", + ltlpt: "count", + ltl: "count", + scui: "count", + bom_at: "count", + bom_ra: "count", + bom_d: "count", + dort: "zeroOrMore", + bom_rqp: "fraction", +}; + +/** + * Values used when neither the server nor the developer gives one + */ +var DEFAULTS = { + tracking: true, + networking: true, + st: true, + vt: true, + cet: true, + crt: true, + lt: true, + cr: false, + bom: true, + scui: 4, + dort: 0, + bom_at: 10, + bom_rqp: 0.5, + bom_ra: 24, + bom_d: 60, +}; + +/** + * Longest settings refresh interval in hours. A Node.js timer cannot wait longer than about 24.8 days, and a + * longer value would make it fire at once, over and over. + */ +var MAX_REFRESH_HOURS = 596; + +/** + * Tells whether a value is a plain object (not null, not an array) + * @param {*} value - value to check + * @returns {Boolean} true for a plain object + */ +function isPlainObject(value) { + return Object.prototype.toString.call(value) === "[object Object]"; +} + +/** + * Tells whether a value fits the kind a setting takes + * @param {String} kind - "switch", "count", "zeroOrMore" or "fraction" + * @param {*} value - value from the server or the developer + * @returns {Boolean} true if the value can be used + */ +function isValidValue(kind, value) { + if (kind === "switch") { + return typeof value === "boolean"; + } + if (typeof value !== "number" || !Number.isFinite(value)) { + return false; + } + if (kind === "count") { + return Math.floor(value) === value && value >= 1; + } + if (kind === "zeroOrMore") { + return Math.floor(value) === value && value >= 0; + } + return value > 0 && value <= 1; +} + +/** + * Keeps only the supported settings with a usable value. Other keys (for example the dashboard's + * "bom_preset", or settings for other platforms) are left out, and an unusable value is left out with a + * warning, so the developer value or the default stays in use for it. + * @param {Object} config - the "c" object of a settings answer + * @param {Function} warn - called with a message for each left out value + * @returns {Object} the usable settings + */ +function normalize(config, warn) { + var result = {}; + Object.keys(config).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(SUPPORTED, key)) { + return; + } + if (isValidValue(SUPPORTED[key], config[key])) { + result[key] = key === "scui" ? Math.min(config[key], MAX_REFRESH_HOURS) : config[key]; + } + else if (typeof warn === "function") { + warn(`The behavior setting [${key}] has an unusable value [${JSON.stringify(config[key])}], it is ignored.`); + } + }); + return result; +} + +/** + * Reads the server's answer to a settings request. The answer must be a JSON object with a "c" object; + * other top-level keys (v, t, lg, ct and future ones) are ignored, so both the v1 and the v2 format work. + * @param {String} responseText - the answer body + * @param {Function} warn - called with a message for each problem + * @returns {Object|null} the usable settings, or null when the answer cannot be used + */ +function parseAnswer(responseText, warn) { + var answer; + try { + answer = JSON.parse(responseText); + } + catch (ex) { + return null; + } + if (!isPlainObject(answer) || !isPlainObject(answer.c)) { + return null; + } + return normalize(answer.c, warn); +} + +/** + * Reads the settings a developer provides with the "behavior_settings" init option, in the same format the + * server sends: an object with a "c" object + * @param {*} provided - the init option value + * @param {Function} warn - called with a message for each problem + * @returns {Object|null} the usable settings, or null when none are provided or the format is wrong + */ +function parseProvided(provided, warn) { + if (typeof provided === "undefined" || provided === null) { + return null; + } + if (!isPlainObject(provided) || !isPlainObject(provided.c)) { + if (typeof warn === "function") { + warn("behavior_settings must be an object with a \"c\" object, as SDK Manager exports it. It is ignored."); + } + return null; + } + return normalize(provided.c, warn); +} + +/** + * Reads settings stored by an earlier run + * @param {*} stored - the stored value + * @returns {Object|null} the usable settings, or null when nothing usable is stored + */ +function parseStored(stored) { + if (!isPlainObject(stored)) { + return null; + } + return normalize(stored); +} + +/** + * Decides the value of a setting: the server's (or the stored or provided) value when there is one, else + * the developer's value, else the default + * @param {Object} settings - settings in use + * @param {String} key - setting key + * @param {*=} developerValue - value set with an init option, undefined when none + * @returns {*} the value to use + */ +function valueOf(settings, key, developerValue) { + if (settings && Object.prototype.hasOwnProperty.call(settings, key)) { + return settings[key]; + } + if (typeof developerValue !== "undefined") { + return developerValue; + } + return DEFAULTS[key]; +} + +/** + * Decides whether sending must pause after a successful request, following the backoff settings: the + * request took at least bom_at seconds, the queue is at most bom_rqp of its size, and the request was + * recorded at most bom_ra hours ago + * @param {Object} input - measurements and settings + * @param {Number} input.durationSeconds - how long the request took + * @param {Number} input.queueLength - requests left in the queue + * @param {Number} input.queueSize - maximum queue size + * @param {Number} input.ageHours - hours since the request was recorded + * @param {Number} input.bom_at - response time threshold in seconds + * @param {Number} input.bom_rqp - queue fill threshold as a fraction of the queue size + * @param {Number} input.bom_ra - request age threshold in hours + * @returns {Boolean} true when sending must pause + */ +function shouldBackOff(input) { + return input.durationSeconds >= input.bom_at + && input.queueLength <= input.queueSize * input.bom_rqp + && input.ageHours <= input.bom_ra; +} + +module.exports = { + SUPPORTED, + DEFAULTS, + MAX_REFRESH_HOURS, + normalize, + parseAnswer, + parseProvided, + parseStored, + valueOf, + shouldBackOff, +}; diff --git a/lib/countly-bulk-user.js b/lib/countly-bulk-user.js index 45e3d37..a7d0f11 100644 --- a/lib/countly-bulk-user.js +++ b/lib/countly-bulk-user.js @@ -79,6 +79,7 @@ function CountlyBulkUser(conf) { return; } + var self = this; conf.maxKeyLength = conf.max_key_length || maxKeyLength; conf.maxValueSize = conf.max_value_size || maxValueSize; conf.maxSegmentationValues = conf.max_segmentation_values || maxSegmentationValues; @@ -172,13 +173,13 @@ function CountlyBulkUser(conf) { if (consents[feature].optin !== true) { consents[feature].optin = true; updateConsent(); - setTimeout(function() { + setTimeout(() => { if (feature === "sessions" && lastParams.begin_session) { - this.begin_session.apply(this, lastParams.begin_session); + self.begin_session.apply(self, lastParams.begin_session); lastParams.begin_session = null; } else if (feature === "views" && lastParams.track_pageview) { - this.track_pageview.apply(this, lastParams.track_pageview); + self.report_view.apply(self, lastParams.track_pageview); lastParams.track_pageview = null; } }, 1); @@ -214,13 +215,15 @@ function CountlyBulkUser(conf) { } else if (consents[feature]) { if (consents[feature].features) { + consents[feature].optin = false; // this is added group, let's iterate through sub features this.remove_consent_internal(consents[feature].features, enforceConsentUpdate); } else { + var wasGiven = consents[feature].optin === true; consents[feature].optin = false; // this is core feature - if (enforceConsentUpdate && consents[feature].optin !== false) { + if (enforceConsentUpdate && wasGiven) { updateConsent(); } } @@ -247,7 +250,7 @@ function CountlyBulkUser(conf) { consentMessage[features[i]] = false; } } - conf.server.add_bulk_request({ consent: JSON.stringify(consentMessage) }); + conf.server.add_request(prepareQuery({ consent: JSON.stringify(consentMessage) })); cc.log(cc.logLevelEnums.DEBUG, "CountlyBulkUser updateConsent, Consent update request has been sent to the queue."); }, 1000); }; @@ -300,9 +303,6 @@ function CountlyBulkUser(conf) { else { query.session_duration = seconds; } - if (conf.ip_address) { - query.ip_address = conf.ip_address; - } if (timestamp) { query.timestamp = timestamp + ((i + 1) * 60); } @@ -791,7 +791,7 @@ function CountlyBulkUser(conf) { if (!query.device_id) { query.device_id = conf.device_id; } - if (conf.ip_address && this.check_consent("location")) { + if (conf.ip_address && self.check_consent("location")) { query.ip_address = conf.ip_address; } return query; diff --git a/lib/countly-bulk.js b/lib/countly-bulk.js index 3ecdc93..16ce0f4 100644 --- a/lib/countly-bulk.js +++ b/lib/countly-bulk.js @@ -40,8 +40,9 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums; * @param {number} [conf.session_update=60] - how often in seconds should session be extended * @param {number} [conf.max_events=100] - maximum amount of events to send in one batch * @param {boolean} [conf.force_post=false] - force using post method for all requests - * @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests - * @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc + * @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests. Countly servers check the checksum of every request inside a bulk request, which bulk requests cannot carry, so an app with a salt configured on the server drops bulk data + * @param {string} [conf.storage_path=.countly-storage/bulk/] - folder where SDK would store data, including queues. A relative path is resolved against the app's working directory. Give each instance its own folder + * @param {number} [conf.request_timeout=30] - seconds to wait for the server's answer; after that the request counts as failed and is sent again later * @param {string} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request * @param {number} [conf.max_key_length=128] - maximum size of all string keys * @param {number} [conf.max_value_size=256] - maximum size of all values in our key-value pairs (Except "picture" field, that has a limit of 4096 chars) @@ -60,7 +61,7 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums; * }); */ function CountlyBulk(conf) { - var SDK_VERSION = "24.10.4"; + var SDK_VERSION = "26.8.0"; var SDK_NAME = "javascript_native_nodejs_bulk"; var empty_queue_callback = null; @@ -106,13 +107,19 @@ function CountlyBulk(conf) { conf.maxBreadcrumbCount = conf.max_breadcrumb_count || maxBreadcrumbCount; conf.maxStackTraceLinesPerThread = conf.max_stack_trace_lines_per_thread || maxStackTraceLinesPerThread; conf.maxStackTraceLineLength = conf.max_stack_trace_line_length || maxStackTraceLineLength; + conf.request_timeout = Number(conf.request_timeout) > 0 ? Math.max(1, Number(conf.request_timeout)) : 30; + + if (conf.salt) { + cc.warnOnce("bulk-salt", "Countly: \"salt\" is set for CountlyBulk. Countly servers check the checksum of every request inside a bulk request, which bulk requests cannot carry, so an app with a salt configured on the server drops all bulk data. Remove the salt from the app in the dashboard, or send this data without bulk mode."); + } // bulk mode is memory only by default if (typeof conf.storage_type === "undefined" && conf.persist_queue === false) { conf.storage_type = CountlyBulk.StorageTypes.MEMORY; } - CountlyStorage.initStorage(conf.storage_path, conf.storage_type, true, conf.custom_storage_method); + var storage = CountlyStorage.createStorage(); + storage.initStorage(conf.storage_path, conf.storage_type, true, conf.custom_storage_method, conf.app_key); this.conf = conf; /** @@ -128,7 +135,7 @@ function CountlyBulk(conf) { * @returns {module:lib/countly-bulk} instance * */ this.add_request = function(query) { - query = cc.truncateObject(query, self.maxKeyLength, self.maxValueSize, self.maxSegmentationValues, "add_request", self.debug); + query = cc.makeJsonSafe(query, "add_request") || {}; if (cluster.isMaster) { if (!query.device_id) { cc.log(cc.logLevelEnums.ERROR, "CountlyBulk add_request, 'device_id' is missing."); @@ -147,13 +154,13 @@ function CountlyBulk(conf) { query.sdk_version = SDK_VERSION; query.timestamp = query.timestamp || getMsTimestamp(); var date = new Date((`${query.timestamp}`).length === 13 ? query.timestamp : parseInt(query.timestamp) * 1000); - query.hour = query.hour || date.getHours(); - query.dow = query.dow || date.getDay(); - query.tz = query.tz || -date.getTimezoneOffset(); + query.hour = typeof query.hour !== "undefined" ? query.hour : date.getHours(); + query.dow = typeof query.dow !== "undefined" ? query.dow : date.getDay(); + query.tz = typeof query.tz !== "undefined" ? query.tz : -date.getTimezoneOffset(); requestQueue.push(query); cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_request, Adding request to the queue."); - CountlyStorage.storeSet("cly_req_queue", requestQueue); + storage.storeSet("cly_req_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_request, Sending message to the parent process. Adding the raw request to the queue."); @@ -175,6 +182,7 @@ function CountlyBulk(conf) { * @returns {module:lib/countly-bulk} instance * */ this.add_bulk_request = function(requests) { + requests = cc.makeJsonSafe(requests, "add_bulk_request"); if (cluster.isMaster) { var query; for (var i in requests) { @@ -196,13 +204,13 @@ function CountlyBulk(conf) { query.sdk_version = SDK_VERSION; query.timestamp = query.timestamp || getMsTimestamp(); var date = new Date((`${query.timestamp}`).length === 13 ? query.timestamp : parseInt(query.timestamp) * 1000); - query.hour = query.hour || date.getHours(); - query.dow = query.dow || date.getDay(); - query.tz = query.tz || -date.getTimezoneOffset(); + query.hour = typeof query.hour !== "undefined" ? query.hour : date.getHours(); + query.dow = typeof query.dow !== "undefined" ? query.dow : date.getDay(); + query.tz = typeof query.tz !== "undefined" ? query.tz : -date.getTimezoneOffset(); cc.log(cc.logLevelEnums.INFO, `CountlyBulk add_bulk_request, adding the request into queue: [${JSON.stringify(query)}]`); requestQueue.push(query); } - CountlyStorage.storeSet("cly_req_queue", requestQueue); + storage.storeSet("cly_req_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_bulk_request, Sending message to the parent process. Adding the raw request to the queue."); @@ -247,7 +255,7 @@ function CountlyBulk(conf) { event.key = cc.truncateSingleValue(event.key, self.maxKeyLength, "add_event", self.debug); event.segmentation = cc.truncateObject(event.segmentation, self.maxKeyLength, self.maxValueSize, self.maxSegmentationValues, "add_event", self.debug); var props = ["key", "count", "sum", "dur", "segmentation", "timestamp"]; - var e = cc.getProperties(event, props); + var e = cc.makeJsonSafe(cc.getProperties(event, props), "add_event"); e.timestamp = e.timestamp || getMsTimestamp(); var date = new Date((`${e.timestamp}`).length === 13 ? e.timestamp : parseInt(e.timestamp) * 1000); e.hour = date.getHours(); @@ -257,11 +265,11 @@ function CountlyBulk(conf) { eventQueue[device_id] = []; } eventQueue[device_id].push(e); - CountlyStorage.storeSet("cly_bulk_event", eventQueue); + storage.storeSet("cly_bulk_event", eventQueue); } else { cc.log(cc.logLevelEnums.INFO, `CountlyBulk add_event, Sending message to the parent process. Adding event: [${event.key}].`); - process.send({ cly_bulk: { device_id: device_id, event: event } }); + process.send({ cly_bulk: { device_id: device_id, event: cc.makeJsonSafe(event, "add_event") } }); } return this; }; @@ -355,65 +363,46 @@ function CountlyBulk(conf) { */ function toBulkRequestQueue(bulkRequest) { bulkQueue.push(bulkRequest); - CountlyStorage.storeSet("cly_bulk_queue", bulkQueue); + storage.storeSet("cly_bulk_queue", bulkQueue); } var self = this; /** - * Makin request making and data processing loop + * Makin request making and data processing loop. Each beat packs all recorded events and requests into + * bulk requests and starts sending them. The queue counts as empty only when nothing is recorded, queued, + * waiting for a retry or being sent, so the start() callback is not called while data is still unsent. */ function heartBeat() { var isEmpty = true; // process event queue var eventChanges = false; + var batchSize = conf.max_events > 0 ? conf.max_events : 100; for (var device_id in eventQueue) { - if (eventQueue[device_id].length > 0) { - eventChanges = true; - if (eventQueue[device_id].length <= conf.max_events) { - self.add_request({ device_id: device_id, events: eventQueue[device_id] }); - eventQueue[device_id] = []; - } - else { - var events = eventQueue[device_id].splice(0, conf.max_events); - self.add_request({ device_id: device_id, events: events }); - } + var deviceEvents = eventQueue[device_id]; + for (var i = 0; i < deviceEvents.length; i += batchSize) { + self.add_request({ device_id: device_id, events: deviceEvents.slice(i, i + batchSize) }); } + delete eventQueue[device_id]; + eventChanges = true; } if (eventChanges) { isEmpty = false; - CountlyStorage.storeSet("cly_bulk_event", eventQueue); + storage.storeSet("cly_bulk_event", eventQueue); } // process request queue into bulk requests if (requestQueue.length > 0) { isEmpty = false; - if (requestQueue.length <= conf.bulk_size) { - toBulkRequestQueue({ app_key: conf.app_key, requests: JSON.stringify(requestQueue) }); - requestQueue = []; - } - else { - var requests = requestQueue.splice(0, conf.bulk_size); - toBulkRequestQueue({ app_key: conf.app_key, requests: JSON.stringify(requests) }); + while (requestQueue.length > 0) { + toBulkRequestQueue({ app_key: conf.app_key, requests: JSON.stringify(requestQueue.splice(0, conf.bulk_size)) }); } - CountlyStorage.storeSet("cly_req_queue", requestQueue); + storage.storeSet("cly_req_queue", requestQueue); } - // process bulk request queue - if (bulkQueue.length > 0 && readyToProcess && cc.getTimestamp() > failTimeout) { + if (bulkQueue.length > 0 || !readyToProcess) { isEmpty = false; - readyToProcess = false; - var params = bulkQueue.shift(); - cc.log(cc.logLevelEnums.DEBUG, "CountlyBulk heartBeat, Processing the request:", params); - makeRequest(params, (err, res) => { - cc.log(cc.logLevelEnums.DEBUG, "CountlyBulk heartBeat, Request finished with response: ", res); - if (err) { - bulkQueue.unshift(res); - failTimeout = cc.getTimestamp() + conf.fail_timeout; - } - CountlyStorage.storeSet("cly_bulk_queue", bulkQueue); - readyToProcess = true; - }, "heartBeat", false); } + processBulkQueue(); if (isEmpty) { empty_count++; @@ -424,12 +413,45 @@ function CountlyBulk(conf) { } } } + else { + empty_count = 0; + } if (initiated) { setTimeout(heartBeat, conf.interval); } } + /** + * Sends the oldest bulk request when no other request is running. It stays in the queue until the server + * confirms it, and after a confirmed request the next one is sent right away. + */ + function processBulkQueue() { + if (bulkQueue.length === 0 || !readyToProcess || cc.getTimestamp() <= failTimeout) { + return; + } + readyToProcess = false; + var params = bulkQueue[0]; + cc.log(cc.logLevelEnums.DEBUG, "CountlyBulk heartBeat, Processing the request:", params); + makeRequest(params, (err, res) => { + cc.log(cc.logLevelEnums.DEBUG, "CountlyBulk heartBeat, Request finished with response: ", res); + if (err) { + failTimeout = cc.getTimestamp() + conf.fail_timeout; + } + else { + var index = bulkQueue.indexOf(params); + if (index !== -1) { + bulkQueue.splice(index, 1); + } + storage.storeSet("cly_bulk_queue", bulkQueue); + } + readyToProcess = true; + if (!err && bulkQueue.length > 0 && initiated) { + setImmediate(processBulkQueue); + } + }, "heartBeat", false); + } + /** * Get unique timestamp in miliseconds * @returns {number} miliseconds timestamp @@ -478,13 +500,29 @@ function CountlyBulk(conf) { } /** - * Making HTTP request + * Making HTTP request. The request fails when the server sends nothing for request_timeout seconds or + * the connection closes before the whole response arrived, and the callback is called exactly once. + * The options passed to http_options always contain a headers object. * @param {Object} params - key value object with URL params * @param {Function} callback - callback when request finished or failed * @param {string} info - function name or any other information for the logs * @param {Boolean} isBroad - if true a broader (more generous) response check would be implemented */ function makeRequest(params, callback, info, isBroad) { + var finished = false; + /** + * Reports the outcome of the request to the callback, once + * @param {Boolean} failed - true if the request failed + */ + function finish(failed) { + if (finished) { + return; + } + finished = true; + if (typeof callback === "function") { + callback(failed, params); + } + } try { info = info || "general"; isBroad = isBroad || false; @@ -497,6 +535,7 @@ function CountlyBulk(conf) { port: serverOptions.port, path: `${apiPath}?${data}`, method: "GET", + headers: {}, }; if (data.length >= 2000) { @@ -509,10 +548,8 @@ function CountlyBulk(conf) { if (method === "POST") { options.method = "POST"; options.path = apiPath; - options.headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(data), - }; + options.headers["Content-Type"] = "application/x-www-form-urlencoded"; + options.headers["Content-Length"] = Buffer.byteLength(data); } if (typeof conf.http_options === "function") { conf.http_options(options); @@ -523,6 +560,7 @@ function CountlyBulk(conf) { } var req = protocol.request(options, (res) => { var str = ""; + res.setEncoding("utf8"); res.on("data", (chunk) => { str += chunk; }); @@ -538,34 +576,38 @@ function CountlyBulk(conf) { // only JSON object with result can pass isResponseValidated = cc.isResponseValid(res.statusCode, str); } - if (isResponseValidated) { - callback(false, params); - } - else { - callback(true, params); + finish(!isResponseValidated); + }); + res.on("error", (err) => { + cc.log(cc.logLevelEnums.ERROR, "CountlyBulk makeRequest, Connection failed while receiving the response. Error:", err); + finish(true); + }); + res.on("close", () => { + if (!res.complete) { + cc.log(cc.logLevelEnums.ERROR, "CountlyBulk makeRequest, The connection closed before the whole response arrived."); + finish(true); } }); }); + req.setTimeout(conf.request_timeout * 1000, () => { + cc.log(cc.logLevelEnums.ERROR, `CountlyBulk makeRequest, The server did not answer within [${conf.request_timeout}] seconds.`); + req.destroy(new Error("request timeout")); + }); + req.on("error", (err) => { + cc.log(cc.logLevelEnums.ERROR, "CountlyBulk makeRequest, Connection failed. Error:", err); + finish(true); + }); if (method === "POST") { // write data to request body req.write(data); } - req.on("error", (err) => { - cc.log(cc.logLevelEnums.ERROR, "CountlyBulk makeRequest, Connection failed. Error:", err); - if (typeof callback === "function") { - callback(true, params); - } - }); - req.end(); } catch (e) { // fallback cc.log(cc.logLevelEnums.ERROR, "CountlyBulk makeRequest, Failed HTTP request. Error: ", e); - if (typeof callback === "function") { - callback(true, params); - } + finish(true); } } @@ -599,9 +641,16 @@ function CountlyBulk(conf) { worker.on("message", handleWorkerMessage); }); - var requestQueue = CountlyStorage.storeGet("cly_req_queue", []); - var eventQueue = CountlyStorage.storeGet("cly_bulk_event", {}); - var bulkQueue = CountlyStorage.storeGet("cly_bulk_queue", []); + var requestQueue = storage.storeGet("cly_req_queue", []); + var eventQueue = storage.storeGet("cly_bulk_event", {}); + var bulkQueue = storage.storeGet("cly_bulk_queue", []); + /** + * getStorage is a testing purposed method which returns the storage of this instance + * @returns {Object} storage + */ + this._getStorage = function() { + return storage; + }; /** * getBulkEventQueue is a testing purposed method which returns the event queue object * @returns {Object} eventQueue diff --git a/lib/countly-common.js b/lib/countly-common.js index fe5140a..7d88748 100644 --- a/lib/countly-common.js +++ b/lib/countly-common.js @@ -3,12 +3,81 @@ */ var crypto = require("crypto"); +/** + * Copies a value for makeJsonSafe + * @param {varies} value - value to copy + * @param {Array} ancestors - objects and arrays that contain the value + * @param {String} errorLog - prefix for error log + * @returns {varies} serializable copy + */ +function copyJsonSafe(value, ancestors, errorLog) { + if (typeof value === "bigint") { + return value.toString(); + } + if (value === null || typeof value !== "object") { + return value; + } + if (value instanceof String || value instanceof Number || value instanceof Boolean) { + return value.valueOf(); + } + if (typeof value.toJSON === "function") { + var converted; + try { + converted = value.toJSON(); + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `${errorLog}, A value could not be converted and was left out: [${ex}].`); + return undefined; + } + if (converted !== value) { + return copyJsonSafe(converted, ancestors, errorLog); + } + } + if (ancestors.indexOf(value) !== -1) { + cc.log(cc.logLevelEnums.ERROR, `${errorLog}, A value that refers back to itself was left out.`); + return undefined; + } + ancestors.push(value); + var copy; + if (Array.isArray(value)) { + copy = value.map((item) => copyJsonSafe(item, ancestors, errorLog)); + } + else { + copy = {}; + Object.keys(value).forEach((key) => { + var item = copyJsonSafe(value[key], ancestors, errorLog); + if (typeof item !== "undefined") { + copy[key] = item; + } + }); + } + ancestors.pop(); + return copy; +} + var cc = { // debug value from Countly debug: false, debugBulk: false, debugBulkUser: false, + warnedOnce: {}, + // called with the level of every error and warning log line, also when logging is off; the SDK counts them for its health check + onErrorOrWarning: null, + /** + * Prints a warning once per process, also when logging is off. Used only for setups in which the SDK + * cannot send or keep data at all, so integrators learn about them without turning on debug logs. + * @param {String} id - identifies the warning + * @param {String} message - warning text + */ + warnOnce: function warnOnce(id, message) { + if (this.warnedOnce[id]) { + return; + } + this.warnedOnce[id] = true; + // eslint-disable-next-line no-console + console.warn(message); + }, /** * log level Enums: * Error - this is a issues that needs attention right now. @@ -94,7 +163,21 @@ var cc = { ob[newKey] = newValue; } } - return ob; + return this.makeJsonSafe(ob, errorLog); + }, + + /** + * Returns a copy of a value that JSON.stringify can always serialize, with the same JSON as the + * original wherever the original could be serialized. BigInt numbers become strings, so database + * IDs keep every digit, and a reference back to an enclosing object or array is left out instead + * of making the whole value unserializable, which would otherwise surface later as an error thrown + * from the SDK's timers. + * @param {varies} value - value to copy + * @param {String=} errorLog - prefix for error log + * @returns {varies} serializable copy + */ + makeJsonSafe: function makeJsonSafe(value, errorLog) { + return copyJsonSafe(value, [], errorLog || "makeJsonSafe"); }, /** @@ -105,6 +188,9 @@ var cc = { * @returns {string|number} - the new truncated value */ truncateSingleValue: function truncateSingleValue(str, limit, errorLog) { + if (typeof str === "bigint") { + str = str.toString(); + } var newStr = str; if (typeof str === 'number') { str = str.toString(); @@ -138,7 +224,13 @@ var cc = { return ob; }, /** - * Convert params object to URL encoded query parameter string + * Convert params object to URL encoded query parameter string. + * Besides what encodeURIComponent encodes, ! ' ( ) * are percent-encoded as well: a server that + * rebuilds the URL before checking the checksum (Countly 26.01 and newer turn ' into %27) then hashes + * exactly the string the SDK hashed. + * Half of a character that needs two code units (a lone surrogate, for example from cutting a string in + * the middle of an emoji) cannot be percent-encoded, so it is sent as the replacement character U+FFFD. + * Otherwise the request could never be sent and would block every request queued after it. * @param {Object} params - object with query parameters * @returns {String} URL encoded query string */ @@ -147,7 +239,9 @@ var cc = { var keys = Object.keys(params || {}); for (var i = 0; i < keys.length; i++) { var key = keys[i]; - str.push(`${key}=${encodeURIComponent(params[key])}`); + var text = String(params[key]).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g, (unit) => (unit.length === 2 ? unit : "\uFFFD")); + var value = encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); + str.push(`${key}=${value}`); } return str.join("&"); }, @@ -234,9 +328,15 @@ var cc = { * @param {string} message - any string message */ log: function log(level, message, ...args) { + if (typeof this.onErrorOrWarning === "function" && (level === this.logLevelEnums.ERROR || level === this.logLevelEnums.WARNING)) { + this.onErrorOrWarning(level); + } if ((this.debug || this.debugBulk || this.debugBulkUser) && typeof console !== "undefined") { - if (args[0] && typeof args[0] === "object") { - args[0] = JSON.stringify(args[0]); + if (args[0] instanceof Error) { + args[0] = args[0].stack || String(args[0]); + } + else if (args[0] && typeof args[0] === "object") { + args[0] = JSON.stringify(this.makeJsonSafe(args[0])); } if (level === this.logLevelEnums.ERROR) { // eslint-disable-next-line no-console diff --git a/lib/countly-storage.js b/lib/countly-storage.js index e1dcf73..dbc8392 100644 --- a/lib/countly-storage.js +++ b/lib/countly-storage.js @@ -3,156 +3,211 @@ const path = require('path'); var cc = require("./countly-common"); // Constants -const defaultPath = "../data/"; // Default storage path -const defaultBulkPath = "../bulk_data/"; // Default bulk storage path const StorageTypes = cc.storageTypeEnums; const defaultStorageType = StorageTypes.FILE; const customTypeName = "custom"; +const defaultFolder = ".countly-storage"; // default storage folder, inside the app's working directory +const defaultBulkFolder = path.join(defaultFolder, "bulk"); +const legacyDefaultPath = "../data/"; // default up to 24.10.4, relative to this folder +const legacyDefaultBulkPath = "../bulk_data/"; +const coreKeys = ["cly_id", "cly_id_type", "cly_queue", "cly_event", "cly_remote_configs"]; +const bulkKeys = ["cly_req_queue", "cly_bulk_event", "cly_bulk_queue"]; +const lockedFileErrors = ["EPERM", "EACCES", "EBUSY"]; -var storagePath; -let storageMethod = {}; -var __cache = {}; -var asyncWriteLock = false; -var asyncWriteQueue = []; +var instancesWithPendingWrites = new Set(); +var exitListenerAdded = false; +var foldersInUse = {}; /** - * Sets the storage method, by default sets file storage and storage path. - * @param {String} userPath - User provided storage path - * @param {StorageTypes} storageType - Whether to use memory only storage or not - * @param {Boolean} isBulk - Whether the storage is for bulk data - * @param {varies} customStorageMethod - Storage methods provided by the user + * Writes all pending file writes of every storage instance synchronously. Runs on process exit, where + * asynchronous writes can no longer finish, so data changed right before process.exit() is kept. */ -var initStorage = function(userPath, storageType, isBulk = false, customStorageMethod = null) { - cc.log(cc.logLevelEnums.INFO, `Initializing storage with userPath: [${userPath}], storageType: [${storageType}], isBulk: [${isBulk}], customStorageMethod type: [${typeof customStorageMethod}].`); +function writePendingFilesOnExit() { + instancesWithPendingWrites.forEach((instance) => { + instance.flushSync(); + }); +} - // set storage type - storageType = storageType || defaultStorageType; - storageMethod = fileStorage; // file storage is default +/** + * Keeps a process "exit" listener registered only while some storage instance has unfinished writes + */ +function updateExitListener() { + if (instancesWithPendingWrites.size > 0 && !exitListenerAdded) { + process.on("exit", writePendingFilesOnExit); + exitListenerAdded = true; + } + else if (instancesWithPendingWrites.size === 0 && exitListenerAdded) { + process.removeListener("exit", writePendingFilesOnExit); + exitListenerAdded = false; + } +} - if (storageType === StorageTypes.MEMORY) { - storageMethod = memoryStorage; - cc.log(cc.logLevelEnums.DEBUG, `Using memory storage!`); +/** + * Renames a file, retrying a few times when the target is briefly locked (on Windows an antivirus or + * indexer can hold a file open for a moment). Each attempt is synchronous, so a rename can never still + * be running when the process exits and land after the synchronous exit write. + * @param {String} from - temporary file path + * @param {String} to - target file path + * @param {Number} attempt - retries done so far + * @param {Function} callback - called with an error or null + */ +function renameWithRetry(from, to, attempt, callback) { + var err = null; + try { + fs.renameSync(from, to); + } + catch (ex) { + err = ex; + } + if (err && lockedFileErrors.indexOf(err.code) !== -1 && attempt < 3) { + setTimeout(() => { + renameWithRetry(from, to, attempt + 1, callback); + }, 20 * (attempt + 1)); + return; } + callback(err); +} - // at this point we either use memory or file storage. If custom storage is provided, check if it is valid and use it instead - if (isCustomStorageValid(customStorageMethod)) { - storageMethod = customStorageMethod; - storageType = customTypeName; - cc.log(cc.logLevelEnums.DEBUG, `Using custom storage!`); +/** + * Removes a file if it exists, ignoring errors + * @param {String} file - file path + */ +function removeQuietly(file) { + try { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + } + catch (ex) { + cc.log(cc.logLevelEnums.DEBUG, `Could not remove [${file}]: [${ex}].`); } +} - // set storage path if not memory storage - if (storageType !== StorageTypes.MEMORY) { - setStoragePath(userPath, isBulk); +/** + * Tells whether a process with the given ID is running + * @param {Number} pid - process ID + * @returns {Boolean} true if the process is running + */ +function isProcessAlive(pid) { + try { + process.kill(pid, 0); + return true; } -}; + catch (ex) { + return ex.code === "EPERM"; + } +} -// Memory-only storage methods -const memoryStorage = { - /** - * Save value in memory - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - storeSet: function(key, value, callback) { - if (key) { - // cc.log(cc.logLevelEnums.VERBOSE, `storeSet, Setting key: [${key}] & value: [${value}]!`); - __cache[key] = value; - if (typeof callback === "function") { - callback(null); - } - } - else { - cc.log(cc.logLevelEnums.WARNING, `storeSet, Provioded key: [${key}] is null!`); +/** + * Removes temporary files that processes which are no longer running left in a storage folder when they + * ended in the middle of a write + * @param {String} folder - storage folder + */ +function removeStaleTempFiles(folder) { + var names = []; + try { + names = fs.readdirSync(folder); + } + catch (ex) { + return; + } + names.forEach((name) => { + var match = /^__.+\.json\.(\d+)(\.sync)?\.tmp$/.exec(name); + if (match && Number(match[1]) !== process.pid && !isProcessAlive(Number(match[1]))) { + removeQuietly(path.join(folder, name)); } - }, - /** - * Get value from memory - * @param {String} key - key of value to get - * @param {varies} def - default value to use if not set - * @returns {varies} value for the key - */ - storeGet: function(key, def) { - // cc.log(cc.logLevelEnums.VERBOSE, `storeGet, Fetching item from memory with key: [${key}].`); - return typeof __cache[key] !== "undefined" ? __cache[key] : def; - }, - /** - * Remove value from memory - * @param {String} key - key of value to remove - */ - storeRemove: function(key) { - delete __cache[key]; - }, -}; + }); +} -// File storage methods -const fileStorage = { - /** - * Save value in storage - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - storeSet: function(key, value, callback) { - __cache[key] = value; - if (!asyncWriteLock) { - asyncWriteLock = true; - writeFile(key, value, callback); - } - else { - asyncWriteQueue.push([key, value, callback]); - } - }, - /** - * Get value from storage - * @param {String} key - key of value to get - * @param {varies} def - default value to use if not set - * @returns {varies} value for the key - */ - storeGet: function(key, def) { - cc.log(cc.logLevelEnums.DEBUG, `storeGet, Fetching item from storage with key: [${key}].`); - if (typeof __cache[key] === "undefined") { - var ob = readFile(key); - var obLen; - // check if the 'read object' is empty or not - try { - obLen = Object.keys(ob).length; - } - catch (error) { - // if we can not even asses length set it to 0 so we can return the default value - obLen = 0; - } +/** + * Tells whether the process can create files in a folder, by writing and removing a small file. A + * permission check alone is not enough on Windows, where it ignores the folder's access rules. + * @param {String} folder - folder path + * @returns {Boolean} true if writable + */ +function isWritable(folder) { + var probe = path.join(folder, `.countly-write-check.${process.pid}`); + try { + fs.writeFileSync(probe, ""); + fs.unlinkSync(probe); + return true; + } + catch (ex) { + return false; + } +} - // if empty or falsy set default value - if (!ob || obLen === 0) { - __cache[key] = def; - } - // else set the value read file has - else { - __cache[key] = ob[key]; - } +/** + * Writes a file so that it always holds either its previous or its new content, never an empty or + * half written one: the data goes to a temporary file that then replaces the target in one rename. + * When the rename keeps failing the target is written directly, so the data is still saved. + * @param {String} file - target file path + * @param {String} data - content to write + * @param {Function} callback - called with an error or null + * @param {Function=} isCancelled - returns true when the write must not replace the target anymore (the storage was reset meanwhile) + */ +function writeFileAtomic(file, data, callback, isCancelled) { + var tmp = `${file}.${process.pid}.tmp`; + fs.writeFile(tmp, data, (writeErr) => { + if (writeErr) { + callback(writeErr); + return; } - return __cache[key]; - }, - storeRemove: function(key) { - delete __cache[key]; - var filePath = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); - fs.access(filePath, fs.constants.F_OK, (accessErr) => { - if (accessErr) { - cc.log(cc.logLevelEnums.WARNING, `storeRemove, No file found with key: [${key}]. Nothing to remove.`); + if (isCancelled && isCancelled()) { + fs.unlink(tmp, () => { + callback(null); + }); + return; + } + renameWithRetry(tmp, file, 0, (renameErr) => { + if (!renameErr || (isCancelled && isCancelled())) { + if (renameErr) { + fs.unlink(tmp, () => { }); + } + callback(null); return; } - fs.unlink(filePath, (err) => { - if (err) { - cc.log(cc.logLevelEnums.ERROR, `storeRemove, Failed to remove file with key: [${key}]. Error: [${err.message}].`); - } - else { - cc.log(cc.logLevelEnums.INFO, `storeRemove, Successfully removed file with key: [${key}].`); - } + fs.writeFile(file, data, (directErr) => { + fs.unlink(tmp, () => { }); + callback(directErr); }); }); - }, + }); +} + +/** + * Synchronous version of writeFileAtomic, for places where only synchronous work can run (process + * exit, fatal crash handling) + * @param {String} file - target file path + * @param {String} data - content to write + */ +function writeFileAtomicSync(file, data) { + var tmp = `${file}.${process.pid}.sync.tmp`; + try { + fs.writeFileSync(tmp, data); + fs.renameSync(tmp, file); + } + catch (ex) { + fs.writeFileSync(file, data); + if (fs.existsSync(tmp)) { + fs.unlinkSync(tmp); + } + } +} + +/** + * Creates a directory if it doesn't exist + * @param {String} dir - The directory path + */ +var createDirectory = function(dir) { + try { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `Failed to create directory at ${dir}: ${ex.stack}`); + } }; var isCustomStorageValid = function(storage) { @@ -172,177 +227,552 @@ var isCustomStorageValid = function(storage) { }; /** - * Sets the storage path, defaulting to a specified path if none is provided. - * @param {String} userPath - User provided storage path - * @param {Boolean} isBulk - Whether the storage is for bulk data + * Creates an independent storage with its own location, type, in-memory cache and file writes. + * The SDK uses the default storage of this module and every CountlyBulk instance creates its own, so one + * instance can never read, overwrite or change the type of another instance's data. + * File storage writes each key to "/__.json" through writeFileAtomic. While a write + * of a key is running, newer values of that key are combined into one follow-up write, and whatever is + * still unwritten when the process exits is written synchronously. + * @returns {Object} storage instance */ -var setStoragePath = function(userPath, isBulk = false) { - storagePath = userPath || (isBulk ? defaultBulkPath : defaultPath); +function createStorage() { + var storagePath; + var legacyBase; + var useLegacyLayout = false; + var registeredFolder = null; + var storageMethod = {}; + var cache = {}; + var writes = {}; + var generation = 0; + var instance = {}; - createDirectory(path.resolve(__dirname, storagePath)); -}; - -/** - * Returns the current storage path - * @returns {String} storagePath - */ -var getStoragePath = function() { - return storagePath; -}; - -/** - * Creates a directory if it doesn't exist - * @param {String} dir - The directory path - */ -var createDirectory = function(dir) { - try { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + /** + * Path of the file that holds a key + * @param {String} key - key of the value + * @returns {String} file path + */ + function getFilePath(key) { + if (useLegacyLayout) { + return getLegacyFilePath(key); } + return path.join(storagePath, `__${key}.json`); } - catch (ex) { - cc.log(cc.logLevelEnums.ERROR, `Failed to create directory at ${dir}: ${ex.stack}`); + + /** + * Path where SDK versions up to 24.10.4 kept a key: the configured path, or the old default, resolved + * against this folder, with the file name appended without a separator + * @param {String} key - key of the value + * @returns {String} file path + */ + function getLegacyFilePath(key) { + return path.resolve(__dirname, `${legacyBase}__${key}.json`); } -}; -/** - * Resets storage-related variables to their initial state - */ -var resetStorage = function() { - storagePath = undefined; - __cache = {}; - asyncWriteLock = false; - asyncWriteQueue = []; - storageMethod = {}; -}; + /** + * Serializes the cached value of a key in the stored file format + * @param {String} key - key of the value + * @returns {String|null} file content, or null when the value cannot be serialized + */ + function serialize(key) { + var ob = {}; + ob[key] = cache[key]; + try { + return JSON.stringify(ob); + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `storeSet, Value for key: [${key}] could not be stored: [${ex}].`); + return null; + } + } -/** - * Read value from file - * @param {String} key - key for file - * @returns {varies} value in file - */ -var readFile = function(key) { - var dir = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); - // try reading data file - var data; - try { - data = fs.readFileSync(dir, 'utf8'); // read file as string + /** + * Moves the files of an older SDK version to the current storage folder once: only when nothing is + * stored in the current folder yet. The old files are kept with a ".migrated" suffix. If the files + * cannot be copied, the old location stays in use, as before this version. + * @param {Array} keys - keys stored by this kind of storage + */ + function migrateLegacyFiles(keys) { + var found = []; + for (var i = 0; i < keys.length; i++) { + var target = getFilePath(keys[i]); + var legacy = getLegacyFilePath(keys[i]); + if (fs.existsSync(target)) { + return; + } + if (legacy !== target && fs.existsSync(legacy)) { + found.push(keys[i]); + } + } + if (!found.length) { + return; + } + var copied = []; + try { + found.forEach((key) => { + var tmp = `${getFilePath(key)}.${process.pid}.tmp`; + fs.copyFileSync(getLegacyFilePath(key), tmp); + fs.renameSync(tmp, getFilePath(key)); + copied.push(key); + }); + } + catch (ex) { + found.forEach((key) => { + removeQuietly(`${getFilePath(key)}.${process.pid}.tmp`); + }); + copied.forEach((key) => { + removeQuietly(getFilePath(key)); + }); + useLegacyLayout = true; + cc.log(cc.logLevelEnums.WARNING, `Could not move the stored data to [${storagePath}], the old location stays in use: [${ex}].`); + return; + } + found.forEach((key) => { + var legacyFile = getLegacyFilePath(key); + try { + fs.renameSync(legacyFile, `${legacyFile}.migrated`); + } + catch (ex) { + cc.log(cc.logLevelEnums.WARNING, `Could not mark [${legacyFile}] as moved: [${ex}].`); + } + }); + cc.log(cc.logLevelEnums.INFO, `Moved the stored data of an older SDK version to [${storagePath}].`); } - catch (ex) { - // there was no file, probably new init - cc.log(cc.logLevelEnums.WARN, `readFile, File not found for key: [${key}]. Returning null.`); - return null; // early exit if file doesn't exist + + /** + * Warns when the storage folder cannot be written, or when another storage of the same kind (SDK or + * CountlyBulk) in this process already stores its files there; storages of different kinds use + * different file names and can share a folder + * @param {String} kind - "core" or "bulk" + */ + function checkFolder(kind) { + var folder = useLegacyLayout ? path.dirname(getLegacyFilePath("check")) : storagePath; + if (!isWritable(folder)) { + cc.warnOnce(`storage-not-writable:${folder}`, `Countly: the storage folder [${folder}] is not writable, so the device ID and unsent data will not survive a restart. Set "storage_path" to a writable folder, or use the memory storage type.`); + } + var registryKey = `${kind}:${folder}`; + if (foldersInUse[registryKey]) { + cc.warnOnce(`storage-shared:${registryKey}`, `Countly: two Countly instances in this process store their data in [${folder}] and can overwrite each other's files. Give each instance its own "storage_path".`); + } + foldersInUse[registryKey] = true; + registeredFolder = registryKey; } - // early exit if file is empty or whitespace - if (!data.trim()) { - cc.log(cc.logLevelEnums.WARN, `readFile, File is empty or contains only whitespace for key: [${key}]. Returning null.`); - return null; + /** + * Finishes a file write, runs its callbacks and starts the follow-up write if the value changed meanwhile + * @param {String} key - key of the value + * @param {Number} writeGeneration - generation the write was started in + * @param {Error|null} err - write error + * @param {Array} callbacks - callbacks waiting for this write + */ + function finishWrite(key, writeGeneration, err, callbacks) { + if (writeGeneration !== generation || !writes[key]) { + return; + } + if (err) { + cc.log(cc.logLevelEnums.ERROR, `writeFile, Writing files failed. Error: [${err}].`); + } + callbacks.forEach((callback) => { + if (typeof callback === "function") { + callback(err); + } + }); + writes[key].writing = false; + if (writes[key].dirty) { + startWrite(key); + return; + } + delete writes[key]; + if (Object.keys(writes).length === 0) { + instancesWithPendingWrites.delete(instance); + updateExitListener(); + } } - try { - // trying to parse json string - data = JSON.parse(data); + /** + * Writes the current value of a key to its file + * @param {String} key - key of the value + */ + function startWrite(key) { + var state = writes[key]; + var { callbacks } = state; + var writeGeneration = generation; + var file = getFilePath(key); + state.writing = true; + state.dirty = false; + state.callbacks = []; + if (!Object.prototype.hasOwnProperty.call(cache, key)) { + fs.unlink(file, (err) => { + finishWrite(key, writeGeneration, err && err.code !== "ENOENT" ? err : null, callbacks); + }); + return; + } + var data = serialize(key); + if (data === null) { + finishWrite(key, writeGeneration, new Error(`value of [${key}] cannot be serialized`), callbacks); + return; + } + writeFileAtomic(file, data, (err) => { + finishWrite(key, writeGeneration, err, callbacks); + }, () => writeGeneration !== generation); } - catch (ex) { - // problem parsing, corrupted file? - cc.log(cc.logLevelEnums.ERROR, `readFile, Failed to parse the file with key: [${key}]. Error: [${ex}].`); - // backup corrupted file data - fs.writeFile(path.resolve(__dirname, `${getStoragePath()}__${key}.${cc.getTimestamp()}${Math.random()}.json`), data, () => { }); - // start with new clean object - return null; // return null in case of corrupted data + + /** + * Schedules writing a key to its file; a write that is already running is followed by one more write + * with the latest value instead of one write per change + * @param {String} key - key of the value + * @param {Function=} callback - called when the value is written + */ + function scheduleWrite(key, callback) { + if (!writes[key]) { + writes[key] = { writing: false, dirty: false, callbacks: [] }; + } + var state = writes[key]; + if (callback) { + state.callbacks.push(callback); + } + instancesWithPendingWrites.add(instance); + updateExitListener(); + if (state.writing) { + state.dirty = true; + return; + } + startWrite(key); } - return data; -}; -/** - * Force store data synchronously on unrecoverable errors to preserve it for next launch - */ -var forceStore = function() { - for (var i in __cache) { - var dir = path.resolve(__dirname, `${getStoragePath()}__${i}.json`); - var ob = {}; - ob[i] = __cache[i]; + /** + * Read value from file. A missing file is normal before the first write, so only other read errors are + * logged as warnings (the health check counts warnings). + * @param {String} key - key for file + * @returns {varies} value in file + */ + function readFile(key) { + var file = getFilePath(key); + var data; + try { + data = fs.readFileSync(file, 'utf8'); + } + catch (ex) { + if (ex && ex.code === "ENOENT") { + cc.log(cc.logLevelEnums.DEBUG, `readFile, File not found for key: [${key}]. Returning null.`); + } + else { + cc.log(cc.logLevelEnums.WARNING, `readFile, Could not read the file for key: [${key}]. Error: [${ex}]. Returning null.`); + } + return null; + } + + if (!data.trim()) { + cc.log(cc.logLevelEnums.WARNING, `readFile, File is empty or contains only whitespace for key: [${key}]. Returning null.`); + return null; + } + try { - fs.writeFileSync(dir, JSON.stringify(ob)); + data = JSON.parse(data); } catch (ex) { - // tried to save whats possible - cc.log(cc.logLevelEnums.ERROR, `forceStore, Saving files failed. Error: [${ex}].`); + cc.log(cc.logLevelEnums.ERROR, `readFile, Failed to parse the file with key: [${key}]. Error: [${ex}].`); + fs.writeFile(`${file}.${cc.getTimestamp()}${Math.random()}.json`, data, () => { }); + return null; } + return data; } -}; -/** - * Write to file and process queue while in asyncWriteLock - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ -var writeFile = function(key, value, callback) { - var ob = {}; - ob[key] = value; - var dir = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); - fs.writeFile(dir, JSON.stringify(ob), (err) => { - if (err) { - cc.log(cc.logLevelEnums.ERROR, `writeFile, Writing files failed. Error: [${err}].`); + const memoryStorage = { + /** + * Save value in memory + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ + storeSet: function(key, value, callback) { + if (key) { + cache[key] = value; + if (typeof callback === "function") { + callback(null); + } + } + else { + cc.log(cc.logLevelEnums.WARNING, `storeSet, Provioded key: [${key}] is null!`); + } + }, + /** + * Get value from memory + * @param {String} key - key of value to get + * @param {varies} def - default value to use if not set + * @returns {varies} value for the key + */ + storeGet: function(key, def) { + return typeof cache[key] !== "undefined" ? cache[key] : def; + }, + /** + * Remove value from memory + * @param {String} key - key of value to remove + */ + storeRemove: function(key) { + delete cache[key]; + }, + }; + + const fileStorage = { + /** + * Save value in storage + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ + storeSet: function(key, value, callback) { + cache[key] = value; + scheduleWrite(key, callback); + }, + /** + * Get value from storage + * @param {String} key - key of value to get + * @param {varies} def - default value to use if not set + * @returns {varies} value for the key + */ + storeGet: function(key, def) { + cc.log(cc.logLevelEnums.DEBUG, `storeGet, Fetching item from storage with key: [${key}].`); + if (typeof cache[key] === "undefined") { + var ob = readFile(key); + var obLen; + try { + obLen = Object.keys(ob).length; + } + catch (error) { + obLen = 0; + } + + if (!ob || obLen === 0) { + cache[key] = def; + } + else { + cache[key] = ob[key]; + } + } + return cache[key]; + }, + /** + * Remove value from storage + * @param {String} key - key of value to remove + */ + storeRemove: function(key) { + delete cache[key]; + scheduleWrite(key); + }, + }; + + /** + * Sets the storage method, by default sets file storage and storage path. + * @param {String} userPath - User provided storage path + * @param {StorageTypes} storageType - Whether to use memory only storage or not + * @param {Boolean} isBulk - Whether the storage is for bulk data + * @param {varies} customStorageMethod - Storage methods provided by the user + * @param {String=} folderName - name of the folder inside the default bulk folder, used only when no path is given + */ + instance.initStorage = function(userPath, storageType, isBulk = false, customStorageMethod = null, folderName = null) { + cc.log(cc.logLevelEnums.INFO, `Initializing storage with userPath: [${userPath}], storageType: [${storageType}], isBulk: [${isBulk}], customStorageMethod type: [${typeof customStorageMethod}].`); + + storageType = storageType || defaultStorageType; + storageMethod = fileStorage; + + if (storageType === StorageTypes.MEMORY) { + storageMethod = memoryStorage; + cc.log(cc.logLevelEnums.DEBUG, `Using memory storage!`); + } + + if (isCustomStorageValid(customStorageMethod)) { + storageMethod = customStorageMethod; + storageType = customTypeName; + cc.log(cc.logLevelEnums.DEBUG, `Using custom storage!`); + } + + if (storageType !== StorageTypes.MEMORY) { + instance.setStoragePath(userPath, isBulk, folderName); } - if (typeof callback === "function") { - callback(err); + }; + + /** + * Sets the storage folder. A relative path is resolved against the app's working directory; without + * a path the folder is ".countly-storage" there (bulk: ".countly-storage/bulk/"). + * For file storage the folder is created and data stored by an older SDK version at its old + * location is moved over once. If the folder cannot be written but the old location can, the old + * location stays in use, so data is kept wherever older versions kept it. + * @param {String} userPath - User provided storage path + * @param {Boolean} isBulk - Whether the storage is for bulk data + * @param {String=} folderName - name of the folder inside the default bulk folder, used only when no path is given + */ + instance.setStoragePath = function(userPath, isBulk = false, folderName = null) { + var givenPath = typeof userPath === "string" && userPath.length ? userPath : null; + var defaultLocation = isBulk ? defaultBulkFolder : defaultFolder; + if (isBulk && folderName) { + defaultLocation = path.join(defaultBulkFolder, String(folderName).replace(/[^A-Za-z0-9_-]/g, "_")); } - if (asyncWriteQueue.length) { - setTimeout(() => { - var arr = asyncWriteQueue.shift(); - cc.log(cc.logLevelEnums.DEBUG, "writeFile, Dequeued array:", arr); - if (arr) { - writeFile(arr[0], arr[1], arr[2]); + storagePath = path.resolve(process.cwd(), givenPath || defaultLocation); + legacyBase = givenPath || (isBulk ? legacyDefaultBulkPath : legacyDefaultPath); + useLegacyLayout = false; + if (storageMethod === fileStorage) { + createDirectory(storagePath); + if (!isWritable(storagePath)) { + var legacyFolder = path.dirname(getLegacyFilePath("check")); + createDirectory(legacyFolder); + if (isWritable(legacyFolder)) { + useLegacyLayout = true; + cc.log(cc.logLevelEnums.WARNING, `Storage folder [${storagePath}] is not writable, data is kept in the old location [${legacyFolder}].`); } - }, 0); + } + if (!useLegacyLayout) { + migrateLegacyFiles(isBulk ? bulkKeys : coreKeys); + } + removeStaleTempFiles(instance.getStoragePath()); + checkFolder(isBulk ? "bulk" : "core"); } - else { - asyncWriteLock = false; + }; + + /** + * Returns the current storage folder + * @returns {String} absolute folder path, or undefined for memory storage + */ + instance.getStoragePath = function() { + if (useLegacyLayout) { + return path.dirname(getLegacyFilePath("check")); } - }); -}; + return storagePath; + }; -var storeSet = function(key, value, callback) { - storageMethod.storeSet(key, value, callback); -}; + /** + * Writes the current value of every key with an unfinished write synchronously, and removes the + * temporary file of an asynchronous write that can no longer finish + */ + instance.flushSync = function() { + if (storageMethod !== fileStorage) { + return; + } + Object.keys(writes).forEach((key) => { + try { + if (!Object.prototype.hasOwnProperty.call(cache, key)) { + if (fs.existsSync(getFilePath(key))) { + fs.unlinkSync(getFilePath(key)); + } + return; + } + var data = serialize(key); + if (data !== null) { + writeFileAtomicSync(getFilePath(key), data); + } + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `flushSync, Saving [${key}] failed. Error: [${ex}].`); + } + removeQuietly(`${getFilePath(key)}.${process.pid}.tmp`); + }); + }; -var storeGet = function(key, def) { - return storageMethod.storeGet(key, def); -}; + /** + * Force store data synchronously on unrecoverable errors to preserve it for next launch. + * Memory and custom storages are left alone. + */ + instance.forceStore = function() { + if (storageMethod !== fileStorage) { + return; + } + Object.keys(cache).forEach((key) => { + var data = serialize(key); + if (data === null) { + return; + } + try { + writeFileAtomicSync(getFilePath(key), data); + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `forceStore, Saving files failed. Error: [${ex}].`); + } + }); + }; -var storeRemove = function(key) { - storageMethod.storeRemove(key); -}; + /** + * Resets the storage to its initial state. Unfinished file writes are saved first, like on a restart. + */ + instance.resetStorage = function() { + instance.flushSync(); + generation++; + if (registeredFolder) { + delete foldersInUse[registeredFolder]; + registeredFolder = null; + } + storagePath = undefined; + legacyBase = undefined; + useLegacyLayout = false; + cache = {}; + writes = {}; + storageMethod = {}; + instancesWithPendingWrites.delete(instance); + updateExitListener(); + }; -/** - * Disclaimer: This method is mainly for testing purposes. - * @returns {StorageTypes} Returns the active storage type for the SDK - */ -var getStorageType = function() { - if (storageMethod === memoryStorage) { - return StorageTypes.MEMORY; - } + instance.readFile = readFile; - if (storageMethod === fileStorage) { - return StorageTypes.FILE; - } + /** + * Write a value to the file of a key, without changing the cached value + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ + instance.writeFile = function(key, value, callback) { + var ob = {}; + ob[key] = value; + writeFileAtomic(getFilePath(key), JSON.stringify(ob), (err) => { + if (typeof callback === "function") { + callback(err); + } + }); + }; - return null; -}; + instance.storeSet = function(key, value, callback) { + storageMethod.storeSet(key, value, callback); + }; + + instance.storeGet = function(key, def) { + return storageMethod.storeGet(key, def); + }; + + instance.storeRemove = function(key) { + storageMethod.storeRemove(key); + }; + + /** + * Disclaimer: This method is mainly for testing purposes. + * @returns {StorageTypes} Returns the active storage type for the SDK + */ + instance.getStorageType = function() { + if (storageMethod === memoryStorage) { + return StorageTypes.MEMORY; + } + + if (storageMethod === fileStorage) { + return StorageTypes.FILE; + } + + return null; + }; + + return instance; +} + +var defaultStorage = createStorage(); module.exports = { - initStorage, - storeSet, - storeGet, - storeRemove, - writeFile, - forceStore, - getStoragePath, - setStoragePath, - resetStorage, - readFile, - getStorageType, -}; \ No newline at end of file + initStorage: defaultStorage.initStorage, + storeSet: defaultStorage.storeSet, + storeGet: defaultStorage.storeGet, + storeRemove: defaultStorage.storeRemove, + writeFile: defaultStorage.writeFile, + forceStore: defaultStorage.forceStore, + flushSync: defaultStorage.flushSync, + getStoragePath: defaultStorage.getStoragePath, + setStoragePath: defaultStorage.setStoragePath, + resetStorage: defaultStorage.resetStorage, + readFile: defaultStorage.readFile, + getStorageType: defaultStorage.getStorageType, + createStorage, + defaultFolder, +}; diff --git a/lib/countly.js b/lib/countly.js index f0a0181..c5a1197 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -27,6 +27,7 @@ var fs = require("fs"); var path = require("path"); var cluster = require("cluster"); var cc = require("./countly-common"); +var behavior = require("./countly-behavior"); var Bulk = require("./countly-bulk"); var CountlyStorage = require("./countly-storage"); @@ -35,7 +36,7 @@ Countly.StorageTypes = cc.storageTypeEnums; Countly.DeviceIdType = cc.deviceIdTypeEnums; Countly.Bulk = Bulk; (function() { - var SDK_VERSION = "24.10.4"; + var SDK_VERSION = "26.8.0"; var SDK_NAME = "javascript_native_nodejs"; var inited = false; @@ -75,6 +76,31 @@ Countly.Bulk = Bulk; var maxStackTraceLineLength = 200; var deviceIdType = null; var heartBeatTimer = null; + var uncaughtExceptionHandler = null; + var unhandledRejectionHandler = null; + var clusterForkHandler = null; + var workerListeners = []; + var workerMessageHandler = null; + var primaryCheckTimer = null; + var requestTimeout = 30; + var standaloneWorker = false; + var sdkGeneration = 0; + var flushWaiters = []; + var processingDisabled = false; + var inFlightRequest = null; + var serverSettings = {}; + var developerSettings = {}; + var settingsUpdatesDisabled = false; + var healthCheckDisabled = false; + var backoffDisabled = false; + var settingsTimer = null; + var settingsFetchAfterMerge = false; + var healthCheckSent = false; + var health = newHealthCounters(); + var healthDirty = false; + var healthSavedAt = 0; + var backoffUntil = 0; + var backoffStreak = 0; /** * Array with list of available features that you can require consent for */ @@ -86,6 +112,16 @@ Countly.Bulk = Bulk; consents[Countly.features[feat]] = {}; } + cc.onErrorOrWarning = function(level) { + if (level === cc.logLevelEnums.ERROR) { + health.el++; + } + else { + health.wl++; + } + healthDirty = true; + }; + /** * Initialize Countly object * @param {Object} conf - Countly initialization {@link Init} object with configuration options @@ -106,7 +142,9 @@ Countly.Bulk = Bulk; * @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests * @param {boolean} [conf.clear_stored_device_id=false] - set it to true if you want to erase the stored device ID * @param {boolean} [conf.test_mode=false] - set it to true if you want to initiate test_mode - * @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc + * @param {string} [conf.storage_path=.countly-storage] - folder where the SDK stores data, including id, queues, etc. A relative path is resolved against the app's working directory + * @param {number} [conf.request_timeout=30] - seconds to wait for the server's answer; after that the request counts as failed and is sent again later + * @param {boolean} [conf.standalone_worker=false] - in a cluster worker, send the worker's data directly instead of handing it to the main process. Needed when the main process does not run the SDK, for example in PM2 cluster mode. Each worker then keeps its data in its own "worker-" folder inside the storage folder * @param {boolean} [conf.require_consent=false] - pass true if you are implementing GDPR compatible consent management. It would prevent running any functionality without proper consent * @param {boolean|function} [conf.remote_config=false] - Enable automatic remote config fetching, provide callback function to be notified when fetching done * @param {function} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request @@ -128,6 +166,10 @@ Countly.Bulk = Bulk; * @param {string} conf.metrics._store - source from where the user/device/installation came from * @param {StorageTypes} conf.storage_type - to determine which storage type is going to be applied * @param {Object} conf.custom_storage_method - user given storage methods + * @param {Object=} conf.behavior_settings - SDK behavior settings to use until the server's settings have been fetched once, in the format SDK Manager exports: an object with a "c" object + * @param {boolean} [conf.disable_sdk_behavior_settings_updates=false] - do not fetch the SDK behavior settings from the server; stored and provided settings still apply + * @param {boolean} [conf.disable_health_check=false] - do not send the health check at start + * @param {boolean} [conf.disable_backoff_mechanism=false] - never pause sending when the server answers slowly, whatever the server settings say * @example * Countly.init({ * app_key: "{YOUR-APP-KEY}", @@ -152,6 +194,16 @@ Countly.Bulk = Bulk; failTimeoutAmount = conf.fail_timeout || Countly.fail_timeout || failTimeoutAmount; sessionUpdate = conf.session_update || Countly.session_update || sessionUpdate; maxEventBatch = conf.max_events || Countly.max_events || maxEventBatch; + requestTimeout = Number(conf.request_timeout || Countly.request_timeout || requestTimeout); + if (!(requestTimeout > 0)) { + cc.log(cc.logLevelEnums.WARNING, `init, request_timeout must be a number of seconds, using 30 seconds instead of [${conf.request_timeout || Countly.request_timeout}].`); + requestTimeout = 30; + } + else if (requestTimeout < 1) { + cc.log(cc.logLevelEnums.WARNING, `init, request_timeout must be at least 1 second, using 1 second instead of [${requestTimeout}].`); + requestTimeout = 1; + } + standaloneWorker = cluster.isWorker && !!(conf.standalone_worker || Countly.standalone_worker); metrics = conf.metrics || Countly.metrics || {}; conf.debug = conf.debug || Countly.debug || false; conf.clear_stored_device_id = conf.clear_stored_device_id || false; @@ -173,12 +225,32 @@ Countly.Bulk = Bulk; Countly.maxBreadcrumbCount = conf.max_breadcrumb_count || Countly.max_breadcrumb_count || conf.max_logs || Countly.max_logs || maxBreadcrumbCount; Countly.maxStackTraceLinesPerThread = conf.max_stack_trace_lines_per_thread || Countly.max_stack_trace_lines_per_thread || maxStackTraceLinesPerThread; Countly.maxStackTraceLineLength = conf.max_stack_trace_line_length || Countly.max_stack_trace_line_length || maxStackTraceLineLength; + settingsUpdatesDisabled = !!(conf.disable_sdk_behavior_settings_updates || Countly.disable_sdk_behavior_settings_updates); + healthCheckDisabled = !!(conf.disable_health_check || Countly.disable_health_check); + backoffDisabled = !!(conf.disable_backoff_mechanism || Countly.disable_backoff_mechanism); + developerSettings = { + rqs: queueSize, + eqs: maxEventBatch, + sui: sessionUpdate, + lkl: Countly.maxKeyLength, + lvs: Countly.maxValueSize, + lsv: Countly.maxSegmentationValues, + lbc: Countly.maxBreadcrumbCount, + ltlpt: Countly.maxStackTraceLinesPerThread, + ltl: Countly.maxStackTraceLineLength, + cr: !!Countly.require_consent, + }; conf.storage_path = conf.storage_path || Countly.storage_path; conf.storage_type = conf.storage_type || Countly.storage_type; // Common module debug value is set to init time debug value cc.debug = conf.debug; - CountlyStorage.initStorage(conf.storage_path, conf.storage_type, false, conf.custom_storage_method); + var storagePath = conf.storage_path; + if (standaloneWorker) { + var workerNumber = typeof process.env.NODE_APP_INSTANCE !== "undefined" ? process.env.NODE_APP_INSTANCE : cluster.worker.id; + storagePath = path.join(path.resolve(process.cwd(), conf.storage_path || CountlyStorage.defaultFolder), `worker-${workerNumber}`); + } + CountlyStorage.initStorage(storagePath, conf.storage_type, false, conf.custom_storage_method); // clear stored device ID if flag is set if (conf.clear_stored_device_id) { @@ -229,12 +301,12 @@ Countly.Bulk = Bulk; cc.log(cc.logLevelEnums.DEBUG, `init, General value limits are,\n for key length: [${Countly.maxKeyLength}],\n for value size: [${Countly.maxValueSize}],\n for segmentation: [${Countly.maxSegmentationValues}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Crash related value limits are,\n for breadcrumb count: [${Countly.maxBreadcrumbCount}],\n for trace lines per thread: [${Countly.maxStackTraceLinesPerThread}],\n for trace line length: [${Countly.maxStackTraceLineLength}].`); - if (cluster.isMaster) { + if (ownsQueue()) { // fetch stored ID and ID type var storedId = CountlyStorage.storeGet("cly_id", null); var storedIdType = CountlyStorage.storeGet("cly_id_type", null); // if there was a stored ID - if (storedId !== null) { + if (storedId !== null && storedId !== "") { Countly.device_id = storedId; // deviceIdType = storedIdType || cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; if (storedIdType === null) { @@ -267,20 +339,34 @@ Countly.Bulk = Bulk; requestQueue = CountlyStorage.storeGet("cly_queue", []); eventQueue = CountlyStorage.storeGet("cly_event", []); remoteConfigs = CountlyStorage.storeGet("cly_remote_configs", {}); + serverSettings = behavior.parseStored(CountlyStorage.storeGet("cly_sbs", null)) + || behavior.parseProvided(conf.behavior_settings || Countly.behavior_settings, warnAboutSettings) + || {}; + health = loadHealthCounters(CountlyStorage.storeGet("cly_hc", null), health); + process.removeListener("exit", saveHealthCountersOnExit); + process.on("exit", saveHealthCountersOnExit); + applyBehaviorSettings(); heartBeat(); - // listen to current workers - if (cluster.workers) { - for (var id in cluster.workers) { - cluster.workers[id].on("message", handleWorkerMessage); + if (cluster.isMaster) { + // listen to current workers + if (cluster.workers) { + for (var id in cluster.workers) { + listenToWorker(cluster.workers[id], true); + } } + // handle future workers + clusterForkHandler = (worker) => { + listenToWorker(worker, false); + }; + cluster.on("fork", clusterForkHandler); } - // handle future workers - cluster.on("fork", (worker) => { - worker.on("message", handleWorkerMessage); - }); if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } + fetchBehaviorSettings(sendHealthCheck); + } + else { + startPrimaryCheck(); } } } @@ -316,7 +402,8 @@ Countly.Bulk = Bulk; failTimeoutAmount = 60; sessionUpdate = 60; maxEventBatch = 100; - readyToProcess = !preventRequestProcessing; + readyToProcess = true; + processingDisabled = !!preventRequestProcessing; trackTime = true; metrics = {}; lastParams = {}; @@ -331,6 +418,49 @@ Countly.Bulk = Bulk; clearInterval(heartBeatTimer); heartBeatTimer = null; } + if (clusterForkHandler) { + cluster.removeListener("fork", clusterForkHandler); + clusterForkHandler = null; + } + workerListeners.forEach((entry) => { + entry.worker.removeListener("message", entry.listener); + }); + workerListeners = []; + stopPrimaryCheck(); + sdkGeneration++; + inFlightRequest = null; + settleFlushWaiters(false); + if (settingsTimer) { + clearTimeout(settingsTimer); + settingsTimer = null; + } + process.removeListener("exit", saveHealthCountersOnExit); + if (consentTimer) { + clearTimeout(consentTimer); + consentTimer = null; + } + serverSettings = {}; + developerSettings = {}; + settingsUpdatesDisabled = false; + healthCheckDisabled = false; + backoffDisabled = false; + settingsFetchAfterMerge = false; + healthCheckSent = false; + health = newHealthCounters(); + healthDirty = false; + healthSavedAt = 0; + backoffUntil = 0; + backoffStreak = 0; + requestTimeout = 30; + standaloneWorker = false; + if (uncaughtExceptionHandler) { + process.removeListener("uncaughtException", uncaughtExceptionHandler); + uncaughtExceptionHandler = null; + } + if (unhandledRejectionHandler) { + process.removeListener("unhandledRejection", unhandledRejectionHandler); + unhandledRejectionHandler = null; + } // cc DEBUG cc.debug = false; @@ -359,6 +489,10 @@ Countly.Bulk = Bulk; Countly.salt = undefined; Countly.require_consent = undefined; Countly.http_options = undefined; + Countly.behavior_settings = undefined; + Countly.disable_sdk_behavior_settings_updates = undefined; + Countly.disable_health_check = undefined; + Countly.disable_backoff_mechanism = undefined; CountlyStorage.resetStorage(); }; @@ -526,13 +660,15 @@ Countly.Bulk = Bulk; } else if (consents[feature]) { if (consents[feature].features) { + consents[feature].optin = false; // this is added group, let's iterate through sub features Countly.remove_consent_internal(consents[feature].features, enforceConsentUpdate); } else { + var wasGiven = consents[feature].optin === true; consents[feature].optin = false; // this is core feature - if (enforceConsentUpdate && consents[feature].optin !== false) { + if (enforceConsentUpdate && wasGiven) { updateConsent(); } } @@ -550,26 +686,50 @@ Countly.Bulk = Bulk; consentTimer = null; } consentTimer = setTimeout(() => { - var consentMessage = {}; - for (var i = 0; i < Countly.features.length; i++) { - if (consents[Countly.features[i]].optin === true) { - consentMessage[Countly.features[i]] = true; - } - else { - consentMessage[Countly.features[i]] = false; - } - } - toRequestQueue({ consent: JSON.stringify(consentMessage) }); - cc.log(cc.logLevelEnums.DEBUG, "updateConsent, Consent update request has been sent to the queue."); + consentTimer = null; + queueConsentRequest(); }, 1000); }; + /** + * Queues a request with the current consent state of every feature + */ + function queueConsentRequest() { + var consentMessage = {}; + for (var i = 0; i < Countly.features.length; i++) { + if (consents[Countly.features[i]].optin === true) { + consentMessage[Countly.features[i]] = true; + } + else { + consentMessage[Countly.features[i]] = false; + } + } + toRequestQueue({ consent: JSON.stringify(consentMessage) }); + cc.log(cc.logLevelEnums.DEBUG, "updateConsent, Consent update request has been sent to the queue."); + } + + /** + * Queues a consent change that is still waiting out its one second delay right away, so that it is sent + * under the device ID of the user who made it and is included in a flush + */ + function sendPendingConsent() { + if (consentTimer) { + clearTimeout(consentTimer); + consentTimer = null; + queueConsentRequest(); + } + } + /** * Start session * @param {boolean} noHeartBeat - true if you don't want to use internal heartbeat to manage session */ Countly.begin_session = function(noHeartBeat) { + if (!setting("st")) { + cc.log(cc.logLevelEnums.INFO, "begin_session, Session tracking is turned off in the SDK behavior settings."); + return; + } if (Countly.check_consent("sessions")) { if (!sessionStarted) { cc.log(cc.logLevelEnums.INFO, "begin_session, Session started."); @@ -612,6 +772,7 @@ Countly.Bulk = Bulk; reportViewDuration(); sessionStarted = false; toRequestQueue({ end_session: 1, session_duration: sec }); + saveHealthCounters(true); } } }; @@ -655,19 +816,35 @@ Countly.Bulk = Bulk; }; /** - * Change current user/device id - * @param {string} newId - new user/device ID to use + * Change current user/device id. + * Without merge, everything recorded for the current user is finished and sent under the current ID first: + * the open view's duration, unsaved custom property changes and recorded events. The session is ended, + * and timed events, breadcrumbs, calls waiting for consent and stored remote config values are cleared, + * so nothing of the previous user is reported for the new one. + * @param {string} newId - new user/device ID to use. Must be a non-empty string; a number is used as text * @param {boolean=} merge - move data from old ID to new ID on server * */ Countly.change_id = function(newId, merge) { + if (typeof newId === "number" && Number.isFinite(newId)) { + newId = String(newId); + } + if (typeof newId !== "string" || newId === "") { + cc.log(cc.logLevelEnums.WARNING, `change_id, The provided ID: [${newId}] is not valid, it must be a non-empty string. The device ID was not changed.`); + return; + } newId = cc.truncateSingleValue(newId, Countly.maxValueSize, "change_id", Countly.debug); cc.log(cc.logLevelEnums.INFO, `change_id, Changing ID. Current ID: [${Countly.device_id}], new ID: [${newId}], merge: [${merge}].`); - if (cluster.isMaster) { + if (Countly.device_id !== newId) { + sendPendingConsent(); + } + if (ownsQueue()) { if (Countly.device_id !== newId) { + var mergeQueued = false; if (!merge) { + closeCurrentUser(); // empty event queue if (eventQueue.length > 0) { - toRequestQueue({ events: JSON.stringify(eventQueue) }); + toRequestQueue({ events: JSON.stringify(eventQueue) }, true); eventQueue = []; CountlyStorage.storeSet("cly_event", eventQueue); } @@ -685,7 +862,7 @@ Countly.Bulk = Bulk; CountlyStorage.storeSet("cly_id_type", deviceIdType); if (merge) { if (Countly.check_any_consent()) { - toRequestQueue({ old_device_id: oldId }); + mergeQueued = toRequestQueue({ old_device_id: oldId }); } else { lastParams.change_id = arguments; @@ -695,20 +872,46 @@ Countly.Bulk = Bulk; // start new session for new id Countly.begin_session(!autoExtend); } - if (Countly.remote_config) { + if (!merge || Countly.remote_config) { remoteConfigs = {}; - if (cluster.isMaster) { - CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); - } + CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); + } + if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } + if (mergeQueued) { + settingsFetchAfterMerge = true; + } + else { + fetchBehaviorSettings(); + } } } else { + if (!merge) { + closeCurrentUser(); + timedEvents = {}; + remoteConfigs = {}; + Countly.remove_consent_internal(Countly.features, false); + } process.send({ cly: { change_id: newId, merge } }); } }; + /** + * Before the device ID changes without merge: sends what is still open for the current user, which is the + * open view's duration and unsaved custom property changes, and forgets the user's breadcrumbs and the + * calls that wait for consent + * */ + function closeCurrentUser() { + reportViewDuration(); + if (Object.keys(customData).length > 0) { + Countly.userData.save(); + } + crashLogs = []; + lastParams = {}; + } + /** * Report custom event * @param {Event} event - Countly {@link Event} object @@ -764,24 +967,31 @@ Countly.Bulk = Bulk; cc.log(cc.logLevelEnums.ERROR, "add_cly_events, Event must have 'key' property."); return; } - if (cluster.isMaster) { + if (ownsQueue()) { + if (!acceptsNewEvent(event.key)) { + return; + } if (!event.count) { event.count = 1; } - event.key = cc.truncateSingleValue(event.key, Countly.maxKeyLength, "add_cly_event", Countly.debug); - event.segmentation = cc.truncateObject(event.segmentation, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "add_cly_event", Countly.debug); + // internal events keep their key and segmentation, as the server recognizes them by both; the developer's view segments are limited in track_view + if (Object.values(cc.internalEventKeyEnums).indexOf(event.key) === -1) { + event.key = cc.truncateSingleValue(event.key, Countly.maxKeyLength, "add_cly_event", Countly.debug); + event.segmentation = cc.truncateObject(event.segmentation, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "add_cly_event", Countly.debug); + } var props = ["key", "count", "sum", "dur", "segmentation"]; - var e = cc.getProperties(event, props); + var e = cc.makeJsonSafe(cc.getProperties(event, props), "add_cly_event"); e.timestamp = getMsTimestamp(); var date = new Date(); e.hour = date.getHours(); e.dow = date.getDay(); - cc.log(cc.logLevelEnums.DEBUG, "add_cly_events, Adding event: ", event); + cc.log(cc.logLevelEnums.DEBUG, "add_cly_events, Adding event: ", e); eventQueue.push(e); CountlyStorage.storeSet("cly_event", eventQueue); + updateKeepAlive(); } else { - process.send({ cly: { event } }); + process.send({ cly: { event: cc.makeJsonSafe(event, "add_cly_event") } }); } } @@ -852,7 +1062,7 @@ Countly.Bulk = Bulk; user.gender = cc.truncateSingleValue(user.gender, Countly.maxValueSize, "user_details", Countly.debug); user.byear = cc.truncateSingleValue(user.byear, Countly.maxValueSize, "user_details", Countly.debug); user.custom = cc.truncateObject(user.custom, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "user_details"); - var request = { user_details: JSON.stringify(cc.getProperties(user, props)) }; + var request = { user_details: JSON.stringify(cc.makeJsonSafe(cc.getProperties(user, props), "user_details")) }; if (!user.picture && user.picturePath) { cc.log(cc.logLevelEnums.INFO, "user_details, Picture is not set but picturePath is set, will try to upload picture from path."); request.picturePath = user.picturePath; @@ -1024,7 +1234,7 @@ Countly.Bulk = Bulk; * */ save() { if (Countly.check_consent("users")) { - toRequestQueue({ user_details: JSON.stringify({ custom: customData }) }); + toRequestQueue({ user_details: JSON.stringify({ custom: cc.makeJsonSafe(customData, "save") }) }); } customData = {}; }, @@ -1093,16 +1303,26 @@ Countly.Bulk = Bulk; }; /** - * Automatically track javascript errors that happen on the nodejs process + * Automatically track javascript errors that happen on the nodejs process. + * An uncaught exception is reported as a fatal crash, stored synchronously and the process exits with code 1. + * An unhandled promise rejection is reported as a non-fatal error and the process keeps running, as it + * did before Node.js 15 made unhandled rejections stop the process. * @param {object=} segments - additional key value pairs you want to provide with error report, like versions of libraries used, etc. * */ Countly.track_errors = function(segments) { cc.log(cc.logLevelEnums.INFO, `track_errors, Tracking errors. Segments provided: [${segments}].`); crashSegments = segments; - process.on("uncaughtException", (err) => { + // replace previously registered handlers so repeated calls don't record the same error multiple times + if (uncaughtExceptionHandler) { + process.removeListener("uncaughtException", uncaughtExceptionHandler); + } + if (unhandledRejectionHandler) { + process.removeListener("unhandledRejection", unhandledRejectionHandler); + } + uncaughtExceptionHandler = (err) => { recordError(err, false); - if (cluster.isMaster) { + if (ownsQueue()) { CountlyStorage.forceStore(); } // eslint-disable-next-line no-console @@ -1110,19 +1330,18 @@ Countly.Bulk = Bulk; // eslint-disable-next-line no-console console.error(err.stack); process.exit(1); - }); + }; + process.on("uncaughtException", uncaughtExceptionHandler); - process.on('unhandledRejection', (reason) => { + unhandledRejectionHandler = (reason) => { var err = new Error(`Unhandled rejection (reason: ${reason && reason.stack ? reason.stack : reason}).`); - recordError(err, false); - if (cluster.isMaster) { - CountlyStorage.forceStore(); - } + recordError(err, true); // eslint-disable-next-line no-console console.error(`${(new Date()).toUTCString()} unhandledRejection:`, err.message); // eslint-disable-next-line no-console console.error(err.stack); - }); + }; + process.on('unhandledRejection', unhandledRejectionHandler); }; /** @@ -1153,12 +1372,26 @@ Countly.Bulk = Bulk; }; /** - * Fetch remote config + * Fetch remote config. Values downloaded for a device ID that is no longer the current one are not used; + * the callback then gets an error and the current values. * @param {array=} keys - Array of keys to fetch, if not provided will fetch all keys * @param {array=} omit_keys - Array of keys to omit, if provided will fetch all keys except provided ones * @param {function=} callback - Callback to notify with first param error and second param remote config object * */ Countly.fetch_remote_config = function(keys, omit_keys, callback) { + if (!setting("networking")) { + if (!callback && typeof omit_keys === "function") { + callback = omit_keys; + } + else if (!callback && typeof keys === "function") { + callback = keys; + } + cc.log(cc.logLevelEnums.WARNING, "fetch_remote_config, Networking is turned off in the SDK behavior settings."); + if (typeof callback === "function") { + callback(new Error("Networking is turned off in the SDK behavior settings"), remoteConfigs); + } + return; + } if (Countly.check_consent("remote-config")) { var request = { method: "fetch_remote_config", @@ -1185,27 +1418,37 @@ Countly.Bulk = Bulk; } } prepareRequest(request); + var generation = sdkGeneration; makeRequest(Countly.url, readPath, request, (err, params, responseText) => { - try { - var configs = JSON.parse(responseText); - if (request.keys || request.omit_keys) { - // we merge config - for (var i in configs) { - remoteConfigs[i] = configs[i]; - } + if (generation !== sdkGeneration || request.device_id !== Countly.device_id) { + cc.log(cc.logLevelEnums.WARNING, "fetch_remote_config, The device ID changed while remote config was downloading, the downloaded values are not used."); + if (typeof callback === "function") { + callback(new Error("The device ID changed while remote config was downloading"), remoteConfigs); } - else { - // we replace config - remoteConfigs = configs; + return; + } + if (!err) { + try { + var configs = JSON.parse(responseText); + if (request.keys || request.omit_keys) { + // we merge config + for (var i in configs) { + remoteConfigs[i] = configs[i]; + } + } + else { + // we replace config + remoteConfigs = configs; + } + if (ownsQueue()) { + CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); + cc.log(cc.logLevelEnums.INFO, `fetch_remote_config, Fetched remote config: [${remoteConfigs}].`); + } } - if (cluster.isMaster) { - CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); - cc.log(cc.logLevelEnums.INFO, `fetch_remote_config, Fetched remote config: [${remoteConfigs}].`); + catch (ex) { + // silent catch } } - catch (ex) { - // silent catch - } if (typeof callback === "function") { callback(err, remoteConfigs); } @@ -1264,6 +1507,10 @@ Countly.Bulk = Bulk; * */ Countly.track_view = function(name, viewSegments) { reportViewDuration(); + if (!setting("vt")) { + cc.log(cc.logLevelEnums.INFO, "track_view, View tracking is turned off in the SDK behavior settings."); + return; + } if (name) { name = cc.truncateSingleValue(name, Countly.maxValueSize, "track_view", Countly.debug); cc.log(cc.logLevelEnums.INFO, `track_view, Tracking view. Name: [${name}].`); @@ -1369,21 +1616,31 @@ Countly.Bulk = Bulk; }; /** - * Make raw request with provided parameters - * @example Countly.request({app_key:"somekey", devide_id:"someid", events:"[{'key':'val','count':1}]", begin_session:1}); + * Make raw request with provided parameters. The values are sent as given, without the SDK's length limits, + * and the request counts towards the request queue size like every other request. + * @example Countly.request({app_key:"somekey", device_id:"someid", events:'[{"key":"val","count":1}]', begin_session:1}); * @param {Object} request - object with key/values which will be used as request parameters * */ Countly.request = function(request) { - request = cc.truncateObject(request, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "request", Countly.debug); + request = cc.makeJsonSafe(request, "request"); - if (!request.app_key || !request.device_id) { + if (!request || typeof request !== "object" || !request.app_key || !request.device_id) { cc.log(cc.logLevelEnums.ERROR, "request, app_key or device_id is missing."); return; } - if (cluster.isMaster) { + if (ownsQueue()) { + if (!setting("tracking")) { + cc.log(cc.logLevelEnums.INFO, "request, Tracking is turned off in the SDK behavior settings. The request is not recorded."); + return; + } + if (requestQueue.length > queueSize) { + cc.log(cc.logLevelEnums.WARNING, "request, Queue overflown. Erasing earliest request from the queue."); + dropOldestRequest(); + } cc.log(cc.logLevelEnums.INFO, "request, Adding the raw request to the queue."); requestQueue.push(request); CountlyStorage.storeSet("cly_queue", requestQueue); + updateKeepAlive(); } else { cc.log(cc.logLevelEnums.INFO, "request, Sending message to the parent process. Adding the raw request to the queue."); @@ -1391,6 +1648,63 @@ Countly.Bulk = Bulk; } }; + /** + * Sends everything recorded so far without waiting for the next heartbeat: recorded events are packed into + * requests and the queued requests are sent one after another, also right after an earlier failed attempt. + * Use it before a script, a scheduled job or a serverless function ends, or in a shutdown handler before + * calling process.exit(). + * @example + * process.on("SIGTERM", async () => { + * Countly.end_session(); + * await Countly.flush(5); + * process.exit(0); + * }); + * @param {number=} timeout - seconds to wait at most. Without it, the call waits until the requests queued at the time of the call are sent, or one of them fails + * @param {function=} callback - called with true when they were all delivered, and with false when something is still waiting: the server could not be reached, the timeout passed, test mode is on, or this is a cluster worker that hands its data to the main process + * @returns {Promise} resolves with the same value the callback receives + * */ + Countly.flush = function(timeout, callback) { + if (typeof timeout === "function") { + callback = timeout; + timeout = undefined; + } + var result = new Promise((resolve) => { + if (!inited || !ownsQueue() || !Countly.url || Countly.test_mode || processingDisabled) { + cc.log(cc.logLevelEnums.WARNING, "flush, Nothing can be sent from this process: the SDK is not initialized, test mode is on, or this cluster worker hands its data to the main process."); + resolve(false); + return; + } + saveHealthCounters(true); + if (!setting("networking")) { + cc.log(cc.logLevelEnums.WARNING, "flush, Networking is turned off in the SDK behavior settings, the data stays queued."); + resolve(false); + return; + } + sendPendingConsent(); + packEvents(); + if (requestQueue.length === 0) { + resolve(true); + return; + } + var waiter = { pending: requestQueue.slice(), timer: null, resolve: resolve }; + if (typeof timeout === "number" && timeout > 0) { + waiter.timer = setTimeout(() => { + flushWaiters = flushWaiters.filter((entry) => entry !== waiter); + resolve(false); + }, timeout * 1000); + } + flushWaiters.push(waiter); + failTimeout = 0; + processRequestQueue(); + }); + if (typeof callback === "function") { + result.then((delivered) => { + process.nextTick(callback, delivered); + }); + } + return result; + }; + /** * PRIVATE METHODS * */ @@ -1430,7 +1744,7 @@ Countly.Bulk = Bulk; request.sdk_name = SDK_NAME; request.sdk_version = SDK_VERSION; request.t = deviceIdType; - if (Countly.check_consent("location")) { + if (Countly.check_consent("location") && setting("lt")) { if (Countly.country_code) { request.country_code = Countly.country_code; } @@ -1455,28 +1769,81 @@ Countly.Bulk = Bulk; /** * Add request to request queue * @param {Object} request - object with request parameters + * @param {Boolean=} alreadyRecorded - true for events recorded before, which are sent even if tracking was turned off since + * @returns {Boolean} true if the request was queued or handed to the main process */ - function toRequestQueue(request) { - if (cluster.isMaster) { + function toRequestQueue(request, alreadyRecorded) { + if (ownsQueue()) { if (!Countly.app_key || !Countly.device_id) { cc.log(cc.logLevelEnums.ERROR, "toRequestQueue, app_key or device_id is missing."); - return; + return false; + } + if (!alreadyRecorded && !acceptsNewRequest(request)) { + return false; } prepareRequest(request); if (requestQueue.length > queueSize) { cc.log(cc.logLevelEnums.WARNING, "toRequestQueue, Queue overflown. Erasing earliest request from the queue."); - requestQueue.shift(); + dropOldestRequest(); } cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Adding request to the queue."); requestQueue.push(request); CountlyStorage.storeSet("cly_queue", requestQueue); + updateKeepAlive(); + return true; } - else { - cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Sending message to the parent process. Adding request to the queue."); - process.send({ cly: { cly_queue: request } }); + cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Sending message to the parent process. Adding request to the queue."); + process.send({ cly: { cly_queue: request } }); + return true; + } + + /** + * Tells whether a new request may be recorded under the SDK behavior settings. Checked where requests are + * queued, so requests that cluster workers hand to this process are checked too. + * @param {Object} request - request parameters + * @returns {Boolean} true if the request may be recorded + */ + function acceptsNewRequest(request) { + if (!setting("tracking")) { + cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Tracking is turned off in the SDK behavior settings. The request is not recorded."); + return false; + } + if (request.crash && !setting("crt")) { + cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Crash tracking is turned off in the SDK behavior settings. The crash is not recorded."); + return false; + } + if ((request.begin_session || (request.session_duration && !request.end_session)) && !setting("st")) { + cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Session tracking is turned off in the SDK behavior settings. The session request is not recorded."); + return false; + } + return true; + } + + /** + * Tells whether a new event may be recorded under the SDK behavior settings: tracking must be on, views + * need view tracking and custom events (keys not starting with "[CLY]_") need custom event tracking + * @param {String} key - event key + * @returns {Boolean} true if the event may be recorded + */ + function acceptsNewEvent(key) { + if (!setting("tracking")) { + cc.log(cc.logLevelEnums.INFO, "add_cly_events, Tracking is turned off in the SDK behavior settings. The event is not recorded."); + return false; + } + if (key === cc.internalEventKeyEnums.VIEW) { + if (!setting("vt")) { + cc.log(cc.logLevelEnums.INFO, "add_cly_events, View tracking is turned off in the SDK behavior settings. The view is not recorded."); + return false; + } + return true; + } + if (String(key).indexOf("[CLY]_") !== 0 && !setting("cet")) { + cc.log(cc.logLevelEnums.INFO, "add_cly_events, Custom event tracking is turned off in the SDK behavior settings. The event is not recorded."); + return false; } + return true; } /** @@ -1492,37 +1859,548 @@ Countly.Bulk = Bulk; } } - // process event queue - if (eventQueue.length > 0) { - if (eventQueue.length <= maxEventBatch) { - toRequestQueue({ events: JSON.stringify(eventQueue) }); - eventQueue = []; + packEvents(); + processRequestQueue(); + saveHealthCounters(false); + + heartBeatTimer = setTimeout(heartBeat, beatInterval); + updateKeepAlive(); + } + + /** + * Moves all recorded events into the request queue, at most max_events events per request + */ + function packEvents() { + if (eventQueue.length === 0) { + return; + } + var batchSize = maxEventBatch > 0 ? maxEventBatch : eventQueue.length; + var events = eventQueue; + eventQueue = []; + for (var i = 0; i < events.length; i += batchSize) { + toRequestQueue({ events: JSON.stringify(events.slice(i, i + batchSize)) }, true); + } + CountlyStorage.storeSet("cly_event", eventQueue); + } + + /** + * Sends the oldest queued request when no other request is running. The request stays in the queue until + * the server confirms it, so it is still stored if the process ends meanwhile. After a confirmed request + * the next one is sent right away instead of waiting for the next heartbeat. + */ + function processRequestQueue() { + if (requestQueue.length === 0 || !readyToProcess || processingDisabled || Countly.test_mode || cc.getTimestamp() <= failTimeout || !setting("networking")) { + return; + } + if (isBackingOff()) { + return; + } + dropOldRequests(); + if (requestQueue.length === 0) { + return; + } + readyToProcess = false; + var params = requestQueue[0]; + var generation = sdkGeneration; + var sentAt = Date.now(); + inFlightRequest = params; + cc.log(cc.logLevelEnums.DEBUG, "Processing the request:", params); + makeRequest(Countly.url, apiPath, params, (err, res, responseText, statusCode) => { + if (generation !== sdkGeneration) { + return; + } + inFlightRequest = null; + cc.log(cc.logLevelEnums.DEBUG, "Request finished. Response:", res); + if (err) { + failTimeout = cc.getTimestamp() + failTimeoutAmount; + cc.log(cc.logLevelEnums.ERROR, `makeRequest, Encountered a problem while making the request: [${err}]`); + if (statusCode > 0 && statusCode < 1000) { + health.sc = statusCode; + health.em = String(responseText || "").substring(0, 1000); + healthDirty = true; + saveHealthCounters(true); + } } else { - var events = eventQueue.splice(0, maxEventBatch); - toRequestQueue({ events: JSON.stringify(events) }); + var index = requestQueue.indexOf(params); + if (index !== -1) { + requestQueue.splice(index, 1); + } + CountlyStorage.storeSet("cly_queue", requestQueue); + checkBackoff(params, sentAt); + if (settingsFetchAfterMerge && params.old_device_id) { + settingsFetchAfterMerge = false; + fetchBehaviorSettings(); + } } - CountlyStorage.storeSet("cly_event", eventQueue); + readyToProcess = true; + if (err) { + settleFlushWaiters(false); + } + else { + updateFlushWaiters(params, true); + } + if (!err && requestQueue.length > 0) { + setImmediate(processRequestQueue); + } + updateKeepAlive(); + }, "heartBeat", false); + } + + /** + * Lets the heartbeat timer keep the Node.js process running only while there is data to send and the + * server is answering. A script then ends by itself once its data is delivered, or when the server cannot + * be reached; undelivered data stays stored and is sent on the next start. + */ + function updateKeepAlive() { + if (!heartBeatTimer) { + return; } + var hasWork = requestQueue.length > 0 || eventQueue.length > 0; + if (hasWork && readyToProcess && !processingDisabled && !Countly.test_mode && cc.getTimestamp() > failTimeout && setting("networking") && !isBackingOff()) { + heartBeatTimer.ref(); + } + else { + heartBeatTimer.unref(); + } + } - // process request queue with event queue - if (requestQueue.length > 0 && readyToProcess && cc.getTimestamp() > failTimeout && !Countly.test_mode) { - readyToProcess = false; - var params = requestQueue.shift(); - cc.log(cc.logLevelEnums.DEBUG, "Processing the request:", params); - makeRequest(Countly.url, apiPath, params, (err, res) => { - cc.log(cc.logLevelEnums.DEBUG, "Request finished. Response:", res); + /** + * Value of an SDK behavior setting: the server's value (also when stored or provided at init), else the + * developer's value, else the default + * @param {String} key - setting key + * @returns {*} the value in use + */ + function setting(key) { + return behavior.valueOf(serverSettings, key, developerSettings[key]); + } + + /** + * Puts the SDK behavior settings into effect. New values apply to data recorded from now on; data already + * recorded is not changed. The server can turn the consent requirement on and off, as on Android and Web; + * without "cr" the developer's requirement applies. + */ + function applyBehaviorSettings() { + queueSize = setting("rqs"); + maxEventBatch = setting("eqs"); + sessionUpdate = setting("sui"); + Countly.maxKeyLength = setting("lkl"); + Countly.maxValueSize = setting("lvs"); + Countly.maxSegmentationValues = setting("lsv"); + Countly.maxBreadcrumbCount = setting("lbc"); + Countly.maxStackTraceLinesPerThread = setting("ltlpt"); + Countly.maxStackTraceLineLength = setting("ltl"); + Countly.require_consent = setting("cr"); + if (!setting("networking")) { + settleFlushWaiters(false); + } + updateKeepAlive(); + } + + /** + * Logs a problem found in the SDK behavior settings + * @param {String} message - what is wrong + */ + function warnAboutSettings(message) { + cc.log(cc.logLevelEnums.WARNING, `behavior settings, ${message}`); + } + + /** + * Adds the common parameters of the requests the SDK sends directly instead of through the queue + * @param {Object} params - request specific parameters + * @returns {Object} the parameters with the common ones added + */ + function directRequestParams(params) { + params.app_key = Countly.app_key; + params.device_id = Countly.device_id; + params.sdk_name = SDK_NAME; + params.sdk_version = SDK_VERSION; + params.t = deviceIdType; + params.av = Countly.app_version; + params.timestamp = getMsTimestamp(); + var date = new Date(); + params.hour = date.getHours(); + params.dow = date.getDay(); + params.tz = -date.getTimezoneOffset(); + return params; + } + + /** + * Fetches the SDK behavior settings from the server and puts them into effect. The request is sent + * directly, also while networking is turned off, so that turning networking back on reaches the SDK. A + * failed request or an unusable answer keeps the current settings. The next fetch follows scui hours later. + * @param {Function=} done - called when the fetch finished or was skipped + */ + function fetchBehaviorSettings(done) { + /** + * Tells the caller that the fetch finished or was skipped + */ + function finish() { + if (typeof done === "function") { + done(); + } + } + if (!inited || !ownsQueue() || !Countly.url || Countly.test_mode || processingDisabled || settingsUpdatesDisabled) { + finish(); + return; + } + var generation = sdkGeneration; + makeRequest(Countly.url, readPath, directRequestParams({ method: "sc" }), (err, params, responseText) => { + if (generation !== sdkGeneration) { + return; + } + var fetched = err ? null : behavior.parseAnswer(responseText, warnAboutSettings); + if (fetched) { + serverSettings = fetched; + CountlyStorage.storeSet("cly_sbs", fetched); + applyBehaviorSettings(); + cc.log(cc.logLevelEnums.INFO, "fetchBehaviorSettings, SDK behavior settings fetched:", fetched); + } + else { + cc.log(cc.logLevelEnums.WARNING, "fetchBehaviorSettings, The SDK behavior settings could not be fetched, the current settings stay in use."); + } + scheduleBehaviorSettingsFetch(); + finish(); + }, "behavior_settings", true); + } + + /** + * Schedules the next SDK behavior settings fetch, scui hours from now. The timer does not keep the process + * running. + */ + function scheduleBehaviorSettingsFetch() { + if (settingsTimer) { + clearTimeout(settingsTimer); + } + settingsTimer = setTimeout(() => { + settingsTimer = null; + fetchBehaviorSettings(); + }, setting("scui") * 3600 * 1000); + settingsTimer.unref(); + } + + /** + * Health check counters with their starting values + * @returns {Object} error and warning log counts (el, wl), the last failed request's status code and + * response (sc, em), and the backoff counts (bom, cbom) + */ + function newHealthCounters() { + return { el: 0, wl: 0, sc: -1, em: "", bom: 0, cbom: 0 }; + } + + /** + * Reads health check counters, for example the ones an earlier run stored; a damaged value starts from zero + * @param {*} stored - stored counters + * @param {Object=} logged - counters of this run so far, whose error and warning counts are added, so + * warnings logged during init before the stored counters were read are reported too + * @returns {Object} a copy of the usable counters + */ + function loadHealthCounters(stored, logged) { + var counters = newHealthCounters(); + if (stored && typeof stored === "object") { + Object.keys(counters).forEach((key) => { + if (typeof stored[key] === typeof counters[key]) { + counters[key] = stored[key]; + } + }); + } + if (logged) { + counters.el += logged.el; + counters.wl += logged.wl; + if (logged.el > 0 || logged.wl > 0) { + healthDirty = true; + } + } + return counters; + } + + /** + * Stores the health check counters when they changed. Unless "now" is set they are stored at most once per + * session update interval, so a failing storage, whose failed writes are counted as errors, cannot cause + * one write after another. + * @param {Boolean} now - true to store right away + */ + function saveHealthCounters(now) { + if (!healthDirty || !inited || !ownsQueue()) { + return; + } + if (!now && Date.now() - healthSavedAt < sessionUpdate * 1000) { + return; + } + healthDirty = false; + healthSavedAt = Date.now(); + CountlyStorage.storeSet("cly_hc", health); + } + + /** + * Stores changed health check counters synchronously when the process exits, because while the process + * runs they are stored at most once per session update interval + */ + function saveHealthCountersOnExit() { + if (healthDirty) { + saveHealthCounters(true); + CountlyStorage.flushSync(); + } + } + + /** + * Sends the health check once per start, directly instead of through the queue, with the error and warning + * log counts, the last failed request and the backoff counts. The counters are cleared only when the + * server confirms the health check; otherwise they are sent again at the next start. + */ + function sendHealthCheck() { + if (healthCheckSent || healthCheckDisabled || !inited || !ownsQueue() || !Countly.url || Countly.test_mode || processingDisabled) { + return; + } + if (!setting("networking")) { + cc.log(cc.logLevelEnums.INFO, "sendHealthCheck, Networking is turned off in the SDK behavior settings, the health check is not sent."); + return; + } + healthCheckSent = true; + var sent = loadHealthCounters(health); + var request = directRequestParams({ + hc: JSON.stringify(sent), + metrics: JSON.stringify({ _app_version: getMetrics()._app_version }), + }); + var generation = sdkGeneration; + makeRequest(Countly.url, apiPath, request, (err) => { + if (generation !== sdkGeneration || err) { + return; + } + health.el = Math.max(0, health.el - sent.el); + health.wl = Math.max(0, health.wl - sent.wl); + health.bom = Math.max(0, health.bom - sent.bom); + if (health.cbom === sent.cbom) { + health.cbom = 0; + } + if (health.sc === sent.sc && health.em === sent.em) { + health.sc = -1; + health.em = ""; + } + healthDirty = true; + saveHealthCounters(true); + }, "health_check", false); + } + + /** + * Tells whether sending is paused because the server answered slowly. A running flush call does not wait + * for the pause. + * @returns {Boolean} true while the pause lasts + */ + function isBackingOff() { + return flushWaiters.length === 0 && Date.now() < backoffUntil; + } + + /** + * After a delivered request, pauses sending for bom_d seconds when the server answered slowly while the + * queue was short and the request recent (backoff), unless the developer turned the backoff off + * @param {Object} request - the delivered request + * @param {Number} sentAt - time the request was sent, in milliseconds + */ + function checkBackoff(request, sentAt) { + if (backoffDisabled || !setting("bom")) { + backoffStreak = 0; + return; + } + var recordedAt = Number(request.timestamp); + var slow = behavior.shouldBackOff({ + durationSeconds: (Date.now() - sentAt) / 1000, + queueLength: requestQueue.length, + queueSize: queueSize, + ageHours: recordedAt > 0 ? (Date.now() - recordedAt) / 3600000 : 0, + bom_at: setting("bom_at"), + bom_rqp: setting("bom_rqp"), + bom_ra: setting("bom_ra"), + }); + if (!slow) { + backoffStreak = 0; + return; + } + backoffUntil = Date.now() + setting("bom_d") * 1000; + backoffStreak++; + health.bom++; + health.cbom = Math.max(health.cbom, backoffStreak); + healthDirty = true; + saveHealthCounters(true); + cc.log(cc.logLevelEnums.INFO, `checkBackoff, The server answered slowly, sending pauses for [${setting("bom_d")}] seconds.`); + } + + /** + * Drops queued requests recorded more than dort hours ago, when the dort setting is above 0 + */ + function dropOldRequests() { + var hours = setting("dort"); + if (!(hours > 0)) { + return; + } + var limit = Date.now() - hours * 3600000; + var kept = []; + requestQueue.forEach((request) => { + if (Number(request.timestamp) > 0 && Number(request.timestamp) < limit) { + updateFlushWaiters(request, false); + } + else { + kept.push(request); + } + }); + if (kept.length !== requestQueue.length) { + cc.log(cc.logLevelEnums.INFO, `dropOldRequests, Dropped [${requestQueue.length - kept.length}] requests recorded more than [${hours}] hours ago.`); + requestQueue = kept; + CountlyStorage.storeSet("cly_queue", requestQueue); + } + } + + /** + * Resolves all waiting flush calls with the same result + * @param {Boolean} result - value the flush calls resolve with + */ + function settleFlushWaiters(result) { + var waiting = flushWaiters; + flushWaiters = []; + waiting.forEach((waiter) => { + clearTimeout(waiter.timer); + waiter.resolve(result); + }); + } + + /** + * Tells the waiting flush calls that a request left the queue. A delivered request is crossed off, and a + * flush call resolves with true once all requests it waits for are delivered, or with false as soon as one + * of them was dropped because the queue was full. + * @param {Object} request - request that left the queue + * @param {Boolean} delivered - true if the server confirmed it, false if it was dropped + */ + function updateFlushWaiters(request, delivered) { + var waiting = []; + flushWaiters.forEach((waiter) => { + var index = waiter.pending.indexOf(request); + if (index === -1) { + waiting.push(waiter); + return; + } + waiter.pending.splice(index, 1); + if (!delivered || waiter.pending.length === 0) { + clearTimeout(waiter.timer); + waiter.resolve(delivered); + return; + } + waiting.push(waiter); + }); + flushWaiters = waiting; + } + + /** + * Drops the oldest request when the queue is full. The request being sent right now can still be delivered, + * so only other requests count as lost for waiting flush calls. + */ + function dropOldestRequest() { + var dropped = requestQueue.shift(); + if (dropped !== inFlightRequest) { + updateFlushWaiters(dropped, false); + } + } + + /** + * Tells whether this process keeps the queues and sends the requests: a process outside a cluster, the + * cluster's main process, or a cluster worker started with standalone_worker + * @returns {Boolean} true if this process sends its data itself + */ + function ownsQueue() { + return cluster.isMaster || standaloneWorker; + } + + /** + * Receives the data of a cluster worker in the main process + * @param {Object} worker - cluster worker + * @param {Boolean} announce - true to tell an already running worker right away that this process runs the SDK + */ + function listenToWorker(worker, announce) { + var entry = { worker: worker, listener: null }; + entry.listener = function(msg) { + handleWorkerMessage(msg, worker); + }; + worker.on("message", entry.listener); + worker.once("exit", () => { + worker.removeListener("message", entry.listener); + workerListeners = workerListeners.filter((item) => item !== entry); + }); + workerListeners.push(entry); + if (announce) { + sendToWorker(worker, { cly: { ack: true } }); + } + } + + /** + * Sends a message to a cluster worker. A worker whose channel is already closed is skipped, and a failed + * send is only logged, since a closed channel would otherwise raise an error event nobody handles. + * @param {Object} worker - cluster worker + * @param {Object} msg - message + */ + function sendToWorker(worker, msg) { + if (typeof worker.isConnected === "function" && !worker.isConnected()) { + return; + } + try { + worker.send(msg, (err) => { if (err) { - requestQueue.unshift(res); - failTimeout = cc.getTimestamp() + failTimeoutAmount; - cc.log(cc.logLevelEnums.ERROR, `makeRequest, Encountered a problem while making the request: [${err}]`); + cc.log(cc.logLevelEnums.WARNING, "sendToWorker, Could not send a message to a cluster worker:", err); } - CountlyStorage.storeSet("cly_queue", requestQueue); - readyToProcess = true; - }, "heartBeat", false); + }); } + catch (ex) { + cc.log(cc.logLevelEnums.WARNING, "sendToWorker, Could not send a message to a cluster worker:", ex); + } + } - heartBeatTimer = setTimeout(heartBeat, beatInterval); + /** + * In a cluster worker that hands its data to the main process: asks the main process whether it runs the + * SDK, and warns once when no answer arrives within 5 seconds, because the worker's data is then not sent. + * The check neither keeps the worker running nor warns when the worker is already disconnecting. + */ + function startPrimaryCheck() { + if (!cluster.isWorker || typeof process.send !== "function" || !process.connected) { + return; + } + workerMessageHandler = function(msg) { + if (msg && msg.cly && msg.cly.ack) { + stopPrimaryCheck(); + } + }; + process.on("message", workerMessageHandler); + process.once("disconnect", stopPrimaryCheck); + primaryCheckTimer = setTimeout(() => { + var { connected } = process; + stopPrimaryCheck(); + if (connected) { + cc.warnOnce("cluster-no-primary", "Countly: this cluster worker hands its data to the main process, but the main process does not run the Countly SDK, so nothing recorded in this worker is sent. Initialize Countly in the main process as well, or pass standalone_worker: true to Countly.init when the main process is not yours (for example in PM2 cluster mode)."); + } + }, 5000); + primaryCheckTimer.unref(); + try { + process.send({ cly: { hello: true } }, (err) => { + if (err) { + cc.log(cc.logLevelEnums.ERROR, "init, Could not contact the main process:", err); + } + }); + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, "init, Could not contact the main process:", ex); + } + } + + /** + * Ends the main process check of a cluster worker + */ + function stopPrimaryCheck() { + if (primaryCheckTimer) { + clearTimeout(primaryCheckTimer); + primaryCheckTimer = null; + } + if (workerMessageHandler) { + process.removeListener("message", workerMessageHandler); + process.removeListener("disconnect", stopPrimaryCheck); + workerMessageHandler = null; + } } /** @@ -1565,6 +2443,10 @@ Countly.Bulk = Bulk; * @param {Object} segments - custom crash segments */ function recordError(err, nonfatal, segments) { + if (!setting("crt")) { + cc.log(cc.logLevelEnums.INFO, "recordError, Crash tracking is turned off in the SDK behavior settings."); + return; + } if (Countly.check_consent("crashes") && err) { segments = segments || crashSegments; var error = ""; @@ -1633,7 +2515,9 @@ Countly.Bulk = Bulk; } /** - * Making HTTP request + * Making HTTP request. The request fails when the server sends nothing for request_timeout seconds or + * the connection closes before the whole response arrived, and the callback is called exactly once. + * The options passed to http_options always contain a headers object. * @param {String} url - URL where to make request * @param {String} api - API endpoint * @param {Object} params - key value object with URL params @@ -1642,6 +2526,22 @@ Countly.Bulk = Bulk; * @param {Boolean} isBroad - if true a broader (more generous) response check would be implemented */ function makeRequest(url, api, params, callback, info, isBroad) { + var finished = false; + /** + * Reports the outcome of the request to the callback, once + * @param {Boolean} failed - true if the request failed + * @param {String=} responseText - response body, when the server answered + * @param {Number=} statusCode - HTTP status code, when the server answered + */ + function finish(failed, responseText, statusCode) { + if (finished) { + return; + } + finished = true; + if (typeof callback === "function") { + callback(failed, params, responseText, statusCode); + } + } try { info = info || "general"; isBroad = isBroad || false; @@ -1654,6 +2554,7 @@ Countly.Bulk = Bulk; port: serverOptions.port, path: api, method: "GET", + headers: {}, }; if (params && params.picturePath) { @@ -1739,10 +2640,8 @@ Countly.Bulk = Bulk; if (method === "POST") { options.method = "POST"; options.path = api; - options.headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(data), - }; + options.headers["Content-Type"] = "application/x-www-form-urlencoded"; + options.headers["Content-Length"] = Buffer.byteLength(data); } } @@ -1755,6 +2654,7 @@ Countly.Bulk = Bulk; } var req = protocol.request(options, (res) => { var str = ""; + res.setEncoding("utf8"); res.on("data", (chunk) => { str += chunk; }); @@ -1770,34 +2670,38 @@ Countly.Bulk = Bulk; // only JSON object with result can pass isResponseValidated = cc.isResponseValid(res.statusCode, str); } - if (isResponseValidated) { - callback(false, params, str); - } - else { - callback(true, params); + finish(!isResponseValidated, str, res.statusCode); + }); + res.on("error", (err) => { + cc.log(cc.logLevelEnums.ERROR, "makeRequest, Connection failed while receiving the response. Error:", err); + finish(true); + }); + res.on("close", () => { + if (!res.complete) { + cc.log(cc.logLevelEnums.ERROR, "makeRequest, The connection closed before the whole response arrived."); + finish(true); } }); }); + req.setTimeout(requestTimeout * 1000, () => { + cc.log(cc.logLevelEnums.ERROR, `makeRequest, The server did not answer within [${requestTimeout}] seconds.`); + req.destroy(new Error("request timeout")); + }); + req.on("error", (err) => { + cc.log(cc.logLevelEnums.ERROR, "makeRequest, Connection failed. Error:", err); + finish(true); + }); if (method === "POST") { // write data to request body req.write(data); } - req.on("error", (err) => { - cc.log(cc.logLevelEnums.ERROR, "v, Connection failed. Error:", err); - if (typeof callback === "function") { - callback(true, params); - } - }); - req.end(); } catch (e) { // fallback cc.log(cc.logLevelEnums.ERROR, "makeRequest, Failed the HTTP request. Error:", e); - if (typeof callback === "function") { - callback(true, params); - } + finish(true); } } @@ -1838,10 +2742,16 @@ Countly.Bulk = Bulk; /** * Handle messages from forked workers * @param {Object} msg - message from worker + * @param {Object=} worker - worker that sent the message, answered when it asks whether this process runs the SDK */ - function handleWorkerMessage(msg) { - if (msg.cly) { - if (msg.cly.cly_queue) { + function handleWorkerMessage(msg, worker) { + if (msg && msg.cly) { + if (msg.cly.hello) { + if (worker) { + sendToWorker(worker, { cly: { ack: true } }); + } + } + else if (msg.cly.cly_queue) { toRequestQueue(msg.cly.cly_queue); } else if (msg.cly.change_id) { diff --git a/package-lock.json b/package-lock.json index 0a497a8..4552ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,32 @@ { "name": "countly-sdk-nodejs", - "version": "24.10.4", + "version": "26.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "countly-sdk-nodejs", - "version": "24.10.4", + "version": "26.8.0", "license": "MIT", "devDependencies": { "docdash": "2.0.2", - "eslint": "6.8.0", - "eslint-config-airbnb-base": "14.1.0", - "eslint-plugin-import": "2.20.1", + "eslint": "8.57.1", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.32.0", "jsdoc": "4.0.5", - "markdownlint": "0.26.0", - "mocha": "11.7.1", - "nyc": "17.1.0" + "markdownlint": "0.41.1", + "mocha": "11.8.0", + "nyc": "18.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -35,9 +35,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -45,21 +45,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -96,14 +96,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -113,14 +113,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -131,7 +131,7 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/lru-cache/-/lru-cache-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", @@ -140,9 +140,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -150,29 +150,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -182,9 +182,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -192,9 +192,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -202,9 +202,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -212,27 +212,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -242,33 +242,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -276,19 +276,150 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://repo.huaweicloud.com/repository/npm/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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/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, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "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, + "license": "Apache-2.0", + "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==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -485,16 +616,6 @@ "node": ">=6" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://repo.huaweicloud.com/repository/npm/resolve-from/-/resolve-from-5.0.0.tgz", @@ -578,6 +699,44 @@ "node": ">=v12.0.0" } }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -589,6 +748,37 @@ "node": ">=14" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -614,11 +804,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -631,13 +843,14 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/aggregate-error/-/aggregate-error-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "license": "MIT", @@ -650,10 +863,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -665,33 +879,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "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", @@ -702,15 +889,19 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/append-transform": { @@ -728,7 +919,7 @@ }, "node_modules/archy": { "version": "1.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/archy/-/archy-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true, "license": "MIT" @@ -742,17 +933,60 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -762,15 +996,57 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -779,13 +1055,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/astral-regex": { + "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/balanced-match": { @@ -795,13 +1088,16 @@ "dev": true }, "node_modules/baseline-browser-mapping": { - "version": "2.8.32", - "resolved": "https://repo.huaweicloud.com/repository/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", - "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "version": "2.11.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz", + "integrity": "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bluebird": { @@ -811,9 +1107,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -828,9 +1124,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -848,11 +1144,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -878,13 +1174,50 @@ } }, "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -895,6 +1228,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -912,9 +1246,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://repo.huaweicloud.com/repository/npm/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -945,24 +1279,54 @@ } }, "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, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/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/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, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/chokidar": { "version": "4.0.3", @@ -982,7 +1346,7 @@ }, "node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/clean-stack/-/clean-stack-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, "license": "MIT", @@ -990,27 +1354,6 @@ "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-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/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1026,33 +1369,35 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=8" + "node": ">=7.0.0" } }, - "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==", + "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, - "dependencies": { - "color-name": "1.1.3" - } + "license": "MIT" }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } }, "node_modules/commondir": { "version": "1.0.1", @@ -1073,16 +1418,6 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, - "node_modules/contains-path": { - "version": "0.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://repo.huaweicloud.com/repository/npm/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -1091,30 +1426,72 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/debug": { @@ -1147,11 +1524,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "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 + "dev": true, + "license": "MIT" }, "node_modules/default-require-extensions": { "version": "3.0.1", @@ -1179,12 +1571,32 @@ "node": ">=8" } }, - "node_modules/define-properties": { + "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -1195,10 +1607,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1227,6 +1663,21 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1235,9 +1686,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.263", - "resolved": "https://repo.huaweicloud.com/repository/npm/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", - "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", "dev": true, "license": "ISC" }, @@ -1260,42 +1711,86 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://repo.huaweicloud.com/repository/npm/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", - "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1304,24 +1799,81 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "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==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -1348,95 +1900,105 @@ } }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@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", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "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", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "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", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-airbnb-base": { - "version": "14.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.1.0.tgz", - "integrity": "sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, "license": "MIT", "dependencies": { - "confusing-browser-globals": "^1.0.9", - "object.assign": "^4.1.0", - "object.entries": "^1.1.1" + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" }, "engines": { - "node": ">= 6" + "node": "^10.12.0 || >=12.0.0" }, "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0", - "eslint-plugin-import": "^2.20.1" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -1444,133 +2006,172 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0" + "debug": "^3.2.7" }, "engines": { "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { + }, + "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, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.20.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", - "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.0.3", - "array.prototype.flat": "^1.2.1", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.1", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.12.0" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "2.x - 6.x" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "1.5.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "esutils": "^2.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "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==", + "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, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "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, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "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, - "engines": { - "node": ">=4" + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">=6.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -1587,10 +2188,11 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1598,20 +2200,12 @@ "node": ">=0.10" } }, - "node_modules/esquery/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/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, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1619,20 +2213,12 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "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/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1646,63 +2232,48 @@ "node": ">=0.10.0" } }, - "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-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, + "license": "ISC", "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "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, + "license": "MIT", "dependencies": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/find-cache-dir": { @@ -1724,15 +2295,20 @@ } }, "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -1745,24 +2321,42 @@ } }, "node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/foreground-child": { "version": "3.3.1", @@ -1781,54 +2375,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1842,22 +2388,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://repo.huaweicloud.com/repository/npm/fromentries/-/fromentries-1.3.2.tgz", @@ -1882,20 +2412,63 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "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, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -1917,38 +2490,78 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://repo.huaweicloud.com/repository/npm/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -1958,44 +2571,110 @@ } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "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==", + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "is-glob": "^4.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">= 6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" }, "engines": { "node": ">=8" @@ -2004,6 +2683,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals/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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2011,53 +2733,71 @@ "dev": true, "license": "ISC" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } + "license": "MIT" }, "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "dunder-proto": "^1.0.0" + }, + "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2066,12 +2806,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -2097,6 +2838,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2106,13 +2860,6 @@ "he": "bin/he" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://repo.huaweicloud.com/repository/npm/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://repo.huaweicloud.com/repository/npm/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2120,32 +2867,22 @@ "dev": true, "license": "MIT" }, - "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/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2168,7 +2905,7 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/indent-string/-/indent-string-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", @@ -2179,8 +2916,10 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "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, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2190,155 +2929,160 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.4" } }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/is-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, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/is-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, - "dependencies": { - "has-flag": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "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-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==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -2347,11 +3091,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, "engines": { "node": ">= 0.4" }, @@ -2359,25 +3108,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "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, - "dependencies": { - "has": "^1.0.3" - }, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2389,12 +3138,29 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2404,11 +3170,32 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.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, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2416,11 +3203,36 @@ "node": ">=0.10.0" } }, + "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, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2429,12 +3241,14 @@ } }, "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2443,6 +3257,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -2453,14 +3277,30 @@ } }, "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==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2469,12 +3309,16 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2494,12 +3338,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2509,12 +3355,31 @@ } }, "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -2542,13 +3407,47 @@ "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, + "license": "MIT", + "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2556,7 +3455,7 @@ }, "node_modules/is-windows": { "version": "1.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/is-windows/-/is-windows-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "license": "MIT", @@ -2565,17 +3464,18 @@ } }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -2631,9 +3531,9 @@ } }, "node_modules/istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://repo.huaweicloud.com/repository/npm/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.1.tgz", + "integrity": "sha512-s3mX05h5wGZeScG6XnOanygPh4SJu5ujMc9YbvpnLGXWy1cRiGbp0NdVcjHxgoZt3WfQppfBsa0y+gWdYJ2pGQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2641,92 +3541,30 @@ "cross-spawn": "^7.0.3", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://repo.huaweicloud.com/repository/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "rimraf": "^6.1.3" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/which": { - "version": "2.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "node": "20 || >=22" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/istanbul-lib-report": { @@ -2744,16 +3582,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "4.0.0", "resolved": "https://repo.huaweicloud.com/repository/npm/make-dir/-/make-dir-4.0.0.tgz", @@ -2783,19 +3611,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://repo.huaweicloud.com/repository/npm/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -2843,16 +3658,17 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "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==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2923,7 +3739,7 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/jsesc/-/jsesc-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", @@ -2934,11 +3750,19 @@ "node": ">=6" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "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 + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -2946,6 +3770,46 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "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, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", @@ -2957,69 +3821,61 @@ } }, "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "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, + "license": "MIT", "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } }, - "node_modules/linkify-it/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-json-file": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "node": ">=10" }, - "engines": { - "node": ">=4" + "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 + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -3028,6 +3884,13 @@ "dev": true, "license": "MIT" }, + "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, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3044,76 +3907,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -3138,15 +3931,25 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -3180,101 +3983,640 @@ "dev": true, "license": "MIT" }, - "node_modules/markdown-it/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "node_modules/markdownlint": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", + "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.2.1" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } }, - "node_modules/markdownlint": { - "version": "0.26.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/markdownlint/-/markdownlint-0.26.0.tgz", - "integrity": "sha512-I7nfRVwKVs/AJi4BlBkUCNj/S4HJjZSSM1wbzdy7vZerFkSGpbs7wX9Rk8rvopvZjTTKFPQWItqdbl2Hh6YXQQ==", + "node_modules/markdownlint/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/markdownlint/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", "dependencies": { - "markdown-it": "13.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=14" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/markdownlint/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/markdownlint/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, - "node_modules/markdownlint/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "node_modules/marked": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", + "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", "dev": true, + "bin": { + "marked": "bin/marked.js" + }, "engines": { - "node": ">=0.12" + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "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-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "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-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/markdownlint/node_modules/linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/markdownlint/node_modules/markdown-it": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", - "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/marked": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", - "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "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==", + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "dev": true, - "engines": { - "node": ">=6" - } + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3283,37 +4625,29 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mocha": { - "version": "11.7.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/mocha/-/mocha-11.7.1.tgz", - "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", "dev": true, "license": "MIT", "dependencies": { @@ -3325,6 +4659,7 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -3353,47 +4688,20 @@ "dev": true }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/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==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mocha/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -3411,20 +4719,22 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/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/mocha/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==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3432,29 +4742,14 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/mocha/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==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3463,45 +4758,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/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==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/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==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/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/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3524,23 +4780,30 @@ "dev": true, "license": "MIT" }, - "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/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/node-preload": { "version": "0.2.1", @@ -3556,39 +4819,19 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://repo.huaweicloud.com/repository/npm/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "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/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/nyc": { - "version": "17.1.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/nyc/-/nyc-17.1.0.tgz", - "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-18.0.0.tgz", + "integrity": "sha512-G5UyHinFkB1BxqGTrmZdB6uIYH0+v7ZnVssuflUDi+J+RhKWyAhRT1RCehBSI6jLFLuUUgFDyLt49mUtdO1XeQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3601,11 +4844,11 @@ "find-up": "^4.1.0", "foreground-child": "^3.3.0", "get-package-type": "^0.1.0", - "glob": "^7.1.6", + "glob": "^13.0.6", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-hook": "^3.0.0", "istanbul-lib-instrument": "^6.0.2", - "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-processinfo": "^3.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.0.2", @@ -3614,33 +4857,17 @@ "p-map": "^3.0.0", "process-on-spawn": "^1.0.0", "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", + "rimraf": "^6.1.3", "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", + "spawn-wrap": "^3.0.0", + "test-exclude": "^8.0.0", "yargs": "^15.0.2" }, "bin": { "nyc": "bin/nyc.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/nyc/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "20 || >=22" } }, "node_modules/nyc/node_modules/camelcase": { @@ -3665,26 +4892,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/nyc/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/nyc/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://repo.huaweicloud.com/repository/npm/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/nyc/node_modules/decamelize": { "version": "1.2.0", "resolved": "https://repo.huaweicloud.com/repository/npm/decamelize/-/decamelize-1.2.0.tgz", @@ -3761,16 +4968,6 @@ "node": ">=6" } }, - "node_modules/nyc/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/nyc/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://repo.huaweicloud.com/repository/npm/resolve-from/-/resolve-from-5.0.0.tgz", @@ -3782,33 +4979,23 @@ } }, "node_modules/nyc/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nyc/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/nyc/node_modules/wrap-ansi": { @@ -3871,10 +5058,14 @@ } }, "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3889,14 +5080,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -3907,28 +5101,32 @@ } }, "node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" } }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "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, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "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" @@ -3937,83 +5135,122 @@ "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": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "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, + "license": "MIT", "dependencies": { - "wrappy": "1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "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/os-tmpdir": { + "node_modules/own-keys": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "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==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { - "p-try": "^1.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^1.1.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-map": { "version": "3.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/p-map/-/p-map-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "license": "MIT", @@ -4024,15 +5261,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/package-hash": { "version": "4.0.0", "resolved": "https://repo.huaweicloud.com/repository/npm/package-hash/-/package-hash-4.0.0.tgz", @@ -4061,6 +5289,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4068,51 +5297,62 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "dev": true, "license": "MIT", "dependencies": { - "error-ex": "^1.2.0" + "@types/unist": "^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" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "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, + "license": "MIT", "engines": { - "node": ">=4" + "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "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==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -4131,19 +5371,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-type": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4151,16 +5378,6 @@ "dev": true, "license": "ISC" }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://repo.huaweicloud.com/repository/npm/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -4240,21 +5457,22 @@ "node": ">=6" } }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -4272,20 +5490,12 @@ "node": ">=8" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4300,66 +5510,83 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "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, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/read-pkg": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - }, "engines": { - "node": ">=4" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/read-pkg-up": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, "engines": { - "node": ">= 14.18.0" + "node": ">= 0.4" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/release-zalgo": { @@ -4402,18 +5629,25 @@ } }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.8.1", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4423,83 +5657,140 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "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==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "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, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "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/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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { - "npm": ">=2.0.0" + "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" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4511,13 +5802,13 @@ } }, "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==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", + "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-blocking": { @@ -4527,70 +5818,160 @@ "dev": true, "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "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-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=4" + "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==", + "dev": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://repo.huaweicloud.com/repository/npm/source-map/-/source-map-0.6.1.tgz", @@ -4602,16 +5983,17 @@ } }, "node_modules/spawn-wrap": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-3.0.0.tgz", + "integrity": "sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { + "cross-spawn": "^7.0.6", "foreground-child": "^2.0.0", "is-windows": "^1.0.2", "make-dir": "^3.0.0", - "rimraf": "^3.0.0", + "rimraf": "^6.1.3", "signal-exit": "^3.0.2", "which": "^2.0.1" }, @@ -4619,24 +6001,9 @@ "node": ">=8" } }, - "node_modules/spawn-wrap/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://repo.huaweicloud.com/repository/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/spawn-wrap/node_modules/foreground-child": { "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/foreground-child/-/foreground-child-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "license": "ISC", @@ -4648,114 +6015,46 @@ "node": ">=8.0.0" } }, - "node_modules/spawn-wrap/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/spawn-wrap/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/spawn-wrap/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, - "node_modules/spawn-wrap/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/spawn-wrap/node_modules/which": { - "version": "2.0.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "node": ">= 0.4" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://repo.huaweicloud.com/repository/npm/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4786,67 +6085,77 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/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" + "node": ">= 0.4" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "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, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "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/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -4863,18 +6172,9 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/strip-bom/-/strip-bom-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", @@ -4895,15 +6195,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -4911,6 +6212,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4918,63 +6220,58 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, + "license": "ISC", "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=6.0.0" + "node": "20 || >=22" } }, - "node_modules/table/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": "18 || 20 || >=22" } }, - "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=6" + "node": "20 || >=22" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://repo.huaweicloud.com/repository/npm/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -4983,37 +6280,27 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "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, + "license": "MIT", "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { - "prelude-ls": "~1.1.2" + "prelude-ls": "^1.2.1" }, "engines": { "node": ">= 0.8.0" @@ -5028,6 +6315,84 @@ "node": ">=8" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://repo.huaweicloud.com/repository/npm/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -5039,36 +6404,42 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" }, "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==", - "dev": true + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://repo.huaweicloud.com/repository/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -5101,60 +6472,89 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://repo.huaweicloud.com/repository/npm/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, "bin": { - "uuid": "dist/bin/uuid" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://repo.huaweicloud.com/repository/npm/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" }, - "bin": { - "which": "bin/which" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-boxed-primitive": { + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "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" + "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" @@ -5167,6 +6567,28 @@ "dev": true, "license": "ISC" }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5221,121 +6643,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "3.0.3", @@ -5368,7 +6681,7 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://repo.huaweicloud.com/repository/npm/yallist/-/yallist-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" @@ -5422,6 +6735,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 20c8138..d584fea 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "countly-sdk-nodejs", - "version": "24.10.4", + "version": "26.8.0", "description": "Countly NodeJS SDK", "main": "lib/countly.js", + "files": [ + "lib", + "index.js", + "CHANGELOG.md" + ], "directories": { "example": "examples", "lib": "lib", @@ -15,11 +20,11 @@ "docs": "node_modules/.bin/jsdoc lib/countly.js lib/countly-bulk.js lib/countly-bulk-user.js README.md -c jsdoc_conf.json -d docs", "pretest": "eslint --ignore-path .eslintignore .", "test": "mocha --timeout 180000 test/tests_*.js --exit --allow-uncaught", - "coverage": "nyc npm test" + "coverage": "nyc --reporter=text --reporter=lcov npm test" }, "repository": { "type": "git", - "url": "https://github.com/Countly/countly-sdk-nodejs" + "url": "git+https://github.com/Countly/countly-sdk-nodejs.git" }, "keywords": [ "Countly", @@ -32,14 +37,18 @@ "url": "https://github.com/Countly/countly-sdk-nodejs/issues" }, "homepage": "https://count.ly/", + "overrides": { + "diff": "8.0.4", + "serialize-javascript": "7.1.0" + }, "devDependencies": { "docdash": "2.0.2", - "eslint": "6.8.0", - "eslint-config-airbnb-base": "14.1.0", - "eslint-plugin-import": "2.20.1", + "eslint": "8.57.1", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.32.0", "jsdoc": "4.0.5", - "markdownlint": "0.26.0", - "mocha": "11.7.1", - "nyc": "17.1.0" + "markdownlint": "0.41.1", + "mocha": "11.8.0", + "nyc": "18.0.0" } } \ No newline at end of file diff --git a/test/compat/compat_check.js b/test/compat/compat_check.js new file mode 100644 index 0000000..ce8a3e4 --- /dev/null +++ b/test/compat/compat_check.js @@ -0,0 +1,322 @@ +/** + * Compatibility check for the oldest supported Node.js versions, which the mocha test suite cannot run on. + * It needs no installed packages: run it with "node test/compat/compat_check.js". It records data the way an app + * does against a local mock server, prints one result line and exits with code 1 when a check fails. + * Keep this file in syntax that Node.js 10 understands. + */ +/* global BigInt */ +var http = require("http"); +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var querystring = require("querystring"); +var Countly = require("../../lib/countly"); +var CountlyBulk = require("../../lib/countly-bulk"); + +var workDir = fs.mkdtempSync(path.join(os.tmpdir(), "countly-compat-")); +var results = { node: process.version, passed: [], failed: [] }; + +/** + * Records the outcome of one check + * @param {String} name - what was checked + * @param {Boolean} condition - true if the check passed + * @param {String=} detail - extra information shown when the check failed + */ +function check(name, condition, detail) { + if (condition) { + results.passed.push(name); + } + else { + results.failed.push(detail ? `${name} (${detail})` : name); + } +} + +/** + * Waits for the given time + * @param {Number} ms - milliseconds to wait + * @returns {Promise} resolves after the time passed + */ +function wait(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** + * Creates a new folder inside the work folder + * @param {String} name - name of the folder + * @returns {String} path of the folder + */ +function newFolder(name) { + var folder = path.join(workDir, name); + fs.mkdirSync(folder); + return folder; +} + +/** + * Deletes a folder with everything in it, in a way that works on Node.js 10 + * @param {String} folder - folder to delete + */ +function removeFolder(folder) { + if (!fs.existsSync(folder)) { + return; + } + fs.readdirSync(folder).forEach((name) => { + var item = path.join(folder, name); + if (fs.lstatSync(item).isDirectory()) { + removeFolder(item); + } + else { + fs.unlinkSync(item); + } + }); + fs.rmdirSync(folder); +} + +/** + * Starts a mock Countly server that records every request. Settings requests are answered with the given + * settings, every other request with Success. + * @param {Boolean} ignoreFirst - true to leave the first data request (not a settings request or health check) unanswered + * @param {Object=} settings - the "c" object of the settings answer, none by default + * @returns {Promise} server url, received requests and a close function + */ +function startServer(ignoreFirst, settings) { + return new Promise((resolve) => { + var requests = []; + var ignored = false; + var sockets = []; + var server = http.createServer((req, res) => { + var body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + var params = querystring.parse(req.url.split("?")[1] || ""); + var fromBody = querystring.parse(body); + Object.keys(fromBody).forEach((key) => { + params[key] = fromBody[key]; + }); + var request = { path: req.url.split("?")[0], params: params }; + requests.push(request); + res.writeHead(200, { "Content-Type": "application/json" }); + if (request.path === "/o/sdk" && params.method === "sc") { + res.end(JSON.stringify({ v: 2, t: 1, c: settings || {} })); + return; + } + if (ignoreFirst && !ignored && typeof params.hc === "undefined") { + ignored = true; + return; + } + res.end("{\"result\":\"Success\"}"); + }); + }); + server.on("connection", (socket) => { + sockets.push(socket); + }); + server.listen(0, "127.0.0.1", () => { + resolve({ + url: `http://127.0.0.1:${server.address().port}`, + requests: requests, + close: function() { + sockets.forEach((socket) => { + socket.destroy(); + }); + return new Promise((done) => { + server.close(done); + }); + }, + }); + }); + }); +} + +/** + * Lists the events of the received requests, including events inside bulk requests + * @param {Array} requests - requests received by the mock server + * @returns {Array} events, each with the device ID it was sent with + */ +function receivedEvents(requests) { + var events = []; + var collect = (list, deviceId) => { + var parsed = typeof list === "string" ? JSON.parse(list) : list; + parsed.forEach((event) => { + event.device_id = deviceId; + events.push(event); + }); + }; + requests.forEach((request) => { + if (request.params.events) { + collect(request.params.events, request.params.device_id); + } + if (request.params.requests) { + JSON.parse(request.params.requests).forEach((inner) => { + if (inner.events) { + collect(inner.events, inner.device_id); + } + }); + } + }); + return events; +} + +/** + * Records sessions, events, a view, user details, a crash and a user switch, then checks what reached the server + * and what was stored on disk + */ +async function checkTracking() { + var mock = await startServer(false); + var storage = newFolder("tracking"); + Countly.init({ app_key: "compat-key", url: mock.url, device_id: "compat-user", storage_path: storage, interval: 100, fail_timeout: 1, request_timeout: 3 }); + Countly.begin_session(); + var segmentation = { plan: "pro" }; + if (typeof BigInt === "function") { + segmentation.order_id = BigInt("9007199254740993"); + } + Countly.add_event({ key: "compat_event", count: 1, sum: 2.5, segmentation: segmentation }); + Countly.add_event({ key: "compat_half_emoji", segmentation: { name: "AliπŸ˜€".substring(0, 4) } }); + Countly.track_view("Compat Home"); + Countly.user_details({ name: "Compat User", custom: { tier: "gold" } }); + Countly.userData.set("plan", "basic"); + Countly.userData.save(); + Countly.log_error(new Error("compat error")); + Countly.change_id("compat-user-2", false); + Countly.add_event({ key: "compat_after_switch" }); + Countly.end_session(); + var delivered = await Countly.flush(20); + var events = receivedEvents(mock.requests); + var event = events.filter((e) => e.key === "compat_event")[0]; + check("flush reports delivery", delivered === true); + check("session sent", mock.requests.some((r) => r.params.begin_session === "1" && r.params.device_id === "compat-user")); + check("event sent", !!event && event.segmentation.plan === "pro"); + if (typeof BigInt === "function") { + check("BigInt sent as exact text", !!event && event.segmentation.order_id === "9007199254740993"); + } + check("event with half an emoji sent", events.some((e) => e.key === "compat_half_emoji")); + check("view sent", events.some((e) => e.key === "[CLY]_view" && e.segmentation.name === "Compat Home")); + check("user details sent", mock.requests.some((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "Compat User")); + check("crash sent", mock.requests.some((r) => r.params.crash && JSON.parse(r.params.crash)._error.indexOf("compat error") !== -1)); + check("new user's event sent with the new device ID", events.some((e) => e.key === "compat_after_switch" && e.device_id === "compat-user-2")); + var idFile = path.join(storage, "__cly_id.json"); + check("device ID stored on disk", fs.existsSync(idFile) && fs.readFileSync(idFile, "utf8").indexOf("compat-user-2") !== -1); + Countly.halt(); + await mock.close(); +} + +/** + * Leaves the first request unanswered and checks that it is given up after request_timeout and sent again + */ +async function checkUnansweredRequest() { + var mock = await startServer(true); + Countly.init({ app_key: "compat-key", url: mock.url, device_id: "compat-timeout", storage_path: newFolder("timeout"), interval: 100, fail_timeout: 1, request_timeout: 1 }); + Countly.add_event({ key: "compat_after_hang" }); + var started = Date.now(); + var first = await Countly.flush(20); + check("unanswered request given up after request_timeout", first === false && Date.now() - started < 5000, `flush ${first} after ${Date.now() - started} ms`); + await wait(1500); + var second = await Countly.flush(20); + var copies = receivedEvents(mock.requests).filter((e) => e.key === "compat_after_hang").length; + check("data sent again after an unanswered request", second === true && copies >= 2, `flush ${second}, ${copies} copies`); + Countly.halt(); + await mock.close(); +} + +/** + * Sends a bulk user's session, event and details with file storage in the default folder of the working directory + */ +async function checkBulk() { + var previousDir = process.cwd(); + var appDir = newFolder("bulk-app"); + process.chdir(appDir); + var mock = await startServer(false); + var server = new CountlyBulk({ app_key: "compat-bulk", url: mock.url, interval: 100, request_timeout: 3, storage_type: CountlyBulk.StorageTypes.FILE }); + var user = server.add_user({ device_id: "compat-bulk-user" }); + user.begin_session({ _os: "Linux" }, 60, Math.floor(Date.now() / 1000) - 120); + user.add_event({ key: "compat_bulk_event", count: 1 }); + user.user_details({ name: "Bulk Compat" }); + await new Promise((resolve) => { + var timer = setTimeout(resolve, 15000); + server.start(() => { + clearTimeout(timer); + resolve(); + }); + }); + server.stop(); + check("bulk event sent", receivedEvents(mock.requests).some((e) => e.key === "compat_bulk_event" && e.device_id === "compat-bulk-user")); + check("bulk default storage folder created", fs.existsSync(path.join(appDir, ".countly-storage", "bulk", "compat-bulk"))); + await mock.close(); + process.chdir(previousDir); +} + +/** + * Waits until a condition is true, or until the time is up + * @param {Function} condition - returns true when the wait is over + * @param {Number} timeoutMs - longest time to wait + * @returns {Promise} true if the condition became true + */ +async function waitFor(condition, timeoutMs) { + var started = Date.now(); + while (!condition()) { + if (Date.now() - started > timeoutMs) { + return false; + } + // eslint-disable-next-line no-await-in-loop + await wait(50); + } + return true; +} + +/** + * Fetches the SDK behavior settings, checks that they are applied and stored, that one health check is sent, + * and that a device ID change fetches the settings again + */ +async function checkBehaviorSettings() { + var mock = await startServer(false, { lkl: 9, eqs: 1 }); + var storage = newFolder("settings"); + Countly.init({ app_key: "compat-key", url: mock.url, device_id: "compat-settings", app_version: "4.2", storage_path: storage, interval: 100, request_timeout: 3 }); + var settingsRequests = () => mock.requests.filter((r) => r.path === "/o/sdk" && r.params.method === "sc"); + var healthChecks = () => mock.requests.filter((r) => typeof r.params.hc !== "undefined"); + check("settings fetched and applied", await waitFor(() => Countly.maxKeyLength === 9, 5000), `key length ${Countly.maxKeyLength}`); + check("health check sent", await waitFor(() => healthChecks().length === 1, 5000)); + var sbsFile = path.join(storage, "__cly_sbs.json"); + check("settings stored on disk", await waitFor(() => fs.existsSync(sbsFile) && fs.readFileSync(sbsFile, "utf8").indexOf("\"lkl\":9") !== -1, 5000)); + var health = healthChecks().length ? JSON.parse(healthChecks()[0].params.hc) : {}; + check("health check counters sent", ["el", "wl", "sc", "em", "bom", "cbom"].every((key) => typeof health[key] !== "undefined"), JSON.stringify(health)); + check("health check app version sent", healthChecks().length > 0 && JSON.parse(healthChecks()[0].params.metrics)._app_version === "4.2"); + Countly.change_id("compat-settings-2", false); + check("settings fetched again after a device ID change", await waitFor(() => settingsRequests().some((r) => r.params.device_id === "compat-settings-2"), 5000)); + Countly.add_event({ key: "compat_limited_key_event" }); + await Countly.flush(10); + check("server limits applied to new data", receivedEvents(mock.requests).some((e) => e.key === "compat_li")); + check("one health check per start", healthChecks().length === 1, `${healthChecks().length} health checks`); + Countly.halt(); + await mock.close(); +} + +/** + * Runs one group of checks and records it as failed if it throws + * @param {String} name - name of the group + * @param {Function} run - async function with the checks + */ +async function runGroup(name, run) { + try { + await run(); + } + catch (ex) { + results.failed.push(`${name} threw: ${ex && ex.stack ? ex.stack.split("\n").slice(0, 2).join(" ") : ex}`); + } +} + +(async() => { + await runGroup("tracking", checkTracking); + await runGroup("unanswered request", checkUnansweredRequest); + await runGroup("bulk", checkBulk); + await runGroup("behavior settings", checkBehaviorSettings); + try { + removeFolder(workDir); + } + catch (ex) { + results.cleanup = `could not remove ${workDir}: ${ex.message}`; + } + process.stdout.write(`${JSON.stringify(results)}\n`); + process.exitCode = results.failed.length === 0 ? 0 : 1; +})(); diff --git a/test/customStorageDirectory/.gitignore b/test/customStorageDirectory/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/test/customStorageDirectory/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/test/helpers/behavior_settings.js b/test/helpers/behavior_settings.js new file mode 100644 index 0000000..211834a --- /dev/null +++ b/test/helpers/behavior_settings.js @@ -0,0 +1,150 @@ +const fs = require("fs"); +const path = require("path"); +const Countly = require("../../lib/countly"); +const CountlyStorage = require("../../lib/countly-storage"); +const { startMockServer } = require("./mock_server"); + +const appKey = "YOUR_APP_KEY"; + +/** + * Starts a mock server that answers settings requests with state.body (and state.status), health checks with + * state.hcStatus (Success by default), and every other request with Success unless "other" answers it. Each + * recorded request gets the time it arrived as receivedAt. + * @param {Object} state - the answers: body (object or text), optional status and hcStatus + * @param {Function=} other - (req, res, entry, count) answers the other requests + * @returns {Promise} the mock server + */ +function settingsServer(state, other) { + return startMockServer((req, res, entry, count) => { + entry.receivedAt = Date.now(); + if (entry.path === "/o/sdk" && entry.params.method === "sc") { + res.writeHead(state.status || 200, { "Content-Type": "application/json" }); + res.end(typeof state.body === "string" ? state.body : JSON.stringify(state.body)); + return; + } + if (typeof entry.params.hc !== "undefined" && state.hcStatus) { + res.writeHead(state.hcStatus, { "Content-Type": "application/json" }); + res.end('{"result":"Internal server error"}'); + return; + } + if (other) { + other(req, res, entry, count); + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }); +} + +/** + * Settings requests the server received + * @param {Object} server - mock server + * @returns {Array} the requests + */ +function settingsRequests(server) { + return server.requests.filter((r) => r.path === "/o/sdk" && r.params.method === "sc"); +} + +/** + * Health checks the server received + * @param {Object} server - mock server + * @returns {Array} the requests + */ +function healthChecks(server) { + return server.requests.filter((r) => r.path === "/i" && typeof r.params.hc !== "undefined"); +} + +/** + * Data requests (not settings requests, not health checks) the server received + * @param {Object} server - mock server + * @returns {Array} the requests + */ +function dataRequests(server) { + return server.requests.filter((r) => r.path === "/i" && typeof r.params.hc === "undefined"); +} + +/** + * Writes a value into the storage folder the way the SDK stores it, before the SDK starts + * @param {String} dir - storage folder + * @param {String} key - storage key + * @param {*} value - value to store + */ +function seed(dir, key, value) { + fs.writeFileSync(path.join(dir, `__${key}.json`), JSON.stringify({ [key]: value })); +} + +/** + * Starts the SDK against a mock server, with its own storage folder + * @param {Object} server - mock server the SDK sends to + * @param {String} dir - storage folder + * @param {Object} config - init options + */ +function startSdk(server, dir, config) { + Countly.halt(false); + Countly.init({ app_key: appKey, url: server.url, storage_path: `${dir}${path.sep}`, interval: 100, ...config }); +} + +/** + * Waits until a condition is true, or until the time is up + * @param {Function} condition - returns true when the wait is over + * @param {Number} timeoutMs - longest time to wait + * @returns {Promise} true if the condition became true + */ +function waitUntil(condition, timeoutMs) { + return new Promise((resolve) => { + const started = Date.now(); + const check = () => { + if (condition()) { + resolve(true); + return; + } + if (Date.now() - started > timeoutMs) { + resolve(false); + return; + } + setTimeout(check, 20); + }; + check(); + }); +} + +/** + * Waits until the SDK holds the given value for a storage key + * @param {String} key - storage key + * @param {*} expected - value in the stored format + * @returns {Promise} true when the SDK holds it + */ +function storedValue(key, expected) { + return waitUntil(() => JSON.stringify(CountlyStorage.storeGet(key, null)) === JSON.stringify(expected), 5000); +} + +/** + * Waits until a key is written to the storage folder with the given value + * @param {String} dir - storage folder + * @param {String} key - storage key + * @param {*} expected - value the file must hold + * @returns {Promise} true when the file holds the value + */ +function storedOnDisk(dir, key, expected) { + return waitUntil(() => { + try { + return JSON.stringify(JSON.parse(fs.readFileSync(path.join(dir, `__${key}.json`), "utf8"))[key]) === JSON.stringify(expected); + } + catch (ex) { + return false; + } + }, 5000); +} + +/** + * Waits for a time + * @param {Number} ms - milliseconds + * @returns {Promise} resolves after the time + */ +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +module.exports = { appKey, settingsServer, settingsRequests, healthChecks, dataRequests, seed, startSdk, waitUntil, storedValue, storedOnDisk, sleep }; diff --git a/test/helpers/helper_functions.js b/test/helpers/helper_functions.js index 361771e..770c3e4 100644 --- a/test/helpers/helper_functions.js +++ b/test/helpers/helper_functions.js @@ -12,18 +12,22 @@ var CountlyStorage = require("../../lib/countly-storage"); var dir = path.resolve(__dirname, "../../"); var dir_test = path.resolve(__dirname, "../"); -// paths for convenience -const DIR_CLY = (`${dir}/data`); -const DIR_CLY_ID = (`${dir}/data/__cly_id.json`); -const DIR_CLY_ID_type = (`${dir}/data/__cly_id_type.json`); -const DIR_CLY_event = (`${dir}/data/__cly_event.json`); -const DIR_CLY_request = (`${dir}/data/__cly_queue.json`); +// paths for convenience, default storage folder is .countly-storage in the working directory (tests run from the SDK root) +const DIR_CLY = (`${dir}/.countly-storage`); +const DIR_CLY_ID = (`${dir}/.countly-storage/__cly_id.json`); +const DIR_CLY_ID_type = (`${dir}/.countly-storage/__cly_id_type.json`); +const DIR_CLY_event = (`${dir}/.countly-storage/__cly_event.json`); +const DIR_CLY_request = (`${dir}/.countly-storage/__cly_queue.json`); + +// Bulk paths for convenience, the default bulk folder is named after the app key used in the tests +const DIR_Bulk = (`${dir}/.countly-storage/bulk/YOUR_APP_KEY`); +const DIR_Bulk_bulk = (`${DIR_Bulk}/__cly_bulk_queue.json`); +const DIR_Bulk_event = (`${DIR_Bulk}/__cly_bulk_event.json`); +const DIR_Bulk_request = (`${DIR_Bulk}/__cly_req_queue.json`); -// Bulk paths for convenience -const DIR_Bulk = (`${dir}/bulk_data`); -const DIR_Bulk_bulk = (`${dir}/bulk_data/__cly_bulk_queue.json`); -const DIR_Bulk_event = (`${dir}/bulk_data/__cly_bulk_event.json`); -const DIR_Bulk_request = (`${dir}/bulk_data/__cly_req_queue.json`); +// default folders of SDK versions up to 24.10.4 +const DIR_Old_CLY = (`${dir}/data`); +const DIR_Old_Bulk = (`${dir}/bulk_data`); // Custom const DIR_Test = (`${dir_test}/customStorageDirectory`); @@ -115,12 +119,16 @@ async function clearStorage(customPath = null) { const relativePath = `../${customPath}`; const resolvedCustomPath = path.resolve(__dirname, relativePath); - await fsp.rm(DIR_CLY, { recursive: true, force: true }).catch(() => { }); - await fsp.rm(DIR_Bulk, { recursive: true, force: true }).catch(() => { }); - await fsp.rm(DIR_Test, { recursive: true, force: true }).catch(() => { }); + // retries cover files that a write of the previous test still holds open for a moment on Windows + const rmOptions = { recursive: true, force: true, maxRetries: 10, retryDelay: 50 }; + await fsp.rm(DIR_CLY, rmOptions).catch(() => { }); + await fsp.rm(DIR_Bulk, rmOptions).catch(() => { }); + await fsp.rm(DIR_Test, rmOptions).catch(() => { }); + await fsp.rm(DIR_Old_CLY, rmOptions).catch(() => { }); + await fsp.rm(DIR_Old_Bulk, rmOptions).catch(() => { }); if (resolvedCustomPath !== null && typeof resolvedCustomPath === 'string') { - await fsp.rm(resolvedCustomPath, { recursive: true, force: true }).catch(() => { }); + await fsp.rm(resolvedCustomPath, rmOptions).catch(() => { }); } const storageExists = await fsp.access(DIR_CLY).then(() => true).catch(() => false); @@ -172,8 +180,8 @@ function eventValidator(eventObject, eventQueue, time) { } // common parameter validation assert.ok(typeof eventQueue.timestamp !== 'undefined'); - assert.ok(typeof eventQueue.hour !== 'undefined'); - assert.ok(typeof eventQueue.dow !== 'undefined'); + assert.ok(eventQueue.hour > -1 && eventQueue.hour < 24); + assert.ok(eventQueue.dow > -1 && eventQueue.dow < 7); } /** * bunch of tests specifically gathered for other validators @@ -190,8 +198,8 @@ function requestBaseParamValidator(resultingObject, id) { assert.ok(typeof resultingObject.sdk_name !== 'undefined'); assert.ok(typeof resultingObject.sdk_version !== 'undefined'); assert.ok(typeof resultingObject.timestamp !== 'undefined'); - assert.ok(resultingObject.dow > -1 && resultingObject.dow < 24); - assert.ok(resultingObject.dow >= 0 && resultingObject.dow < 8); + assert.ok(resultingObject.hour > -1 && resultingObject.hour < 24); + assert.ok(resultingObject.dow > -1 && resultingObject.dow < 7); } /** * bunch of tests specifically gathered for testing crashes @@ -278,8 +286,8 @@ function viewEventValidator(name, viewObj, time) { assert.equal('[CLY]_view', viewObj.key); assert.equal(1, viewObj.count); assert.ok(typeof viewObj.timestamp !== 'undefined'); - assert.ok(viewObj.dow > -1 && viewObj.dow < 24); - assert.ok(viewObj.dow > 0 && viewObj.dow < 8); + assert.ok(viewObj.hour > -1 && viewObj.hour < 24); + assert.ok(viewObj.dow > -1 && viewObj.dow < 7); assert.equal(name, viewObj.segmentation.name); if (typeof time === 'undefined') { assert.equal(1, viewObj.segmentation.visit); diff --git a/test/helpers/mock_server.js b/test/helpers/mock_server.js new file mode 100644 index 0000000..ee83722 --- /dev/null +++ b/test/helpers/mock_server.js @@ -0,0 +1,103 @@ +const crypto = require("crypto"); +const http = require("http"); + +/** + * Starts a local Countly-like server on 127.0.0.1 that records every request it receives. + * Query string and form-encoded body parameters are merged into entry.params. + * @param {Function=} handler - (req, res, entry, count) answers a request itself; without it every request gets {"result":"Success"} + * @returns {Promise} url, requests, waitFor(predicate, timeoutMs) and close() + */ +function startMockServer(handler) { + const requests = []; + const server = http.createServer((req, res) => { + const chunks = []; + req.on("data", (chunk) => { + chunks.push(chunk); + }); + req.on("end", () => { + const body = Buffer.concat(chunks); + const parsed = new URL(req.url, "http://localhost"); + const params = Object.fromEntries(parsed.searchParams); + if ((req.headers["content-type"] || "").indexOf("application/x-www-form-urlencoded") === 0) { + Object.assign(params, Object.fromEntries(new URLSearchParams(body.toString("utf8")))); + } + const entry = { method: req.method, url: req.url, path: parsed.pathname, headers: req.headers, params: params, body: body }; + requests.push(entry); + if (handler) { + handler(req, res, entry, requests.length); + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }); + }); + return new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + resolve({ + url: `http://127.0.0.1:${server.address().port}`, + requests: requests, + waitFor: function(predicate, timeoutMs) { + return new Promise((done) => { + const started = Date.now(); + const check = () => { + if (predicate(requests)) { + done(true); + } + else if (Date.now() - started > timeoutMs) { + done(false); + } + else { + setTimeout(check, 20); + } + }; + check(); + }); + }, + close: function() { + if (typeof server.closeAllConnections === "function") { + server.closeAllConnections(); + } + return new Promise((done) => { + server.close(() => done()); + }); + }, + }); + }); + }); +} + +/** + * Lists the events of all received requests in arrival order, each with the device_id of its request + * @param {Array} requests - entries recorded by startMockServer + * @returns {Array} events with a device_id field added + */ +function receivedEvents(requests) { + const events = []; + requests.forEach((entry) => { + if (entry.params.events) { + JSON.parse(entry.params.events).forEach((event) => { + event.device_id = entry.params.device_id; + events.push(event); + }); + } + }); + return events; +} + +/** + * Checks a GET request's checksum the way Countly 26.01 servers do: the query is rebuilt with the WHATWG URL + * parser, the checksum parameter is removed, and the rest is hashed together with the salt + * @param {Object} entry - request recorded by startMockServer, or an object with a url + * @param {String} salt - salt configured for the app + * @returns {Boolean} true if the server would accept the checksum + */ +function passesServerChecksum(entry, salt) { + const parsed = new URL(entry.url, "http://localhost"); + const checksum = parsed.searchParams.get("checksum256"); + let payload = (parsed.pathname + parsed.search).substring(parsed.pathname.length + 1); + payload = payload.replace(`&checksum256=${checksum}`, "").replace(`checksum256=${checksum}`, ""); + const expected = crypto.createHash("sha256").update(payload + salt).digest("hex").toUpperCase(); + return expected === String(checksum).toUpperCase(); +} + +module.exports = { startMockServer, receivedEvents, passesServerChecksum }; diff --git a/test/helpers/run_script.js b/test/helpers/run_script.js new file mode 100644 index 0000000..2682256 --- /dev/null +++ b/test/helpers/run_script.js @@ -0,0 +1,79 @@ +const { spawn } = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +/** + * Runs a helper script in its own Node.js process, so behaviour that ends or crashes a process + * (process.exit, uncaught errors, cluster workers) cannot affect the test runner. + * @param {String} script - file name inside test/helpers/scripts + * @param {Object} env - extra environment variables for the script + * @param {Number} timeoutMs - the process is killed after this time + * @param {String=} cwd - working directory of the process + * @returns {Promise} code, signal, stdout, stderr and timedOut + */ +function runScript(script, env, timeoutMs, cwd) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [path.join(__dirname, "scripts", script)], { + env: { ...process.env, ...env }, + cwd: cwd || process.cwd(), + stdio: ["ignore", "pipe", "pipe", "ipc"], + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + child.stdout.on("data", (chunk) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + const timer = setTimeout(() => { + timedOut = true; + child.kill(); + }, timeoutMs); + child.on("exit", (code, signal) => { + clearTimeout(timer); + resolve({ code, signal, stdout, stderr, timedOut }); + }); + }); +} + +const createdDirs = []; + +/** + * Creates an empty temporary folder for one test + * @param {String} name - readable part of the folder name + * @returns {String} absolute folder path + */ +function makeTempDir(name) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), `countly-node-${name}-`)); + createdDirs.push(dir); + return dir; +} + +/** + * Removes the temporary folders created so far by makeTempDir + */ +function removeTempDirs() { + while (createdDirs.length) { + fs.rmSync(createdDirs.pop(), { recursive: true, force: true, maxRetries: 10, retryDelay: 50 }); + } +} + +/** + * Reads the value stored under a key in a storage file of the SDK + * @param {String} file - path of a __.json file + * @param {String} key - storage key + * @returns {*} stored value, or undefined when the file is missing or unreadable + */ +function readStoredFile(file, key) { + try { + return JSON.parse(fs.readFileSync(file, "utf8"))[key]; + } + catch (ex) { + return undefined; + } +} + +module.exports = { runScript, makeTempDir, removeTempDirs, readStoredFile }; diff --git a/test/helpers/scripts/bigint_values.js b/test/helpers/scripts/bigint_values.js new file mode 100644 index 0000000..26c1bcd --- /dev/null +++ b/test/helpers/scripts/bigint_values.js @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +/* global BigInt */ +const path = require("path"); +const Countly = require("../../../lib/countly"); + +Countly.init({ + app_key: "YOUR_APP_KEY", + url: process.env.COUNTLY_URL, + storage_path: `${process.env.STORAGE_DIR}${path.sep}`, + storage_type: process.env.STORAGE_TYPE || undefined, + device_id: "bigint-user", + interval: 200, +}); +const circular = { name: "loop" }; +circular.self = circular; +Countly.add_event({ key: "order_paid", segmentation: { order_id: BigInt("9007199254740993"), plan: "pro", loop: circular } }); +Countly.track_view("Checkout", { cart_id: BigInt(12) }); +setTimeout(() => { + console.log("survived"); + process.exit(0); +}, 2500); diff --git a/test/helpers/scripts/bulk_consent_replay.js b/test/helpers/scripts/bulk_consent_replay.js new file mode 100644 index 0000000..99ea87c --- /dev/null +++ b/test/helpers/scripts/bulk_consent_replay.js @@ -0,0 +1,17 @@ +/* eslint-disable no-console */ +const CountlyBulk = require("../../../lib/countly-bulk"); + +const server = new CountlyBulk({ + app_key: "YOUR_APP_KEY", + url: process.env.COUNTLY_URL, + interval: 200, +}); +server.start(); +const user = server.add_user({ device_id: "consent-user", require_consent: true }); +user.begin_session({ _os: "Linux", _os_version: "6", _app_version: "1.0" }, 0, 1700000000); +user.report_view("Home", 1700000000, 5); +user.add_consent(["sessions", "views"]); +setTimeout(() => { + console.log("survived"); + process.exit(0); +}, 3000); diff --git a/test/helpers/scripts/cluster_app.js b/test/helpers/scripts/cluster_app.js new file mode 100644 index 0000000..c2fce5b --- /dev/null +++ b/test/helpers/scripts/cluster_app.js @@ -0,0 +1,46 @@ +/* eslint-disable no-console */ +const cluster = require("cluster"); +const path = require("path"); +const Countly = require("../../../lib/countly"); + +const mode = process.env.CLUSTER_MODE; +const config = { + app_key: "YOUR_APP_KEY", + url: process.env.COUNTLY_URL, + storage_path: `${process.env.STORAGE_DIR}${path.sep}`, + interval: 200, +}; + +if (cluster.isMaster) { + if (mode === "primary_sdk" || mode === "worker_switch") { + config.device_id = "cluster-primary"; + Countly.init(config); + } + const worker = cluster.fork(); + setTimeout(() => { + worker.kill(); + process.exit(0); + }, Number(process.env.RUN_MS || 8000)); +} +else if (mode === "worker_switch") { + Countly.init(config); + Countly.track_view("Worker page"); + Countly.userData.set("plan", "worker-pro"); + Countly.add_log("worker breadcrumb"); + setTimeout(() => { + Countly.change_id("cluster-next-user", false); + Countly.log_error(new Error("after switch")); + }, 500); + setInterval(() => { }, 1000); +} +else { + config.standalone_worker = mode === "standalone"; + if (mode === "standalone") { + config.device_id = "cluster-worker"; + } + Countly.init(config); + Countly.begin_session(); + Countly.add_event({ key: "worker_event" }); + console.log(`worker device id: ${Countly.get_device_id()}`); + setInterval(() => { }, 1000); +} diff --git a/test/helpers/scripts/cluster_disconnected.js b/test/helpers/scripts/cluster_disconnected.js new file mode 100644 index 0000000..3fb14dc --- /dev/null +++ b/test/helpers/scripts/cluster_disconnected.js @@ -0,0 +1,19 @@ +/* eslint-disable no-console */ +const cluster = require("cluster"); +const path = require("path"); +const Countly = require("../../../lib/countly"); + +if (cluster.isMaster) { + const worker = cluster.fork(); + worker.on("disconnect", () => { + Countly.init({ app_key: "YOUR_APP_KEY", url: "http://127.0.0.1:1", storage_path: `${process.env.STORAGE_DIR}${path.sep}`, device_id: "primary", test_mode: true }); + setTimeout(() => { + console.log("main process still running"); + process.exit(0); + }, 1000); + }); +} +else { + process.disconnect(); + setTimeout(() => process.exit(0), 3000); +} diff --git a/test/helpers/scripts/cluster_worker_disconnects.js b/test/helpers/scripts/cluster_worker_disconnects.js new file mode 100644 index 0000000..1a2a1cc --- /dev/null +++ b/test/helpers/scripts/cluster_worker_disconnects.js @@ -0,0 +1,17 @@ +/* eslint-disable no-console */ +const cluster = require("cluster"); +const path = require("path"); +const Countly = require("../../../lib/countly"); + +if (cluster.isMaster) { + const started = Date.now(); + const worker = cluster.fork(); + worker.on("exit", () => { + console.log(`worker exited after ${Date.now() - started} ms`); + process.exit(0); + }); +} +else { + Countly.init({ app_key: "YOUR_APP_KEY", url: "http://127.0.0.1:1", storage_path: `${process.env.STORAGE_DIR}${path.sep}` }); + process.disconnect(); +} diff --git a/test/helpers/scripts/exit_after_call.js b/test/helpers/scripts/exit_after_call.js new file mode 100644 index 0000000..9fbcb15 --- /dev/null +++ b/test/helpers/scripts/exit_after_call.js @@ -0,0 +1,35 @@ +const fs = require("fs"); +const path = require("path"); +const Countly = require("../../../lib/countly"); + +const dir = process.env.STORAGE_DIR; +Countly.init({ + app_key: "YOUR_APP_KEY", + url: "http://127.0.0.1:1", + storage_path: `${dir}${path.sep}`, + test_mode: true, + device_id: "exit-user", +}); +for (let i = 0; i < 300; i++) { + Countly.user_details({ name: `user ${i}` }); +} + +const file = path.join(dir, "__cly_queue.json"); +let lastSize = -1; +let stableChecks = 0; +const poll = setInterval(() => { + let size = 0; + try { + size = fs.statSync(file).size; + } + catch (ex) { + size = 0; + } + stableChecks = size > 0 && size === lastSize ? stableChecks + 1 : 0; + lastSize = size; + if (stableChecks >= 5) { + clearInterval(poll); + Countly.user_details({ name: "last before exit" }); + process.exit(0); + } +}, 100); diff --git a/test/helpers/scripts/flush_callback_throws.js b/test/helpers/scripts/flush_callback_throws.js new file mode 100644 index 0000000..c423a27 --- /dev/null +++ b/test/helpers/scripts/flush_callback_throws.js @@ -0,0 +1,17 @@ +/* eslint-disable no-console */ +const path = require("path"); +const Countly = require("../../../lib/countly"); + +process.on("uncaughtException", (err) => { + console.log(`uncaught exception: ${err.message}`); + process.exit(0); +}); +process.on("unhandledRejection", (reason) => { + console.log(`unhandled rejection: ${reason && reason.message}`); + process.exit(0); +}); +Countly.init({ app_key: "YOUR_APP_KEY", url: "http://127.0.0.1:1", storage_path: `${process.env.STORAGE_DIR}${path.sep}`, device_id: "flush-user" }); +Countly.user_details({ name: "flush" }); +Countly.flush(() => { + throw new Error("thrown in flush callback"); +}); diff --git a/test/helpers/scripts/health_on_exit.js b/test/helpers/scripts/health_on_exit.js new file mode 100644 index 0000000..6509d18 --- /dev/null +++ b/test/helpers/scripts/health_on_exit.js @@ -0,0 +1,17 @@ +const path = require("path"); +const Countly = require("../../../lib/countly"); + +Countly.init({ + app_key: "YOUR_APP_KEY", + url: "http://127.0.0.1:1", + storage_path: `${process.env.STORAGE_DIR}${path.sep}`, + device_id: "exit-user", + interval: 100, + disable_health_check: true, + disable_sdk_behavior_settings_updates: true, +}); +Countly.change_id(""); +setTimeout(() => { + Countly.change_id(""); + process.exit(0); +}, 500); diff --git a/test/helpers/scripts/rejection.js b/test/helpers/scripts/rejection.js new file mode 100644 index 0000000..1a65881 --- /dev/null +++ b/test/helpers/scripts/rejection.js @@ -0,0 +1,17 @@ +/* eslint-disable no-console */ +const path = require("path"); +const Countly = require("../../../lib/countly"); + +Countly.init({ + app_key: "YOUR_APP_KEY", + url: process.env.COUNTLY_URL, + storage_path: `${process.env.STORAGE_DIR}${path.sep}`, + device_id: "rejection-user", + interval: 200, +}); +Countly.track_errors(); +Promise.reject(new Error("unhandled payment failure")); +setTimeout(() => { + console.log("still running"); + process.exit(0); +}, 2500); diff --git a/test/helpers/scripts/script_exits.js b/test/helpers/scripts/script_exits.js new file mode 100644 index 0000000..7ab7a02 --- /dev/null +++ b/test/helpers/scripts/script_exits.js @@ -0,0 +1,12 @@ +/* eslint-disable no-console */ +const path = require("path"); +const Countly = require("../../../lib/countly"); + +Countly.init({ + app_key: "YOUR_APP_KEY", + url: process.env.COUNTLY_URL, + storage_path: `${process.env.STORAGE_DIR}${path.sep}`, + device_id: "script-user", +}); +Countly.add_event({ key: process.env.EVENT_KEY || "backup_finished" }); +console.log("script body finished"); diff --git a/test/helpers/scripts/storage_location.js b/test/helpers/scripts/storage_location.js new file mode 100644 index 0000000..8797ec0 --- /dev/null +++ b/test/helpers/scripts/storage_location.js @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +const Countly = require("../../../lib/countly"); +const CountlyStorage = require("../../../lib/countly-storage"); + +const config = { + app_key: "YOUR_APP_KEY", + url: "http://127.0.0.1:1", + test_mode: true, +}; +if (process.env.DEVICE_ID) { + config.device_id = process.env.DEVICE_ID; +} +if (process.env.STORAGE_PATH) { + config.storage_path = process.env.STORAGE_PATH; +} +Countly.init(config); +Countly.add_event({ key: "location_check" }); +setTimeout(() => { + console.log(JSON.stringify({ deviceId: Countly.get_device_id(), storagePath: CountlyStorage.getStoragePath() })); + process.exit(0); +}, 800); diff --git a/test/tests_behavior_settings.js b/test/tests_behavior_settings.js new file mode 100644 index 0000000..a38964f --- /dev/null +++ b/test/tests_behavior_settings.js @@ -0,0 +1,499 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const Countly = require("../lib/countly"); +const CountlyStorage = require("../lib/countly-storage"); +const behavior = require("../lib/countly-behavior"); +const hp = require("./helpers/helper_functions"); +const { receivedEvents } = require("./helpers/mock_server"); +const { makeTempDir, removeTempDirs } = require("./helpers/run_script"); +const { appKey, settingsServer, settingsRequests, dataRequests, seed, startSdk, storedValue, storedOnDisk, sleep } = require("./helpers/behavior_settings"); + +describe("SDK behavior settings tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + afterEach(() => { + Countly.halt(true); + }); + after(() => { + removeTempDirs(); + }); + + it("1- reads the server's answer in both formats, ignores unknown keys and keeps only usable values", () => { + const warnings = []; + const v2 = behavior.parseAnswer(JSON.stringify({ + v: 2, t: 1, lg: { e: false }, ct: 1, c: { tracking: false, rqs: 50, bom_preset: "Default", filter_preset: "Blacklisting", jte: ["a"], lkl: "64", ltl: 0, eqs: 2.5, bom_rqp: 1.5, scui: 100000 }, + }), (message) => warnings.push(message)); + assert.deepStrictEqual(v2, { tracking: false, rqs: 50, scui: behavior.MAX_REFRESH_HOURS }); + assert.equal(warnings.length, 4, warnings.join("\n")); + assert.deepStrictEqual(behavior.parseAnswer(JSON.stringify({ v: 1, t: 1, c: { networking: true } })), { networking: true }); + assert.deepStrictEqual(behavior.parseAnswer(JSON.stringify({ c: {} })), {}); + [JSON.stringify({ result: "Request does not match checksum" }), "[]", "\"text\"", "not json", JSON.stringify({ c: [] }), JSON.stringify({ c: null })].forEach((answer) => { + assert.strictEqual(behavior.parseAnswer(answer), null, answer); + }); + assert.strictEqual(behavior.parseProvided({ tracking: false }), null); + assert.deepStrictEqual(behavior.parseProvided({ c: { tracking: false } }), { tracking: false }); + }); + + it("2- uses the server's value, else the developer's, else the default, and pauses only when all three backoff conditions hold", () => { + assert.equal(behavior.valueOf({ rqs: 5 }, "rqs", 100), 5); + assert.equal(behavior.valueOf({}, "rqs", 100), 100); + assert.equal(behavior.valueOf({}, "bom_d"), 60); + assert.equal(behavior.valueOf({}, "tracking"), true); + const base = { durationSeconds: 10, queueLength: 50, queueSize: 100, ageHours: 24, bom_at: 10, bom_rqp: 0.5, bom_ra: 24 }; + assert.equal(behavior.shouldBackOff(base), true); + assert.equal(behavior.shouldBackOff({ ...base, durationSeconds: 9.9 }), false); + assert.equal(behavior.shouldBackOff({ ...base, queueLength: 51 }), false); + assert.equal(behavior.shouldBackOff({ ...base, ageHours: 24.1 }), false); + }); + + it("3- fetches the settings at start with the app's identity, stores them and puts them into effect", async() => { + const server = await settingsServer({ body: { v: 2, t: 1, c: { lkl: 7, rqs: 300 } } }); + const dir = makeTempDir("sbs-fetch"); + startSdk(server, dir, { device_id: "settings-user", app_version: "3.1" }); + const inUse = await storedValue("cly_sbs", { lkl: 7, rqs: 300 }); + await server.close(); + assert.ok(inUse, "the fetched settings were not put into effect"); + const request = settingsRequests(server)[0]; + assert.equal(request.params.app_key, appKey); + assert.equal(request.params.device_id, "settings-user"); + assert.equal(request.params.sdk_name, "javascript_native_nodejs"); + assert.equal(request.params.av, "3.1"); + assert.ok(request.params.sdk_version); + assert.equal(Countly.maxKeyLength, 7); + assert.ok(await storedOnDisk(dir, "cly_sbs", { lkl: 7, rqs: 300 }), "the settings were not stored"); + }); + + it("4- with tracking off nothing new is recorded, data recorded before is still delivered, and turning it on resumes recording", async() => { + const state = { body: { c: { tracking: false } } }; + const server = await settingsServer(state); + startSdk(server, makeTempDir("sbs-tracking"), { device_id: "tracking-user", interval: 10000 }); + Countly.add_event({ key: "before_switch" }); + await storedValue("cly_sbs", { tracking: false }); + Countly.add_event({ key: "while_off" }); + Countly.user_details({ name: "while off" }); + Countly.request({ app_key: appKey, device_id: "tracking-user", begin_session: 1 }); + await Countly.flush(5); + assert.deepStrictEqual(receivedEvents(dataRequests(server)).map((e) => e.key), ["before_switch"]); + assert.equal(dataRequests(server).filter((r) => r.params.user_details || r.params.begin_session).length, 0); + + state.body = { c: { tracking: true } }; + Countly.change_id("tracking-user-2", false); + await storedValue("cly_sbs", { tracking: true }); + Countly.add_event({ key: "after_switch" }); + await Countly.flush(5); + await server.close(); + assert.deepStrictEqual(receivedEvents(dataRequests(server)).map((e) => e.key), ["before_switch", "after_switch"]); + }); + + it("5- with networking off data is kept but only the settings are fetched, and turning it on delivers the backlog", async() => { + const state = { body: { c: { networking: false } } }; + const server = await settingsServer(state); + startSdk(server, makeTempDir("sbs-networking"), { device_id: "network-user" }); + await storedValue("cly_sbs", { networking: false }); + Countly.add_event({ key: "kept_event" }); + Countly.user_details({ name: "kept" }); + const started = Date.now(); + const flushed = await Countly.flush(5); + assert.strictEqual(flushed, false); + assert.ok(Date.now() - started < 1000, "flush waited although nothing can be sent"); + await sleep(500); + assert.equal(dataRequests(server).length, 0, "data was sent while networking was off"); + assert.equal(server.requests.filter((r) => r.params.hc).length, 0, "the health check was sent while networking was off"); + let remoteConfigError = null; + Countly.fetch_remote_config((err) => { + remoteConfigError = err; + }); + assert.ok(remoteConfigError instanceof Error, "remote config did not report that networking is off"); + + state.body = { c: { networking: true } }; + Countly.change_id("network-user-2", false); + await storedValue("cly_sbs", { networking: true }); + const delivered = await Countly.flush(5); + Countly.fetch_remote_config(); + const remoteConfigSent = await server.waitFor((reqs) => reqs.some((r) => r.params.method === "fetch_remote_config"), 5000); + await server.close(); + assert.strictEqual(delivered, true); + assert.ok(remoteConfigSent, "remote config was not fetched after networking was turned on"); + assert.equal(server.requests.filter((r) => r.params.method === "fetch_remote_config" && r.params.device_id === "network-user").length, 0, "remote config was fetched while networking was off"); + assert.deepStrictEqual(receivedEvents(dataRequests(server)).map((e) => e.key), ["kept_event"]); + assert.equal(dataRequests(server).filter((r) => r.params.user_details).length, 1); + }); + + it("6- each feature switch turns off only its own feature", async() => { + const switches = { st: false, vt: false, cet: false, crt: false, lt: false }; + const server = await settingsServer({ body: { c: switches } }); + const dir = makeTempDir("sbs-features"); + seed(dir, "cly_sbs", switches); + startSdk(server, dir, { device_id: "feature-user", country_code: "TR", city: "Izmir" }); + Countly.begin_session(); + Countly.track_view("Home"); + Countly.add_event({ key: "custom_event" }); + Countly.add_event({ key: "[CLY]_star_rating", segmentation: { rating: 5 } }); + Countly.log_error(new Error("not recorded")); + Countly.user_details({ name: "Feature User" }); + Countly.end_session(); + await Countly.flush(5); + await server.close(); + const requests = dataRequests(server); + assert.equal(requests.filter((r) => r.params.begin_session || r.params.end_session || r.params.crash).length, 0); + assert.deepStrictEqual(receivedEvents(requests).map((e) => e.key), ["[CLY]_star_rating"]); + const details = requests.find((r) => r.params.user_details); + assert.ok(details, "user details were not sent"); + assert.equal(details.params.location, ""); + assert.equal(details.params.country_code, undefined); + assert.equal(details.params.city, undefined); + }); + + it("7- key, value, segmentation and batch limits from the server apply to custom events recorded afterwards, and internal events keep their key and segmentation", async() => { + const limits = { lkl: 5, lvs: 3, lsv: 1, eqs: 1 }; + const server = await settingsServer({ body: { c: limits } }); + const dir = makeTempDir("sbs-limits"); + seed(dir, "cly_sbs", limits); + startSdk(server, dir, { device_id: "limit-user", max_key_length: 50, max_value_size: 50, max_segmentation_values: 10, max_events: 10 }); + Countly.add_event({ key: "abcdefghij", segmentation: { plan: "premium", tier: "gold" } }); + Countly.add_event({ key: "second" }); + Countly.track_view("Home"); + Countly.add_event({ key: "[CLY]_star_rating", segmentation: { rating: 5, widget_id: "w1", platform: "node" } }); + await Countly.flush(5); + await server.close(); + const withEvents = dataRequests(server).filter((r) => r.params.events); + assert.equal(withEvents.length, 4, "events were not sent one per request"); + const events = receivedEvents(withEvents); + assert.deepStrictEqual(events.map((e) => e.key), ["abcde", "secon", "[CLY]_view", "[CLY]_star_rating"]); + assert.deepStrictEqual(events[0].segmentation, { plan: "pre" }); + assert.equal(events[2].segmentation.name, "Hom"); + assert.equal(events[2].segmentation.visit, 1); + assert.deepStrictEqual(events[3].segmentation, { rating: 5, widget_id: "w1", platform: "node" }); + }); + + it("8- the server can require consent and lift the developer's consent requirement, and without cr the developer's requirement applies", async() => { + const state = { body: { c: { cr: true } } }; + const server = await settingsServer(state); + const required = makeTempDir("sbs-cr-on"); + seed(required, "cly_sbs", { cr: true }); + startSdk(server, required, { device_id: "cr-user" }); + Countly.add_event({ key: "without_consent" }); + Countly.add_consent("events"); + Countly.add_event({ key: "with_consent" }); + await Countly.flush(5); + assert.deepStrictEqual(receivedEvents(dataRequests(server)).map((e) => e.key), ["with_consent"]); + + state.body = { c: { cr: false } }; + const lifted = makeTempDir("sbs-cr-off"); + seed(lifted, "cly_sbs", { cr: false }); + startSdk(server, lifted, { device_id: "cr-user-2", require_consent: true }); + Countly.add_event({ key: "recorded_without_consent" }); + await Countly.flush(5); + + state.body = { c: {} }; + const developers = makeTempDir("sbs-cr-developer"); + seed(developers, "cly_sbs", {}); + startSdk(server, developers, { device_id: "cr-user-3", require_consent: true }); + Countly.add_event({ key: "still_needs_consent" }); + await Countly.flush(5); + await server.close(); + const keys = receivedEvents(dataRequests(server)).map((e) => e.key); + assert.deepStrictEqual(keys, ["with_consent", "recorded_without_consent"]); + }); + + it("9- a setting the server stops sending goes back to the developer's value", async() => { + const server = await settingsServer({ body: { c: {} } }); + const dir = makeTempDir("sbs-removed"); + seed(dir, "cly_sbs", { lkl: 5 }); + startSdk(server, dir, { device_id: "removed-user", max_key_length: 50 }); + assert.equal(Countly.maxKeyLength, 5, "the stored settings were not used at start"); + const replaced = await storedValue("cly_sbs", {}); + await server.close(); + assert.ok(replaced, "the empty answer did not replace the stored settings"); + assert.equal(Countly.maxKeyLength, 50); + }); + + it("10- a failed request or an unusable answer keeps the settings in use", async() => { + const state = { status: 400, body: { result: "Invalid method" } }; + const server = await settingsServer(state); + const dir = makeTempDir("sbs-keep"); + seed(dir, "cly_sbs", { lkl: 5 }); + startSdk(server, dir, { device_id: "keep-user" }); + const answers = [ + { status: 400, body: { result: "Invalid method" } }, + { status: 429, body: "Too Many Requests" }, + { status: 200, body: { result: "Request does not match checksum" } }, + { status: 200, body: "not json" }, + { status: 200, body: { c: [] } }, + ]; + for (let i = 0; i < answers.length; i++) { + state.status = answers[i].status; + state.body = answers[i].body; + if (i > 0) { + Countly.change_id(`keep-user-${i}`, false); + } + // eslint-disable-next-line no-await-in-loop + await server.waitFor((reqs) => reqs.filter((r) => r.params.method === "sc").length > i, 5000); + // eslint-disable-next-line no-await-in-loop + await sleep(100); + assert.equal(Countly.maxKeyLength, 5, `answer ${i} changed the settings`); + } + await server.close(); + assert.deepStrictEqual(CountlyStorage.storeGet("cly_sbs", null), { lkl: 5 }); + }); + + it("11- settings stored by an earlier run are used at the next start before the first request", async() => { + const firstServer = await settingsServer({ body: { c: { networking: false } } }); + const dir = makeTempDir("sbs-restart"); + startSdk(firstServer, dir, { device_id: "restart-user" }); + await storedValue("cly_sbs", { networking: false }); + await firstServer.close(); + Countly.halt(false); + + const secondServer = await settingsServer({ status: 500, body: "down" }); + startSdk(secondServer, dir, { device_id: "restart-user" }); + Countly.add_event({ key: "recorded_offline" }); + const flushed = await Countly.flush(5); + await secondServer.waitFor((reqs) => reqs.some((r) => r.params.method === "sc"), 5000); + await sleep(300); + await secondServer.close(); + assert.strictEqual(flushed, false); + assert.equal(dataRequests(secondServer).length, 0, "data was sent although the stored settings turned networking off"); + }); + + it("12- settings provided at init are used until the server's settings have been fetched once", async() => { + const server = await settingsServer({ status: 500, body: "down" }); + const dir = makeTempDir("sbs-provided"); + startSdk(server, dir, { device_id: "provided-user", behavior_settings: { c: { lkl: 6 } } }); + assert.equal(Countly.maxKeyLength, 6); + await server.waitFor((reqs) => reqs.some((r) => r.params.method === "sc"), 5000); + await server.close(); + assert.equal(fs.existsSync(path.join(dir, "__cly_sbs.json")), false, "provided settings must not be stored as the server's"); + }); + + it("13- a device ID change without merge fetches the settings again with the new ID", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("sbs-id-change"), { device_id: "first-id" }); + await server.waitFor((reqs) => reqs.some((r) => r.params.hc), 5000); + Countly.change_id("second-id", false); + const refetched = await server.waitFor((reqs) => reqs.filter((r) => r.params.method === "sc").length === 2, 5000); + await server.close(); + assert.ok(refetched, "the settings were not fetched again"); + assert.deepStrictEqual(settingsRequests(server).map((r) => r.params.device_id), ["first-id", "second-id"]); + }); + + it("14- a device ID change with merge fetches the settings after the merge request was delivered", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("sbs-merge"), { device_id: "merge-from" }); + await server.waitFor((reqs) => reqs.some((r) => r.params.hc), 5000); + Countly.change_id("merge-to", true); + await server.waitFor((reqs) => reqs.filter((r) => r.params.method === "sc").length === 2, 5000); + await server.close(); + const mergeIndex = server.requests.findIndex((r) => r.params.old_device_id === "merge-from"); + const fetchIndex = server.requests.findIndex((r) => r.params.method === "sc" && r.params.device_id === "merge-to"); + assert.ok(mergeIndex !== -1, "the merge request was not sent"); + assert.ok(fetchIndex > mergeIndex, "the settings were fetched before the merge request was delivered"); + }); + + it("15- a device ID change with merge fetches the settings right away when the merge request cannot be queued", async() => { + const server = await settingsServer({ body: { c: { tracking: false } } }); + startSdk(server, makeTempDir("sbs-merge-off"), { device_id: "merge-off-from" }); + await storedValue("cly_sbs", { tracking: false }); + Countly.change_id("merge-off-to", true); + const fetched = await server.waitFor((reqs) => reqs.some((r) => r.params.method === "sc" && r.params.device_id === "merge-off-to"), 5000); + await server.close(); + assert.ok(fetched, "the settings were not fetched after the device ID change"); + assert.equal(server.requests.filter((r) => r.params.old_device_id).length, 0); + }); + + it("16- without consent only the settings fetch and the health check are sent", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("sbs-no-consent"), { device_id: "no-consent-user", require_consent: true }); + Countly.begin_session(); + Countly.add_event({ key: "no_consent_event" }); + const checked = await server.waitFor((reqs) => reqs.some((r) => r.params.hc), 5000); + await sleep(300); + await server.close(); + assert.ok(checked, "the health check was not sent"); + assert.equal(settingsRequests(server).length, 1); + assert.equal(dataRequests(server).length, 0); + }); + + it("17- a device ID change without merge removes consent without telling the server", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("sbs-consent-reset"), { device_id: "consent-user", require_consent: true }); + Countly.add_consent("events"); + await server.waitFor((reqs) => reqs.some((r) => r.params.consent), 5000); + Countly.change_id("consent-user-2", false); + await sleep(1500); + await server.close(); + assert.equal(server.requests.filter((r) => r.params.consent).length, 1); + assert.equal(Countly.check_consent("events"), false); + }); + + it("18- consent changes are not sent while tracking is off", async() => { + const server = await settingsServer({ body: { c: { tracking: false } } }); + const dir = makeTempDir("sbs-consent-tracking"); + seed(dir, "cly_sbs", { tracking: false }); + startSdk(server, dir, { device_id: "consent-off-user", require_consent: true }); + Countly.add_consent("events"); + await sleep(1500); + await server.close(); + assert.equal(server.requests.filter((r) => r.params.consent).length, 0); + assert.equal(Countly.check_consent("events"), true); + }); + + it("19- pauses sending for bom_d seconds after a slow answer", async() => { + const server = await settingsServer({ body: { c: { bom_at: 1, bom_d: 2, bom_rqp: 1 } } }, (req, res, entry) => { + const delay = entry.params.user_details && JSON.parse(entry.params.user_details).name === "slow" ? 1100 : 0; + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }, delay); + }); + const dir = makeTempDir("sbs-backoff"); + seed(dir, "cly_sbs", { bom_at: 1, bom_d: 2, bom_rqp: 1 }); + startSdk(server, dir, { device_id: "backoff-user", disable_health_check: true }); + Countly.user_details({ name: "slow" }); + Countly.user_details({ name: "next" }); + const delivered = await server.waitFor((reqs) => reqs.some((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "next"), 10000); + await server.close(); + assert.ok(delivered, "the next request was not sent after the pause"); + const slow = server.requests.find((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "slow"); + const next = server.requests.find((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "next"); + assert.ok(next.receivedAt - slow.receivedAt >= 2900, `the next request came ${next.receivedAt - slow.receivedAt} ms after the slow one`); + const counters = CountlyStorage.storeGet("cly_hc", null); + assert.deepStrictEqual([counters.bom, counters.cbom], [1, 1]); + }); + + it("20- the developer's backoff opt-out wins over the server's backoff settings", async() => { + const server = await settingsServer({ body: { c: { bom: true, bom_at: 1, bom_d: 2, bom_rqp: 1 } } }, (req, res, entry) => { + const delay = entry.params.user_details && JSON.parse(entry.params.user_details).name === "slow" ? 1100 : 0; + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }, delay); + }); + const dir = makeTempDir("sbs-backoff-off"); + seed(dir, "cly_sbs", { bom: true, bom_at: 1, bom_d: 2, bom_rqp: 1 }); + startSdk(server, dir, { device_id: "backoff-off-user", disable_health_check: true, disable_backoff_mechanism: true }); + Countly.user_details({ name: "slow" }); + Countly.user_details({ name: "next" }); + await server.waitFor((reqs) => reqs.some((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "next"), 10000); + await server.close(); + const slow = server.requests.find((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "slow"); + const next = server.requests.find((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "next"); + assert.ok(next.receivedAt - slow.receivedAt < 2000, `the next request waited ${next.receivedAt - slow.receivedAt} ms`); + }); + + it("21- drops queued requests recorded more than dort hours ago, also while flush waits for them", async() => { + const server = await settingsServer({ body: { c: { dort: 1 } } }, (req, res, entry) => { + const delay = entry.params.user_details && JSON.parse(entry.params.user_details).name === "slow" ? 2000 : 0; + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }, delay); + }); + const dir = makeTempDir("sbs-dort"); + const now = Date.now(); + seed(dir, "cly_sbs", { dort: 1 }); + seed(dir, "cly_queue", [ + { app_key: appKey, device_id: "dort-user", user_details: JSON.stringify({ name: "too old" }), timestamp: now - (2 * 3600000) }, + { app_key: appKey, device_id: "dort-user", user_details: JSON.stringify({ name: "slow" }), timestamp: now - 60000 }, + { app_key: appKey, device_id: "dort-user", user_details: JSON.stringify({ name: "turns old" }), timestamp: now - 3600000 + 1000 }, + ]); + startSdk(server, dir, { device_id: "dort-user" }); + const started = Date.now(); + const delivered = await Countly.flush(10); + const elapsed = Date.now() - started; + await server.close(); + assert.strictEqual(delivered, false, "flush must report that a request it waited for was dropped"); + assert.ok(elapsed < 5000, `flush resolved after ${elapsed} ms instead of when the request was dropped`); + assert.deepStrictEqual(dataRequests(server).filter((r) => r.params.user_details).map((r) => JSON.parse(r.params.user_details).name), ["slow"]); + assert.ok(await storedOnDisk(dir, "cly_queue", []), "the dropped requests are still stored"); + }); + + it("22- flush sends right away during a backoff pause", async() => { + const backoff = { bom_at: 1, bom_d: 30, bom_rqp: 1 }; + const server = await settingsServer({ body: { c: backoff } }, (req, res, entry) => { + const delay = entry.params.user_details && JSON.parse(entry.params.user_details).name === "slow" ? 1100 : 0; + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }, delay); + }); + const dir = makeTempDir("sbs-backoff-flush"); + seed(dir, "cly_sbs", backoff); + startSdk(server, dir, { device_id: "backoff-flush-user", disable_health_check: true }); + Countly.user_details({ name: "slow" }); + await server.waitFor((reqs) => reqs.some((r) => r.params.user_details), 5000); + await storedValue("cly_hc", { el: 0, wl: 0, sc: -1, em: "", bom: 1, cbom: 1 }); + Countly.user_details({ name: "urgent" }); + await sleep(500); + const sentBeforeFlush = server.requests.some((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "urgent"); + const started = Date.now(); + const delivered = await Countly.flush(5); + const elapsed = Date.now() - started; + await server.close(); + assert.equal(sentBeforeFlush, false, "the request was sent during the pause"); + assert.strictEqual(delivered, true); + assert.ok(elapsed < 2000, `flush waited ${elapsed} ms`); + }); + + it("23- the queue size from the server applies, dropping the oldest requests", async() => { + const limits = { rqs: 2, networking: false }; + const server = await settingsServer({ body: { c: limits } }); + const dir = makeTempDir("sbs-queue-size"); + seed(dir, "cly_sbs", limits); + startSdk(server, dir, { device_id: "queue-user", queue_size: 100 }); + ["A", "B", "C", "D"].forEach((name) => { + Countly.user_details({ name: name }); + }); + await server.waitFor((reqs) => reqs.some((r) => r.params.method === "sc"), 5000); + await server.close(); + const queued = CountlyStorage.storeGet("cly_queue", []).filter((r) => r.user_details).map((r) => JSON.parse(r.user_details).name); + assert.deepStrictEqual(queued, ["B", "C", "D"]); + }); + + it("24- the session update interval from the server applies", async() => { + const server = await settingsServer({ body: { c: { sui: 1 } } }); + const dir = makeTempDir("sbs-session-update"); + seed(dir, "cly_sbs", { sui: 1 }); + startSdk(server, dir, { device_id: "session-update-user", session_update: 60 }); + Countly.begin_session(); + const extended = await server.waitFor((reqs) => reqs.some((r) => r.params.session_duration && !r.params.end_session), 4000); + await server.close(); + assert.ok(extended, "the session was not extended after one second"); + }); + + it("25- breadcrumb and stack trace limits from the server apply to crashes", async() => { + const limits = { lbc: 2, ltlpt: 2, ltl: 10 }; + const server = await settingsServer({ body: { c: limits } }); + const dir = makeTempDir("sbs-crash-limits"); + seed(dir, "cly_sbs", limits); + startSdk(server, dir, { device_id: "crash-limit-user" }); + ["one", "two", "three", "four"].forEach((record) => { + Countly.add_log(record); + }); + Countly.log_error(new Error("limits")); + await Countly.flush(5); + await server.close(); + const crash = JSON.parse(dataRequests(server).find((r) => r.params.crash).params.crash); + assert.equal(crash._logs, "two\nthree\nfour"); + const lines = crash._error.split("\n"); + assert.equal(lines.length, 2, crash._error); + assert.equal(lines[0], "Error: lim"); + assert.ok(lines.every((line) => line.length <= 10), crash._error); + }); + + it("26- a device ID change with merge keeps consent", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("sbs-merge-consent"), { device_id: "merge-consent-user", require_consent: true }); + Countly.add_consent("events"); + Countly.change_id("merge-consent-user-2", true); + Countly.add_event({ key: "after_merge" }); + await Countly.flush(5); + await server.waitFor((reqs) => reqs.some((r) => r.params.consent), 5000); + await server.close(); + assert.equal(Countly.check_consent("events"), true); + assert.deepStrictEqual(receivedEvents(dataRequests(server)).map((e) => [e.key, e.device_id]), [["after_merge", "merge-consent-user-2"]]); + assert.equal(server.requests.filter((r) => r.params.consent).length, 1); + }); +}); diff --git a/test/tests_bulk.js b/test/tests_bulk.js index d0a6724..67381ba 100644 --- a/test/tests_bulk.js +++ b/test/tests_bulk.js @@ -1,15 +1,24 @@ /* eslint-disable no-console */ /* global runthis */ const assert = require("assert"); +const cluster = require("cluster"); +const path = require("path"); const CountlyBulk = require("../lib/countly-bulk"); var hp = require("./helpers/helper_functions"); -var storage = require("../lib/countly-storage"); var testUtils = require("./helpers/test_utils"); +// every CountlyBulk instance registers a cluster "fork" listener and this file intentionally creates one instance per test +cluster.setMaxListeners(30); + const { StorageTypes } = CountlyBulk; var appKey = "YOUR_APP_KEY"; var serverUrl = "https://tests.url.cly"; +// relative storage paths are resolved against the working directory, which is the SDK root when tests run +var customPath = "test/customStorageDirectory/"; +var customFolder = path.resolve(customPath); +// every bulk instance stores its files in its own folder, named after its app key +var defaultBulkFolder = path.resolve(".countly-storage", "bulk", appKey); function validateCrash(validator, nonfatal) { assert.ok(validator.crash._os); @@ -75,13 +84,14 @@ function validateCreatedBulkData(bulk) { const isValid = hp.validateUserDetails(actualUserDetails, testUtils.getUserDetailsObj()); assert.equal(true, isValid); - var testUser3Request = reqQueue.find((request) => request.device_id === "TestUser3"); - assert.ok(testUser3Request); + // requests must be queued in recording order: user details, raw request, crash + var testUser3Request = reqQueue[1]; assert.strictEqual(testUser3Request.device_id, "TestUser3"); assert.strictEqual(testUser3Request.app_key, "YOUR_APP_KEY"); assert.strictEqual(testUser3Request.sdk_name, "javascript_native_nodejs_bulk"); - var testUser4Request = reqQueue.find((request) => request.device_id === "TestUser4"); + var testUser4Request = reqQueue[2]; + assert.strictEqual(testUser4Request.device_id, "TestUser4"); validateCrash(testUser4Request, true); } @@ -102,7 +112,7 @@ describe("Bulk Tests", () => { app_key: appKey, url: serverUrl, }); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); shouldFilesExist(false); createBulkData(bulk); @@ -119,37 +129,37 @@ describe("Bulk Tests", () => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, storage_type: StorageTypes.FILE, }); shouldFilesExist(true, true); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true, true); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); done(); }, hp.sWait); }); // file storage type in config - // path should become the default "../bulk_data/" + // path should become the default bulk folder of this app key it("3- CNR_file", (done) => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, storage_type: StorageTypes.FILE, }); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); done(); }, hp.mWait); }); @@ -162,14 +172,14 @@ describe("Bulk Tests", () => { url: serverUrl, storage_type: StorageTypes.MEMORY, }); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); shouldFilesExist(false); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(false); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); done(); }, hp.mWait); }); @@ -180,17 +190,17 @@ describe("Bulk Tests", () => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, storage_type: StorageTypes.MEMORY, }); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); shouldFilesExist(false); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(false); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); done(); }, hp.mWait); }); @@ -203,14 +213,14 @@ describe("Bulk Tests", () => { url: serverUrl, persist_queue: true, }); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); done(); }, hp.mWait); }); @@ -223,14 +233,14 @@ describe("Bulk Tests", () => { url: serverUrl, persist_queue: false, }); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); shouldFilesExist(false); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(false); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); done(); }, hp.mWait); }); @@ -241,17 +251,17 @@ describe("Bulk Tests", () => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, persist_queue: true, }); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); done(); }, hp.mWait); }); @@ -262,17 +272,17 @@ describe("Bulk Tests", () => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, persist_queue: false, }); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); shouldFilesExist(false); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(false); - assert.equal(storage.getStoragePath(), undefined); + assert.equal(bulk._getStorage().getStoragePath(), undefined); done(); }, hp.mWait); }); @@ -286,14 +296,14 @@ describe("Bulk Tests", () => { storage_type: StorageTypes.FILE, persist_queue: true, }); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); done(); }, hp.mWait); }); @@ -307,14 +317,14 @@ describe("Bulk Tests", () => { storage_type: StorageTypes.FILE, persist_queue: true, }); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../bulk_data/"); + assert.equal(bulk._getStorage().getStoragePath(), defaultBulkFolder); done(); }, hp.mWait); }); @@ -326,40 +336,73 @@ describe("Bulk Tests", () => { app_key: appKey, url: serverUrl, storage_type: StorageTypes.FILE, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, persist_queue: true, }); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); done(); }, hp.mWait); }); // persist_queue is false and storage type is file and custom path is given - // storage type should overrule and switch into the custom path file storage + // storage type should overrule and switch into the custom path file storage it("13- CNR_cPath_persistFalse_file", (done) => { var bulk = new CountlyBulk({ app_key: appKey, url: serverUrl, storage_type: StorageTypes.FILE, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, persist_queue: false, }); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); shouldFilesExist(true); createBulkData(bulk); setTimeout(() => { validateCreatedBulkData(bulk); shouldFilesExist(true); - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(bulk._getStorage().getStoragePath(), customFolder); done(); }, hp.mWait); }); + + // explicitly provided time fields must reach the queue untouched, including valid zero values (midnight, Sunday, UTC) + it("14- add_request preserves explicit zero time fields", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + }); + bulk.add_request({ + device_id: "zeroUser", timestamp: 1735516800, hour: 0, dow: 0, tz: 0, + }); + bulk.add_bulk_request([{ + device_id: "zeroBulkUser", timestamp: 1735516800, hour: 0, dow: 0, tz: 0, + }]); + + setTimeout(() => { + // both add_request and add_bulk_request push into the same request queue in call order + var reqQueue = bulk._getBulkRequestQueue(); + assert.strictEqual(reqQueue.length, 2); + var req = reqQueue[0]; + assert.strictEqual(req.device_id, "zeroUser"); + assert.strictEqual(req.timestamp, 1735516800); + assert.strictEqual(req.hour, 0); + assert.strictEqual(req.dow, 0); + assert.strictEqual(req.tz, 0); + var bulkReq = reqQueue[1]; + assert.strictEqual(bulkReq.device_id, "zeroBulkUser"); + assert.strictEqual(bulkReq.timestamp, 1735516800); + assert.strictEqual(bulkReq.hour, 0); + assert.strictEqual(bulkReq.dow, 0); + assert.strictEqual(bulkReq.tz, 0); + done(); + }, hp.sWait); + }); }); \ No newline at end of file diff --git a/test/tests_bulk_fixes.js b/test/tests_bulk_fixes.js new file mode 100644 index 0000000..ccf9076 --- /dev/null +++ b/test/tests_bulk_fixes.js @@ -0,0 +1,188 @@ +/* eslint-disable no-console */ +const assert = require("assert"); +const cluster = require("cluster"); +const CountlyBulk = require("../lib/countly-bulk"); +const hp = require("./helpers/helper_functions"); +const { startMockServer } = require("./helpers/mock_server"); +const { runScript } = require("./helpers/run_script"); + +const appKey = "YOUR_APP_KEY"; + +// every CountlyBulk instance registers a cluster "fork" listener and these tests create several instances +cluster.setMaxListeners(60); + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function bulkInnerRequests(requests) { + const inner = []; + requests.filter((r) => r.path === "/i/bulk").forEach((r) => { + JSON.parse(r.params.requests).forEach((request) => { + inner.push(request); + }); + }); + return inner; +} + +describe("Bulk fix tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + + it("1- a bulk user created with an IP address can record everything, and every request carries the IP", () => { + const bulk = new CountlyBulk({ app_key: appKey, url: "http://127.0.0.1:1" }); + const user = bulk.add_user({ device_id: "ip-user", ip_address: "8.8.8.8" }); + user.begin_session({ _os: "Linux", _os_version: "6", _app_version: "1.0" }, 90, 1700000000) + .user_details({ name: "IP user" }) + .report_view("Home", 1700000060, 30) + .report_conversion("campaign-1", "campaign-user-1") + .custom_set("plan", "pro") + .custom_save(); + const forUser = bulk._getBulkRequestQueue().filter((r) => r.device_id === "ip-user"); + assert.ok(forUser.length >= 6, `only ${forUser.length} requests were queued`); + assert.ok(forUser.some((r) => r.begin_session === 1)); + forUser.forEach((request) => { + assert.equal(request.ip_address, "8.8.8.8"); + }); + }); + + it("2- consent changes of a bulk user reach the queue with the user's device ID", async() => { + const bulk = new CountlyBulk({ app_key: appKey, url: "http://127.0.0.1:1" }); + const user = bulk.add_user({ device_id: "consent-user", require_consent: true }); + user.add_consent(["events", "users"]); + await sleep(1200); + user.remove_consent("events"); + await sleep(1200); + const consents = bulk._getBulkRequestQueue().filter((r) => r.consent); + assert.equal(consents.length, 2); + consents.forEach((request) => { + assert.equal(request.device_id, "consent-user"); + }); + assert.equal(JSON.parse(consents[0].consent).events, true); + assert.equal(JSON.parse(consents[1].consent).events, false); + assert.equal(JSON.parse(consents[1].consent).users, true); + }); + + it("3- giving consent replays the session and view that were held back, without crashing", async() => { + const server = await startMockServer(); + const run = await runScript("bulk_consent_replay.js", { COUNTLY_URL: server.url }, 20000); + await server.close(); + assert.equal(run.code, 0, run.stderr); + assert.ok(run.stdout.indexOf("survived") !== -1, "the process crashed"); + const inner = bulkInnerRequests(server.requests).filter((r) => r.device_id === "consent-user"); + assert.ok(inner.some((r) => r.begin_session === 1), "the held-back session was not replayed"); + assert.ok(inner.some((r) => r.events && r.events.some((e) => e.key === "[CLY]_view" && e.segmentation.name === "Home")), "the held-back view was not replayed"); + }); + + it("4- the queue-empty callback waits until data waiting for a retry is delivered", async() => { + let delivered = 0; + const server = await startMockServer((req, res, entry, count) => { + if (count === 1) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end('{"result":"Error"}'); + return; + } + delivered++; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }); + const bulk = new CountlyBulk({ app_key: appKey, url: server.url, interval: 100, fail_timeout: 1 }); + bulk.add_request({ device_id: "retry-user", begin_session: 1 }); + const deliveredWhenEmpty = await new Promise((resolve) => { + let reported = false; + bulk.start(() => { + if (!reported) { + reported = true; + resolve(delivered); + } + }); + setTimeout(() => resolve(-1), 10000); + }); + bulk.stop(); + await server.close(); + assert.equal(deliveredWhenEmpty, 1, "the callback reported an empty queue while data was still waiting"); + }); + + it("5- queued bulk requests are sent one after another without waiting for the next heartbeat", async() => { + const server = await startMockServer(); + const bulk = new CountlyBulk({ app_key: appKey, url: server.url, interval: 10000 }); + for (let i = 0; i < 120; i++) { + bulk.add_request({ device_id: `user-${i}`, begin_session: 1 }); + } + bulk.start(); + const allSent = await server.waitFor((reqs) => bulkInnerRequests(reqs).length === 120, 3000); + bulk.stop(); + await server.close(); + assert.ok(allSent, `only ${bulkInnerRequests(server.requests).length} of 120 requests were sent within 3 s`); + assert.equal(server.requests.length, 3); + }); + + it("6- users whose events were all sent no longer stay in memory", async() => { + const server = await startMockServer(); + const bulk = new CountlyBulk({ app_key: appKey, url: server.url, interval: 100 }); + for (let i = 0; i < 3; i++) { + bulk.add_event(`device-${i}`, { key: "visit" }); + } + bulk.start(); + await server.waitFor((reqs) => bulkInnerRequests(reqs).length === 3, 3000); + bulk.stop(); + await server.close(); + assert.deepStrictEqual(Object.keys(bulk._getBulkEventQueue()), []); + }); + + it("7- warns that tamper protection (salt) cannot be used with bulk requests", () => { + const warnings = []; + const originalWarn = console.warn; + console.warn = (...args) => { + warnings.push(args.join(" ")); + }; + let bulk; + try { + bulk = new CountlyBulk({ app_key: appKey, url: "http://127.0.0.1:1", salt: "secret" }); + } + finally { + console.warn = originalWarn; + } + assert.ok(bulk); + assert.ok(warnings.some((w) => w.indexOf("salt") !== -1), "no warning about salt was printed"); + }); + + it("8- a bulk request that gets no answer is given up after request_timeout and sent again", async() => { + const server = await startMockServer((req, res, entry, count) => { + if (count > 1) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + } + }); + const bulk = new CountlyBulk({ app_key: appKey, url: server.url, interval: 100, fail_timeout: 1, request_timeout: 1 }); + bulk.add_request({ device_id: "bulk-timeout-user", begin_session: 1 }); + bulk.start(); + const resent = await server.waitFor((reqs) => bulkInnerRequests(reqs).filter((r) => r.device_id === "bulk-timeout-user").length >= 2, 10000); + bulk.stop(); + await server.close(); + assert.ok(resent, "the bulk queue stopped after the unanswered request"); + }); + + it("9- a bulk request whose answer is cut off midway is sent again", async() => { + const server = await startMockServer((req, res, entry, count) => { + if (count === 1) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.write('{"res'); + setTimeout(() => req.socket.destroy(), 50); + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); + }); + const bulk = new CountlyBulk({ app_key: appKey, url: server.url, interval: 100, fail_timeout: 1 }); + bulk.add_request({ device_id: "bulk-cutoff-user", begin_session: 1 }); + bulk.start(); + const resent = await server.waitFor((reqs) => bulkInnerRequests(reqs).filter((r) => r.device_id === "bulk-cutoff-user").length >= 2, 10000); + bulk.stop(); + await server.close(); + assert.ok(resent, "the bulk queue stopped after the cut-off response"); + }); +}); diff --git a/test/tests_cluster.js b/test/tests_cluster.js new file mode 100644 index 0000000..7d284ce --- /dev/null +++ b/test/tests_cluster.js @@ -0,0 +1,97 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const hp = require("./helpers/helper_functions"); +const { startMockServer, receivedEvents } = require("./helpers/mock_server"); +const { runScript, makeTempDir, removeTempDirs } = require("./helpers/run_script"); + +async function runCluster(mode, settings) { + const server = await startMockServer(settings ? (req, res, entry) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(entry.path === "/o/sdk" ? JSON.stringify({ c: settings }) : '{"result":"Success"}'); + } : undefined); + const dir = makeTempDir(`cluster-${mode}`); + if (settings) { + fs.writeFileSync(path.join(dir, "__cly_sbs.json"), JSON.stringify({ cly_sbs: settings })); + } + const run = await runScript("cluster_app.js", { COUNTLY_URL: server.url, STORAGE_DIR: dir, CLUSTER_MODE: mode, RUN_MS: "8000" }, 30000); + await server.close(); + return { run, requests: server.requests, dir, output: run.stdout + run.stderr }; +} + +describe("Cluster tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + after(() => { + removeTempDirs(); + }); + + it("1- a worker warns when its main process does not run Countly, instead of silently dropping data", async() => { + const result = await runCluster("worker_only"); + assert.equal(result.requests.length, 0); + assert.ok(result.output.indexOf("standalone_worker") !== -1, "no warning explains that nothing is sent"); + }); + + it("2- standalone_worker lets a worker send its own data from its own storage folder", async() => { + const result = await runCluster("standalone"); + const events = receivedEvents(result.requests); + assert.ok(events.some((e) => e.key === "worker_event" && e.device_id === "cluster-worker"), "the worker's event was not sent"); + assert.ok(result.requests.some((r) => r.params.begin_session === "1" && r.params.device_id === "cluster-worker")); + assert.ok(fs.existsSync(path.join(result.dir, "worker-1", "__cly_id.json")), "the worker did not use its own storage folder"); + assert.equal(result.output.indexOf("standalone_worker"), -1); + }); + + it("3- workers still hand their data to a main process that runs Countly", async() => { + const result = await runCluster("primary_sdk"); + const events = receivedEvents(result.requests); + assert.ok(events.some((e) => e.key === "worker_event" && e.device_id === "cluster-primary"), "the worker's event did not reach the main process"); + assert.equal(result.output.indexOf("standalone_worker"), -1); + }); + + it("4- the main process does not crash when it starts Countly while a worker is disconnecting", async() => { + const run = await runScript("cluster_disconnected.js", { STORAGE_DIR: makeTempDir("cluster-disconnected") }, 20000); + assert.equal(run.code, 0, run.stderr); + assert.ok(run.stdout.indexOf("main process still running") !== -1, run.stderr); + }); + + it("5- a worker that disconnects exits right away and does not warn", async() => { + const run = await runScript("cluster_worker_disconnects.js", { STORAGE_DIR: makeTempDir("cluster-worker-exit") }, 20000); + const match = /worker exited after (\d+) ms/.exec(run.stdout); + assert.ok(match, run.stdout + run.stderr); + assert.ok(Number(match[1]) < 3000, `the worker needed ${match[1]} ms to exit`); + assert.equal((run.stdout + run.stderr).indexOf("standalone_worker"), -1); + }); + + it("6- a user switch in a worker keeps the previous user's open view, profile changes and breadcrumbs with that user", async() => { + const result = await runCluster("worker_switch"); + const views = receivedEvents(result.requests).filter((e) => e.key === "[CLY]_view" && typeof e.dur !== "undefined"); + assert.equal(views.length, 1); + assert.equal(views[0].device_id, "cluster-primary"); + const profile = result.requests.find((r) => r.params.user_details); + assert.equal(profile.params.device_id, "cluster-primary"); + assert.equal(JSON.parse(profile.params.user_details).custom.plan, "worker-pro"); + const crash = result.requests.find((r) => r.params.crash); + assert.equal(crash.params.device_id, "cluster-next-user"); + assert.equal(typeof JSON.parse(crash.params.crash)._logs, "undefined"); + }); + + it("7- only the process that sends the data fetches the SDK behavior settings and sends the health check, also after a user switch in a worker", async() => { + const [primary, standalone, switched] = await Promise.all([runCluster("primary_sdk"), runCluster("standalone"), runCluster("worker_switch")]); + const settingsIds = (result) => result.requests.filter((r) => r.path === "/o/sdk" && r.params.method === "sc").map((r) => r.params.device_id); + const healthIds = (result) => result.requests.filter((r) => typeof r.params.hc !== "undefined").map((r) => r.params.device_id); + assert.deepStrictEqual(settingsIds(primary), ["cluster-primary"]); + assert.deepStrictEqual(healthIds(primary), ["cluster-primary"]); + assert.deepStrictEqual(settingsIds(standalone), ["cluster-worker"]); + assert.deepStrictEqual(healthIds(standalone), ["cluster-worker"]); + assert.deepStrictEqual(settingsIds(switched), ["cluster-primary", "cluster-next-user"]); + assert.deepStrictEqual(healthIds(switched), ["cluster-primary"]); + }); + + it("8- the data a worker hands to the main process follows the main process's SDK behavior settings", async() => { + const result = await runCluster("primary_sdk", { st: false, cet: false }); + assert.ok(result.requests.some((r) => typeof r.params.hc !== "undefined"), "the main process did not run"); + assert.equal(receivedEvents(result.requests).filter((e) => e.key === "worker_event").length, 0, "the worker's custom event was recorded although custom events are turned off"); + assert.equal(result.requests.filter((r) => r.params.begin_session).length, 0, "the worker's session was recorded although sessions are turned off"); + }); +}); diff --git a/test/tests_consents.js b/test/tests_consents.js index a89b4e1..aabe83f 100644 --- a/test/tests_consents.js +++ b/test/tests_consents.js @@ -1,5 +1,4 @@ var assert = require("assert"); -var fs = require("fs"); var hp = require("./helpers/helper_functions"); var Countly = require("../lib/countly"); @@ -97,7 +96,7 @@ describe("Internal event consent tests", () => { Countly.change_id("Richard Wagner II", false); events(); setTimeout(() => { - assert.ok(!fs.existsSync(hp.eventDir)); + assert.deepStrictEqual(hp.readEventQueue(), []); done(); }, hp.sWait); }); diff --git a/test/tests_crash_safety.js b/test/tests_crash_safety.js new file mode 100644 index 0000000..1999f07 --- /dev/null +++ b/test/tests_crash_safety.js @@ -0,0 +1,84 @@ +/* global BigInt */ +const assert = require("assert"); +const path = require("path"); +const Countly = require("../lib/countly"); +const hp = require("./helpers/helper_functions"); +const cc = require("../lib/countly-common"); +const { startMockServer, receivedEvents } = require("./helpers/mock_server"); +const { runScript, makeTempDir, removeTempDirs } = require("./helpers/run_script"); + +async function checkBigIntScript(storageType) { + const server = await startMockServer(); + const env = { COUNTLY_URL: server.url, STORAGE_DIR: makeTempDir("bigint") }; + if (storageType) { + env.STORAGE_TYPE = storageType; + } + const run = await runScript("bigint_values.js", env, 20000); + await server.close(); + assert.equal(run.code, 0, run.stderr); + assert.ok(run.stdout.indexOf("survived") !== -1, "the app crashed"); + const events = receivedEvents(server.requests); + const order = events.find((e) => e.key === "order_paid"); + assert.ok(order, "the event was not delivered"); + assert.equal(order.segmentation.order_id, "9007199254740993"); + assert.equal(order.segmentation.plan, "pro"); + assert.deepStrictEqual(order.segmentation.loop, { name: "loop" }); + const view = events.find((e) => e.key === "[CLY]_view"); + assert.equal(view.segmentation.cart_id, "12"); +} + +describe("Crash safety tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + afterEach(() => { + Countly.halt(true); + }); + after(() => { + removeTempDirs(); + }); + + it("1- a BigInt or a self-referencing object in event and view data does not crash the app (file storage)", async() => { + await checkBigIntScript(); + }); + + it("2- a BigInt or a self-referencing object in event and view data does not crash the app (memory storage)", async() => { + await checkBigIntScript("memory"); + }); + + it("3- BigInt values in user details and custom user properties are stored as text", () => { + Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", interval: 10000, storage_path: `${makeTempDir("bigint-user")}${path.sep}`, device_id: "big-user" }); + const loop = { level: 1 }; + loop.self = loop; + Countly.user_details({ name: "Big", custom: { account_id: BigInt(42), loop: loop } }); + Countly.userData.set("customer_id", BigInt("12345678901234567890")); + Countly.userData.save(); + Countly.log_error(new Error("with big segments"), { order_id: BigInt(7) }); + const queue = hp.readRequestQueue(); + const details = JSON.parse(queue[0].user_details); + assert.equal(details.custom.account_id, "42"); + assert.deepStrictEqual(details.custom.loop, { level: 1 }); + assert.equal(JSON.parse(queue[1].user_details).custom.customer_id, "12345678901234567890"); + assert.equal(JSON.parse(queue[2].crash)._custom.order_id, "7"); + }); + + it("4- track_errors reports an unhandled promise rejection as non-fatal and the app keeps running", async() => { + const server = await startMockServer(); + const run = await runScript("rejection.js", { COUNTLY_URL: server.url, STORAGE_DIR: makeTempDir("rejection") }, 20000); + await server.close(); + assert.ok(run.stdout.indexOf("still running") !== -1, "the app stopped after the rejection"); + const crashes = server.requests.filter((r) => r.params.crash).map((r) => JSON.parse(r.params.crash)); + assert.equal(crashes.length, 1); + assert.equal(crashes[0]._nonfatal, true); + assert.ok(crashes[0]._error.indexOf("unhandled payment failure") !== -1); + }); + + it("5- safe copies serialize exactly like the original wherever the original can be serialized", () => { + /* eslint-disable no-new-wrappers */ + const value = { + text: new String("boxed"), number: new Number(7), flag: new Boolean(false), when: new Date(0), list: [1, "a", null, undefined], skip: undefined, fn: () => 1, nested: { deep: [{ a: 1 }] }, + }; + /* eslint-enable no-new-wrappers */ + assert.equal(JSON.stringify(cc.makeJsonSafe(value)), JSON.stringify(value)); + }); +}); diff --git a/test/tests_crashes.js b/test/tests_crashes.js index 12f2688..2bccd3b 100644 --- a/test/tests_crashes.js +++ b/test/tests_crashes.js @@ -1,5 +1,6 @@ /* eslint-disable no-console */ /* global runthis */ +var assert = require("assert"); var Countly = require("../lib/countly"); var hp = require("./helpers/helper_functions"); @@ -52,8 +53,31 @@ describe("Unhandled Error logic", () => { it("Validate unhandled rejection recording", (done) => { setTimeout(() => { var req = hp.readRequestQueue()[0]; - hp.crashRequestValidator(req, false); + // the process keeps running after an unhandled rejection, so it is recorded as non-fatal + hp.crashRequestValidator(req, true); done(); }, hp.mWait); }); }); +describe("Error handler registration logic", () => { + before(async() => { + await hp.clearStorage(); + }); + it("Repeated track_errors should not stack error handlers", (done) => { + // initialize SDK + initMain(); + Countly.track_errors(); + var uncaughtCount = process.listenerCount("uncaughtException"); + var rejectionCount = process.listenerCount("unhandledRejection"); + // repeated calls must replace the previous handlers, not add new ones + Countly.track_errors(); + Countly.track_errors(); + assert.equal(process.listenerCount("uncaughtException"), uncaughtCount); + assert.equal(process.listenerCount("unhandledRejection"), rejectionCount); + // halt must remove the SDK handlers + Countly.halt(true); + assert.equal(process.listenerCount("uncaughtException"), uncaughtCount - 1); + assert.equal(process.listenerCount("unhandledRejection"), rejectionCount - 1); + done(); + }); +}); diff --git a/test/tests_delivery.js b/test/tests_delivery.js new file mode 100644 index 0000000..5edbe74 --- /dev/null +++ b/test/tests_delivery.js @@ -0,0 +1,246 @@ +const assert = require("assert"); +const path = require("path"); +const Countly = require("../lib/countly"); +const cc = require("../lib/countly-common"); +const hp = require("./helpers/helper_functions"); +const { startMockServer, receivedEvents, passesServerChecksum } = require("./helpers/mock_server"); +const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); + +const appKey = "YOUR_APP_KEY"; + +function answerSuccess(res) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"result":"Success"}'); +} + +function initSending(config) { + Countly.halt(false); + Countly.init({ app_key: appKey, storage_path: `${makeTempDir("delivery")}${path.sep}`, ...config }); +} + +describe("Delivery tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + afterEach(() => { + Countly.halt(true); + }); + after(() => { + removeTempDirs(); + }); + + it("1- retries a request that gets no answer after request_timeout, and later data still arrives", async() => { + const server = await startMockServer((req, res, entry, count) => { + if (count > 1) { + answerSuccess(res); + } + }); + initSending({ url: server.url, device_id: "timeout-user", interval: 100, fail_timeout: 1, request_timeout: 1, disable_sdk_behavior_settings_updates: true, disable_health_check: true }); + Countly.user_details({ name: "first" }); + Countly.user_details({ name: "second" }); + const delivered = await server.waitFor((reqs) => reqs.some((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "second"), 10000); + await server.close(); + assert.ok(delivered, "the queue stopped after the unanswered request"); + const firstAttempts = server.requests.filter((r) => r.params.user_details && JSON.parse(r.params.user_details).name === "first"); + assert.equal(firstAttempts.length, 2); + }); + + it("2- treats a response that is cut off midway as failed and sends the request again", async() => { + const server = await startMockServer((req, res, entry, count) => { + if (count === 1) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.write('{"res'); + setTimeout(() => req.socket.destroy(), 50); + return; + } + answerSuccess(res); + }); + initSending({ url: server.url, device_id: "cutoff-user", interval: 100, fail_timeout: 1, disable_sdk_behavior_settings_updates: true, disable_health_check: true }); + Countly.user_details({ name: "cut" }); + const resent = await server.waitFor((reqs) => reqs.length >= 2, 10000); + await server.close(); + assert.ok(resent, "the queue stopped after the cut-off response"); + assert.equal(JSON.parse(server.requests[1].params.user_details).name, "cut"); + }); + + it("3- lets http_options add a header to every request, including GET requests", async() => { + const server = await startMockServer(); + initSending({ + url: server.url, + device_id: "header-user", + interval: 100, + http_options: (options) => { + options.headers["x-api-gateway"] = "gw-1"; + }, + }); + Countly.user_details({ name: "header-check" }); + const delivered = await server.waitFor((reqs) => reqs.some((r) => r.params.user_details), 5000); + await server.close(); + assert.ok(delivered, "no request reached the server"); + const request = server.requests.find((r) => r.params.user_details); + assert.equal(request.method, "GET"); + assert.equal(request.headers["x-api-gateway"], "gw-1"); + }); + + it("4- signs requests containing apostrophes so 26.01 servers accept the checksum", async() => { + const salt = "e2e-salt"; + const server = await startMockServer(); + initSending({ url: server.url, device_id: "salt-user", interval: 100, salt: salt }); + Countly.add_event({ key: "profile_saved", segmentation: { owner: "O'Neil", note: "it's (fine)!*" } }); + const delivered = await server.waitFor((reqs) => reqs.some((r) => r.params.events), 5000); + await server.close(); + assert.ok(delivered, "the event was not sent"); + const request = server.requests.find((r) => r.params.events); + assert.equal(request.method, "GET"); + assert.ok(passesServerChecksum(request, salt), "the checksum does not match the server's check"); + const event = JSON.parse(request.params.events)[0]; + assert.equal(event.segmentation.owner, "O'Neil"); + assert.equal(event.segmentation.note, "it's (fine)!*"); + }); + + it("5- flush sends every queued request one after another, without waiting for heartbeats", async() => { + const server = await startMockServer(); + initSending({ url: server.url, device_id: "flush-user", interval: 10000 }); + for (let i = 0; i < 5; i++) { + Countly.user_details({ name: `user ${i}` }); + } + Countly.add_event({ key: "flush_event" }); + const started = Date.now(); + const result = await Countly.flush(); + const elapsed = Date.now() - started; + await server.close(); + assert.strictEqual(result, true); + assert.ok(elapsed < 5000, `flush took ${elapsed} ms`); + assert.equal(server.requests.filter((r) => r.params.user_details).length, 5); + assert.deepStrictEqual(receivedEvents(server.requests).map((e) => e.key), ["flush_event"]); + assert.equal(hp.readRequestQueue().length, 0); + }); + + it("6- flush resolves false when the server cannot be reached, and the data stays queued", async() => { + initSending({ url: "http://127.0.0.1:1", device_id: "offline-user", interval: 10000 }); + Countly.user_details({ name: "kept" }); + const result = await Countly.flush(); + assert.strictEqual(result, false); + const queue = hp.readRequestQueue(); + assert.equal(queue.length, 1); + assert.equal(JSON.parse(queue[0].user_details).name, "kept"); + }); + + it("7- a script that records data exits by itself after the data is delivered", async() => { + const server = await startMockServer(); + const dir = makeTempDir("script"); + const run = await runScript("script_exits.js", { COUNTLY_URL: server.url, STORAGE_DIR: dir }, 20000); + await server.close(); + assert.equal(run.timedOut, false, "the script did not exit by itself"); + assert.equal(run.code, 0); + assert.deepStrictEqual(receivedEvents(server.requests).map((e) => e.key), ["backup_finished"]); + }); + + it("8- a script exits even when the server cannot be reached, and the next run delivers the stored data", async() => { + const dir = makeTempDir("script-offline"); + const offline = await runScript("script_exits.js", { COUNTLY_URL: "http://127.0.0.1:1", STORAGE_DIR: dir }, 20000); + assert.equal(offline.timedOut, false, "the script did not exit while the server was unreachable"); + const stored = readStoredFile(path.join(dir, "__cly_queue.json"), "cly_queue"); + assert.ok(Array.isArray(stored) && stored.length === 1, "the undelivered event was not kept on disk"); + + const server = await startMockServer(); + const online = await runScript("script_exits.js", { COUNTLY_URL: server.url, STORAGE_DIR: dir, EVENT_KEY: "second_run" }, 20000); + await server.close(); + assert.equal(online.timedOut, false); + const events = receivedEvents(server.requests); + assert.deepStrictEqual(events.map((e) => e.key), ["backup_finished", "second_run"]); + assert.ok(events.every((e) => e.device_id === "script-user")); + }); + + it("9- flush resolves false when one of its requests is dropped because the queue is full", async() => { + const server = await startMockServer((req, res) => { + setTimeout(() => answerSuccess(res), 300); + }); + initSending({ url: server.url, device_id: "overflow-user", interval: 10000, queue_size: 1 }); + Countly.user_details({ name: "A" }); + Countly.user_details({ name: "B" }); + const flushed = Countly.flush(); + Countly.user_details({ name: "C" }); + Countly.user_details({ name: "D" }); + const result = await flushed; + await server.close(); + assert.strictEqual(result, false); + const names = server.requests.filter((r) => r.params.user_details).map((r) => JSON.parse(r.params.user_details).name); + assert.equal(names.indexOf("B"), -1); + }); + + it("10- an error thrown in the flush callback surfaces like in any other callback, not as an unhandled rejection", async() => { + const run = await runScript("flush_callback_throws.js", { STORAGE_DIR: makeTempDir("flush-callback") }, 20000); + assert.ok(run.stdout.indexOf("uncaught exception: thrown in flush callback") !== -1, run.stdout + run.stderr); + }); + + it("11- signs values with any character so that the checksum check of 26.01 servers passes", () => { + const salt = "parity-salt"; + const characters = ["πŸ˜€", "πŸ‘πŸ½", "π„ž", "\u{10FFFF}"]; + for (let code = 0; code <= 0xFFFF; code++) { + if (code < 0xD800 || code > 0xDFFF) { + characters.push(String.fromCharCode(code)); + } + } + const failing = characters.filter((character) => { + const params = { app_key: appKey, device_id: `user${character}`, events: JSON.stringify([{ key: `key${character}`, segmentation: { value: character } }]) }; + const query = cc.addChecksum(cc.serializeParams(params), salt, false, false); + return !passesServerChecksum({ url: `/i?${query}` }, salt); + }); + assert.deepStrictEqual(failing.map((character) => `U+${character.codePointAt(0).toString(16).toUpperCase()}`), []); + }); + + it("12- a device ID cut in the middle of an emoji is still sent, and does not block the data behind it", async() => { + const server = await startMockServer(); + const cutName = "AliπŸ˜€".substring(0, 4); + initSending({ url: server.url, device_id: cutName, interval: 100, fail_timeout: 1 }); + Countly.add_event({ key: "first" }); + Countly.change_id("healthy-user", false); + Countly.add_event({ key: "after_switch" }); + const delivered = await Countly.flush(10); + await server.close(); + assert.strictEqual(delivered, true); + const events = receivedEvents(server.requests); + assert.deepStrictEqual(events.map((e) => [e.key, e.device_id]), [["first", "Ali\uFFFD"], ["after_switch", "healthy-user"]]); + }); + + it("13- flush before init resolves false, also through its callback", async() => { + Countly.halt(false); + const result = await Countly.flush(); + const viaCallback = await new Promise((resolve) => { + Countly.flush(1, resolve); + }); + assert.strictEqual(result, false); + assert.strictEqual(viaCallback, false); + }); + + it("14- flush resolves true right away when nothing is waiting to be sent", async() => { + const server = await startMockServer(); + initSending({ url: server.url, device_id: "idle-user", interval: 10000, disable_sdk_behavior_settings_updates: true, disable_health_check: true }); + const started = Date.now(); + const result = await Countly.flush(); + const elapsed = Date.now() - started; + await server.close(); + assert.strictEqual(result, true); + assert.ok(elapsed < 1000, `flush took ${elapsed} ms`); + assert.equal(server.requests.length, 0); + }); + + it("15- flush stops waiting after its time limit, and the data is still delivered afterwards", async() => { + const server = await startMockServer((req, res) => { + setTimeout(() => answerSuccess(res), 1500); + }); + initSending({ url: server.url, device_id: "slow-user", interval: 100, request_timeout: 10 }); + Countly.user_details({ name: "slow" }); + const started = Date.now(); + const result = await Countly.flush(0.3); + const elapsed = Date.now() - started; + const later = await Countly.flush(5); + await server.close(); + assert.strictEqual(result, false); + assert.ok(elapsed < 1400, `flush waited ${elapsed} ms instead of stopping at its time limit`); + assert.strictEqual(later, true); + assert.equal(server.requests.filter((r) => r.params.user_details).length, 1); + assert.equal(hp.readRequestQueue().length, 0); + }); +}); diff --git a/test/tests_device_id_type.js b/test/tests_device_id_type.js index 4e7c683..76f50d0 100644 --- a/test/tests_device_id_type.js +++ b/test/tests_device_id_type.js @@ -220,8 +220,8 @@ describe("Device ID type tests", () => { checkRequestsForT(req, cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); done(); }, hp.sWait); - }, hp.lWait); - }, hp.lWait); + }, hp.sWait); + }, hp.sWait); }); it("9- With stored sdk ID and no new ID, flag set", (done) => { diff --git a/test/tests_health_check.js b/test/tests_health_check.js new file mode 100644 index 0000000..1a30948 --- /dev/null +++ b/test/tests_health_check.js @@ -0,0 +1,186 @@ +const assert = require("assert"); +const path = require("path"); +const Countly = require("../lib/countly"); +const hp = require("./helpers/helper_functions"); +const { passesServerChecksum } = require("./helpers/mock_server"); +const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); +const { appKey, settingsServer, settingsRequests, healthChecks, dataRequests, seed, startSdk, storedValue, storedOnDisk, sleep } = require("./helpers/behavior_settings"); + +const cleared = { el: 0, wl: 0, sc: -1, em: "", bom: 0, cbom: 0 }; + +/** + * Tells whether a recorded request is a health check + * @param {Object} request - request recorded by the mock server + * @returns {Boolean} true for a health check + */ +function isHealthCheck(request) { + return typeof request.params.hc !== "undefined"; +} + +describe("Health check tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + afterEach(() => { + Countly.halt(true); + }); + after(() => { + removeTempDirs(); + }); + + it("1- sends one health check per start, after the settings fetch, with the counters, the app version and the app's identity", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("hc-payload"), { device_id: "hc-user", app_version: "2.5" }); + const sent = await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + Countly.change_id("hc-user-2", false); + await server.waitFor((reqs) => reqs.filter((r) => r.params.method === "sc").length === 2, 5000); + Countly.begin_session(); + await Countly.flush(5); + await server.close(); + assert.ok(sent, "the health check was not sent"); + const checks = healthChecks(server); + assert.equal(checks.length, 1, "more than one health check was sent in one run"); + const check = checks[0]; + assert.ok(server.requests.indexOf(check) > server.requests.indexOf(settingsRequests(server)[0]), "the health check was sent before the settings were fetched"); + assert.equal(check.path, "/i"); + assert.deepStrictEqual(JSON.parse(check.params.hc), cleared); + assert.deepStrictEqual(JSON.parse(check.params.metrics), { _app_version: "2.5" }); + assert.equal(check.params.app_key, appKey); + assert.equal(check.params.device_id, "hc-user"); + assert.equal(check.params.sdk_name, "javascript_native_nodejs"); + assert.ok(check.params.sdk_version); + assert.equal(hp.readRequestQueue().filter(isHealthCheck).length, 0, "the health check was queued"); + }); + + it("2- a request the server refuses is reported in the next start's health check, and the counters are cleared once the server confirms it", async() => { + const refusal = JSON.stringify({ result: "Internal server error", detail: "x".repeat(1500) }); + const refusing = await settingsServer({ body: { c: {} } }, (req, res) => { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(refusal); + }); + const dir = makeTempDir("hc-failure"); + startSdk(refusing, dir, { device_id: "hc-failure-user", disable_health_check: true }); + Countly.user_details({ name: "refused" }); + const flushed = await Countly.flush(5); + await refusing.close(); + assert.strictEqual(flushed, false); + const failure = { el: 2, wl: 0, sc: 500, em: refusal.substring(0, 1000), bom: 0, cbom: 0 }; + assert.ok(await storedOnDisk(dir, "cly_hc", failure), "the refused request was not stored for the health check"); + + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, dir, { device_id: "hc-failure-user" }); + await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + const reset = await storedValue("cly_hc", cleared); + const delivered = await Countly.flush(5); + await server.close(); + assert.deepStrictEqual(JSON.parse(healthChecks(server)[0].params.hc), failure); + assert.ok(reset, "the counters were not cleared after the server confirmed the health check"); + assert.strictEqual(delivered, true); + assert.equal(dataRequests(server).filter((r) => r.params.user_details).length, 1); + }); + + it("3- the counters stay stored when the server does not confirm the health check, and the next start sends them again", async() => { + const stored = { el: 3, wl: 2, sc: 503, em: "busy", bom: 1, cbom: 1 }; + const dir = makeTempDir("hc-unconfirmed"); + seed(dir, "cly_hc", stored); + const refusing = await settingsServer({ body: { c: {} }, hcStatus: 500 }); + startSdk(refusing, dir, { device_id: "hc-unconfirmed-user" }); + await refusing.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + const withRefusal = { ...stored, el: stored.el + 1 }; + const kept = await storedValue("cly_hc", withRefusal); + await sleep(300); + await refusing.close(); + assert.equal(healthChecks(refusing).length, 1, "the health check was sent again in the same run"); + assert.deepStrictEqual(JSON.parse(healthChecks(refusing)[0].params.hc), stored); + assert.ok(kept, "the counters were not kept, with the refused health check counted as an error"); + + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, dir, { device_id: "hc-unconfirmed-user" }); + await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + const reset = await storedValue("cly_hc", cleared); + await server.close(); + assert.deepStrictEqual(JSON.parse(healthChecks(server)[0].params.hc), withRefusal); + assert.ok(reset, "the counters were not cleared after the server confirmed the health check"); + }); + + it("4- no health check when it is turned off, while networking is off or in test mode, but turning off settings updates keeps it", async() => { + const state = { body: { c: {} } }; + const server = await settingsServer(state); + const fromDevice = (deviceId) => server.requests.filter((r) => r.params.device_id === deviceId); + + startSdk(server, makeTempDir("hc-disabled"), { device_id: "hc-disabled-user", disable_health_check: true }); + await server.waitFor((reqs) => reqs.some((r) => r.params.method === "sc" && r.params.device_id === "hc-disabled-user"), 5000); + await sleep(300); + + state.body = { c: { networking: false } }; + const offline = makeTempDir("hc-networking-off"); + seed(offline, "cly_sbs", { networking: false }); + startSdk(server, offline, { device_id: "hc-offline-user" }); + await server.waitFor((reqs) => reqs.some((r) => r.params.method === "sc" && r.params.device_id === "hc-offline-user"), 5000); + await sleep(300); + + state.body = { c: {} }; + startSdk(server, makeTempDir("hc-test-mode"), { device_id: "hc-test-mode-user", test_mode: true }); + await sleep(300); + + startSdk(server, makeTempDir("hc-no-updates"), { device_id: "hc-no-updates-user", disable_sdk_behavior_settings_updates: true }); + const sent = await server.waitFor((reqs) => reqs.some((r) => isHealthCheck(r) && r.params.device_id === "hc-no-updates-user"), 5000); + await server.close(); + assert.equal(fromDevice("hc-disabled-user").filter(isHealthCheck).length, 0, "the health check was sent although it is turned off"); + assert.equal(fromDevice("hc-offline-user").filter(isHealthCheck).length, 0, "the health check was sent while networking was off"); + assert.equal(fromDevice("hc-test-mode-user").length, 0, "a request was sent in test mode"); + assert.ok(sent, "turning off settings updates also turned off the health check"); + assert.equal(fromDevice("hc-no-updates-user").filter((r) => r.params.method === "sc").length, 0, "the settings were fetched although updates are turned off"); + }); + + it("5- counts errors and warnings while logging is off, including warnings logged during init", async() => { + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("hc-logs"), { device_id: "hc-log-user", request_timeout: "soon" }); + Countly.end_event("never_started"); + Countly.change_id(""); + await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + await server.close(); + const check = JSON.parse(healthChecks(server)[0].params.hc); + assert.deepStrictEqual([check.el, check.wl], [1, 2]); + }); + + it("6- is still sent when the settings cannot be fetched, and counts the failed fetch", async() => { + const server = await settingsServer({ status: 500, body: { result: "Internal server error" } }); + startSdk(server, makeTempDir("hc-no-settings"), { device_id: "hc-no-settings-user" }); + const sent = await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + await server.close(); + assert.ok(sent, "the health check was not sent after the failed settings fetch"); + assert.deepStrictEqual(JSON.parse(healthChecks(server)[0].params.hc), { ...cleared, el: 1, wl: 1 }); + }); + + it("7- with a salt, the settings request and the health check carry a checksum that 26.01 servers accept", async() => { + const salt = "hc-salt"; + const server = await settingsServer({ body: { c: {} } }); + startSdk(server, makeTempDir("hc-salt"), { device_id: "hc-salt-user", salt: salt }); + await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + await server.close(); + const check = healthChecks(server)[0]; + assert.ok(passesServerChecksum(settingsRequests(server)[0], salt), "the settings request checksum does not match"); + assert.ok(passesServerChecksum(check, salt), "the health check checksum does not match"); + assert.equal(passesServerChecksum(check, "another-salt"), false); + }); + + it("8- the stored counters survive a damaged value and start from zero for the damaged part", async() => { + const server = await settingsServer({ body: { c: {} } }); + const dir = makeTempDir("hc-damaged"); + seed(dir, "cly_hc", { el: "many", wl: 4, sc: null, em: 7, bom: 2, cbom: 1 }); + startSdk(server, dir, { device_id: "hc-damaged-user" }); + await server.waitFor((reqs) => reqs.some(isHealthCheck), 5000); + const reset = await storedValue("cly_hc", cleared); + await server.close(); + assert.deepStrictEqual(JSON.parse(healthChecks(server)[0].params.hc), { el: 0, wl: 4, sc: -1, em: "", bom: 2, cbom: 1 }); + assert.ok(reset, "the damaged counters were not replaced after the server confirmed the health check"); + }); + + it("9- counters that change shortly before the process exits are still stored", async() => { + const dir = makeTempDir("hc-exit"); + const run = await runScript("health_on_exit.js", { STORAGE_DIR: dir }, 20000); + assert.equal(run.code, 0, run.stdout + run.stderr); + assert.deepStrictEqual(readStoredFile(path.join(dir, "__cly_hc.json"), "cly_hc"), { ...cleared, wl: 2 }); + }); +}); diff --git a/test/tests_internal_limits.js b/test/tests_internal_limits.js index 82f0a10..17406cd 100644 --- a/test/tests_internal_limits.js +++ b/test/tests_internal_limits.js @@ -81,9 +81,9 @@ describe("Testing internal limits", () => { assert.ok(!event.segmentation["key of 4"]); assert.equal(event.segmentation["key of 3"], "Value of"); // common parameter validation - assert.ok(event.timestamp !== 'undefined'); - assert.ok(event.hour !== 'undefined'); - assert.ok(event.dow !== 'undefined'); + assert.ok(typeof event.timestamp !== 'undefined'); + assert.ok(event.hour > -1 && event.hour < 24); + assert.ok(event.dow > -1 && event.dow < 7); done(); }, hp.sWait); }); @@ -97,14 +97,14 @@ describe("Testing internal limits", () => { setTimeout(() => { // read event queue var event = hp.readEventQueue()[0]; - assert.equal(event.key, "[CLY]_vi"); + assert.equal(event.key, "[CLY]_view"); assert.equal(event.segmentation.name, "a very l"); assert.equal(event.segmentation.visit, 1); assert.ok(event.segmentation.segment); // common parameter validation - assert.ok(event.timestamp !== 'undefined'); - assert.ok(event.hour !== 'undefined'); - assert.ok(event.dow !== 'undefined'); + assert.ok(typeof event.timestamp !== 'undefined'); + assert.ok(event.hour > -1 && event.hour < 24); + assert.ok(event.dow > -1 && event.dow < 7); done(); }, hp.sWait); }); @@ -134,9 +134,9 @@ describe("Testing internal limits", () => { assert.ok(req.sdk_name); assert.ok(req.sdk_version); // common parameter validation - assert.ok(req.timestamp !== 'undefined'); - assert.ok(req.hour !== 'undefined'); - assert.ok(req.dow !== 'undefined'); + assert.ok(typeof req.timestamp !== 'undefined'); + assert.ok(req.hour > -1 && req.hour < 24); + assert.ok(req.dow > -1 && req.dow < 7); var crash = JSON.parse(req.crash); assert.equal(crash._logs, "log5 too\nlog6\nlog7"); assert.ok(crash._os); @@ -183,9 +183,9 @@ describe("Testing internal limits", () => { assert.ok(req.sdk_name); assert.ok(req.sdk_version); // common parameter validation - assert.ok(req.timestamp !== 'undefined'); - assert.ok(req.hour !== 'undefined'); - assert.ok(req.dow !== 'undefined'); + assert.ok(typeof req.timestamp !== 'undefined'); + assert.ok(req.hour > -1 && req.hour < 24); + assert.ok(req.dow > -1 && req.dow < 7); var details = JSON.parse(req.user_details); assert.equal(details.name, 'Gottlob '); assert.equal(details.username, 'Grundges'); @@ -227,9 +227,9 @@ describe("Testing internal limits", () => { assert.ok(req.sdk_name); assert.ok(req.sdk_version); // common parameter validation - assert.ok(req.timestamp !== 'undefined'); - assert.ok(req.hour !== 'undefined'); - assert.ok(req.dow !== 'undefined'); + assert.ok(typeof req.timestamp !== 'undefined'); + assert.ok(req.hour > -1 && req.hour < 24); + assert.ok(req.dow > -1 && req.dow < 7); var details = JSON.parse(req.user_details).custom; // set assert.equal(details['name of '], 'Bertrand'); diff --git a/test/tests_package.js b/test/tests_package.js new file mode 100644 index 0000000..323f682 --- /dev/null +++ b/test/tests_package.js @@ -0,0 +1,16 @@ +const assert = require("assert"); +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const sdkRoot = path.resolve(__dirname, ".."); + +describe("Package tests", () => { + it("1- the npm package contains the SDK code and its documents, and no tests or repository files", () => { + const output = execSync("npm pack --dry-run --json --ignore-scripts", { cwd: sdkRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + const packed = JSON.parse(output)[0].files.map((file) => file.path).sort(); + const libFiles = fs.readdirSync(path.join(sdkRoot, "lib")).filter((name) => name.endsWith(".js")).map((name) => `lib/${name}`); + const expected = ["CHANGELOG.md", "LICENSE", "README.md", "index.js", "package.json"].concat(libFiles).sort(); + assert.deepStrictEqual(packed, expected); + }); +}); diff --git a/test/tests_request_queue.js b/test/tests_request_queue.js new file mode 100644 index 0000000..0480916 --- /dev/null +++ b/test/tests_request_queue.js @@ -0,0 +1,175 @@ +/* eslint-disable no-console */ +/* global runthis */ +var assert = require("assert"); +var Countly = require("../lib/countly"); +var hp = require("./helpers/helper_functions"); + +// init function +function initMain(extraConfig) { + var config = { + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + interval: 10000, + max_events: -1, + }; + for (var key in extraConfig) { + config[key] = extraConfig[key]; + } + Countly.init(config); +} + +describe("Request queue tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + it("Requests are queued in recording order", (done) => { + // initialize SDK + initMain(); + // record different features back to back + Countly.begin_session(); + Countly.user_details({ name: "orderCheck" }); + try { + runthis(); + } + catch (ex) { + Countly.log_error(ex); + } + Countly.report_trace({ + type: "device", name: "orderTrace", stz: 1234567890123, etz: 1234567890321, apm_metrics: { duration: 198 }, + }); + // read request queue + setTimeout(() => { + var rq = hp.readRequestQueue(); + assert.equal(rq.length, 4); + // queue must hold begin_session, user_details, crash and apm requests in call order + hp.sessionRequestValidator(rq[0]); + assert.equal(JSON.parse(rq[1].user_details).name, "orderCheck"); + hp.crashRequestValidator(rq[2], true); + assert.ok(rq[3].apm); + for (var i = 0; i < rq.length; i++) { + hp.requestBaseParamValidator(rq[i]); + } + done(); + }, hp.sWait); + }); + it("Events are batched into requests in recording order", (done) => { + // initialize SDK with a small batch size and a short processing interval + initMain({ max_events: 2, interval: 500 }); + for (var i = 1; i < 6; i++) { + Countly.add_event({ key: `batch_${i}`, count: 1 }); + } + // wait for the heartbeat to move all events into the request queue + setTimeout(() => { + // event queue must be fully drained + assert.equal(hp.readEventQueue().length, 0); + var rq = hp.readRequestQueue(); + assert.equal(rq.length, 3); + var batchSizes = []; + var recordedKeys = []; + for (var j = 0; j < rq.length; j++) { + hp.requestBaseParamValidator(rq[j]); + var batch = JSON.parse(rq[j].events); + batchSizes.push(batch.length); + for (var k = 0; k < batch.length; k++) { + recordedKeys.push(batch[k].key); + assert.ok(typeof batch[k].timestamp !== 'undefined'); + assert.ok(batch[k].hour > -1 && batch[k].hour < 24); + assert.ok(batch[k].dow > -1 && batch[k].dow < 7); + } + } + // batches must respect max_events and keep the original recording order + assert.deepStrictEqual(batchSizes, [2, 2, 1]); + assert.deepStrictEqual(recordedKeys, ["batch_1", "batch_2", "batch_3", "batch_4", "batch_5"]); + done(); + }, hp.mWait); + }); + it("Overflown request queue drops oldest requests first", (done) => { + // initialize SDK with a tiny request queue + initMain({ queue_size: 3 }); + for (var i = 1; i < 6; i++) { + Countly.user_details({ name: `user_${i}` }); + } + // read request queue + setTimeout(() => { + var rq = hp.readRequestQueue(); + // current implementation lets the queue grow to queue_size + 1 before evicting the oldest request + assert.equal(rq.length, 4); + var names = []; + for (var j = 0; j < rq.length; j++) { + hp.requestBaseParamValidator(rq[j]); + names.push(JSON.parse(rq[j].user_details).name); + } + assert.deepStrictEqual(names, ["user_2", "user_3", "user_4", "user_5"]); + done(); + }, hp.sWait); + }); + it("APM trace request content is preserved", (done) => { + // initialize SDK + initMain(); + Countly.report_trace({ + type: "device", name: "forLoopProfiling", stz: 1234567890123, etz: 1234567890321, apm_metrics: { duration: 198 }, + }); + // read request queue + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + hp.requestBaseParamValidator(req); + var apm = JSON.parse(req.apm); + assert.equal(apm.type, "device"); + assert.equal(apm.name, "forLoopProfiling"); + assert.equal(apm.stz, 1234567890123); + assert.equal(apm.etz, 1234567890321); + assert.equal(apm.apm_metrics.duration, 198); + // trace timestamp must be the provided start time + assert.equal(apm.timestamp, apm.stz); + assert.ok(apm.hour > -1 && apm.hour < 24); + assert.ok(apm.dow > -1 && apm.dow < 7); + done(); + }, hp.sWait); + }); + it("Request queue persists order across halt and re-init", (done) => { + // initialize SDK + initMain(); + Countly.begin_session(); + setTimeout(() => { + assert.equal(hp.readRequestQueue().length, 1); + // simulate app restart without wiping the storage + Countly.halt(true); + initMain(); + Countly.user_details({ name: "afterRestart" }); + setTimeout(() => { + var rq = hp.readRequestQueue(); + assert.equal(rq.length, 2); + // stored request must survive the restart and stay in front of the new one + hp.sessionRequestValidator(rq[0]); + assert.equal(JSON.parse(rq[1].user_details).name, "afterRestart"); + // both requests must belong to the same stored device ID + assert.equal(rq[0].device_id, rq[1].device_id); + done(); + }, hp.sWait); + }, hp.sWait); + }); + it("Raw requests keep their values whole and follow the queue size limit", (done) => { + initMain({ queue_size: 3 }); + var events = []; + for (var i = 0; i < 10; i++) { + events.push({ key: `purchase_${i}`, count: 1, segmentation: { item: `sku-${i}`, store: "main street" } }); + } + var eventsJson = JSON.stringify(events); + Countly.request({ app_key: "YOUR_APP_KEY", device_id: "raw-user", events: eventsJson }); + setTimeout(() => { + var rq = hp.readRequestQueue(); + assert.equal(rq.length, 1); + // the raw value must reach the queue exactly as given, however long it is + assert.equal(rq[0].events, eventsJson); + assert.equal(JSON.parse(rq[0].events).length, 10); + for (var j = 0; j < 5; j++) { + Countly.request({ app_key: "YOUR_APP_KEY", device_id: `raw-user-${j}`, begin_session: 1 }); + } + var limited = hp.readRequestQueue(); + // same limit as for the SDK's own requests: the queue keeps at most queue_size + 1 requests + assert.equal(limited.length, 4); + assert.equal(limited[limited.length - 1].device_id, "raw-user-4"); + done(); + }, hp.sWait); + }); +}); diff --git a/test/tests_salt.js b/test/tests_salt.js index 5a7db3a..9a6c78b 100644 --- a/test/tests_salt.js +++ b/test/tests_salt.js @@ -180,6 +180,8 @@ describe("Salt integration tests", () => { app_key: "YOUR_APP_KEY", url: serverInfo.url, interval: 10, + disable_sdk_behavior_settings_updates: true, + disable_health_check: true, }); Countly.begin_session(true); @@ -204,6 +206,8 @@ describe("Salt integration tests", () => { app_key: "YOUR_APP_KEY", url: serverInfo.url, interval: 10, + disable_sdk_behavior_settings_updates: true, + disable_health_check: true, salt: salt, }); @@ -240,6 +244,8 @@ describe("Salt integration tests", () => { app_key: "YOUR_APP_KEY", url: serverInfo.url, interval: 10, + disable_sdk_behavior_settings_updates: true, + disable_health_check: true, salt: salt, }); diff --git a/test/tests_storage.js b/test/tests_storage.js index 2e3c85e..b797d90 100644 --- a/test/tests_storage.js +++ b/test/tests_storage.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ /* global runthis */ const assert = require("assert"); +const path = require("path"); var Countly = require("../lib/countly"); var storage = require("../lib/countly-storage"); var hp = require("./helpers/helper_functions"); @@ -8,6 +9,10 @@ var testUtils = require("./helpers/test_utils"); var appKey = "YOUR_APP_KEY"; var serverUrl = "https://your.server.ly"; +// relative storage paths are resolved against the working directory, which is the SDK root when tests run +var customPath = "test/customStorageDirectory/"; +var defaultFolder = path.resolve(".countly-storage"); +var customFolder = path.resolve(customPath); const { StorageTypes } = Countly; @@ -31,11 +36,11 @@ function validateStorageMethods() { function validateStorageTypeAndPath(expectedStorageType, isCustomPath = false) { if (expectedStorageType === StorageTypes.FILE) { if (!isCustomPath) { - assert.equal(storage.getStoragePath(), "../data/"); + assert.equal(storage.getStoragePath(), defaultFolder); assert.equal(storage.getStorageType(), StorageTypes.FILE); } else { - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(storage.getStoragePath(), customFolder); assert.equal(storage.getStorageType(), StorageTypes.FILE); } } @@ -46,11 +51,11 @@ function validateStorageTypeAndPath(expectedStorageType, isCustomPath = false) { // get storage type returns null in case of a custom method else if (expectedStorageType === null) { if (!isCustomPath) { - assert.equal(storage.getStoragePath(), "../data/"); + assert.equal(storage.getStoragePath(), defaultFolder); assert.equal(storage.getStorageType(), null); } else { - assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(storage.getStoragePath(), customFolder); assert.equal(storage.getStorageType(), null); } } @@ -132,7 +137,7 @@ describe("Storage Tests", () => { }); // if no config option provided sdk should init storage with default settings - // "../data/" as the storage path, FILE as the storage type + // ".countly-storage" in the working directory as the storage path, FILE as the storage type it("1- noConfigOption", (done) => { Countly.init({ app_key: appKey, @@ -155,7 +160,7 @@ describe("Storage Tests", () => { Countly.init({ app_key: appKey, url: serverUrl, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, storage_type: StorageTypes.FILE, }); shouldFilesExist(true); @@ -171,7 +176,7 @@ describe("Storage Tests", () => { }); // if invalid path is provided such as null or undefined sdk should init storage with default path - // validateStorageTypeAndPath checks path as "../data/" if true is not passed as second param + // validateStorageTypeAndPath checks path as the default folder if true is not passed as second param it("3- file_invalid_path", (done) => { Countly.init({ app_key: appKey, @@ -239,7 +244,7 @@ describe("Storage Tests", () => { app_key: appKey, url: serverUrl, storage_type: StorageTypes.MEMORY, - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, }); shouldFilesExist(false); validateStorageTypeAndPath(StorageTypes.MEMORY); @@ -302,17 +307,17 @@ describe("Storage Tests", () => { app_key: appKey, url: serverUrl, custom_storage_method: testUtils.getCustomStorage(), - storage_path: "../test/customStorageDirectory/", + storage_path: customPath, }); shouldFilesExist(true); // storage type will be null since custom method is provided // path will be the custom path in this case - validateStorageTypeAndPath(null, "../test/customStorageDirectory/"); + validateStorageTypeAndPath(null, customPath); createData(); setTimeout(() => { shouldFilesExist(true); - validateStorageTypeAndPath(null, "../test/customStorageDirectory/"); + validateStorageTypeAndPath(null, customPath); validateData(false, true); done(); }, hp.mWait); @@ -387,7 +392,7 @@ describe("Storage Tests", () => { // initStorage method with File storage type and custom path // storage should initialize correctly with custom path it("14- initStorage_file_cPath_noInit", (done) => { - storage.initStorage("../test/customStorageDirectory/", StorageTypes.FILE); + storage.initStorage(customPath, StorageTypes.FILE); shouldFilesExist(true); // storage type will be File since it's default validateStorageTypeAndPath(StorageTypes.FILE, true); @@ -416,7 +421,7 @@ describe("Storage Tests", () => { // initStorage method with custom storage method and custom path it("17- initStorage_custom_cPath_noInit", (done) => { - storage.initStorage("../test/customStorageDirectory/", null, false, testUtils.getCustomStorage()); + storage.initStorage(customPath, null, false, testUtils.getCustomStorage()); shouldFilesExist(true); validateStorageTypeAndPath(null, true); validateStorageMethods(); diff --git a/test/tests_storage_safety.js b/test/tests_storage_safety.js new file mode 100644 index 0000000..6fe71ff --- /dev/null +++ b/test/tests_storage_safety.js @@ -0,0 +1,355 @@ +/* eslint-disable no-console */ +const assert = require("assert"); +const cluster = require("cluster"); +const fs = require("fs"); +const path = require("path"); +const Countly = require("../lib/countly"); +const CountlyBulk = require("../lib/countly-bulk"); +const CountlyStorage = require("../lib/countly-storage"); +const hp = require("./helpers/helper_functions"); +const { startMockServer, receivedEvents } = require("./helpers/mock_server"); +const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); + +const { StorageTypes } = Countly; +const sdkRoot = path.resolve(__dirname, ".."); + +// every CountlyBulk instance registers a cluster "fork" listener and these tests create several instances +cluster.setMaxListeners(60); +const oldDefaultDir = path.join(sdkRoot, "data"); + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** + * Waits until a condition is true, or until the time is up + * @param {Function} condition - returns true when the wait is over + * @param {Number} timeoutMs - longest time to wait + * @returns {Promise} resolves when the condition is true or the time is up + */ +function waitUntil(condition, timeoutMs) { + return new Promise((resolve) => { + const started = Date.now(); + const check = () => { + if (condition() || Date.now() - started > timeoutMs) { + resolve(); + return; + } + setTimeout(check, 20); + }; + check(); + }); +} + +function lastLine(output) { + const lines = output.trim().split("\n"); + return JSON.parse(lines[lines.length - 1]); +} + +function bulkInnerRequests(requests) { + const inner = []; + requests.filter((r) => r.path === "/i/bulk").forEach((r) => { + JSON.parse(r.params.requests).forEach((request) => { + inner.push(request); + }); + }); + return inner; +} + +function deadPid() { + for (let pid = 900000; ; pid++) { + try { + process.kill(pid, 0); + } + catch (ex) { + if (ex.code === "ESRCH") { + return pid; + } + } + } +} + +describe("Storage safety tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + after(() => { + Countly.halt(true); + removeTempDirs(); + }); + afterEach(() => { + fs.rmSync(oldDefaultDir, { recursive: true, force: true }); + fs.rmSync(path.join(sdkRoot, "lib", "relative_store"), { recursive: true, force: true }); + ["cly_id", "cly_id_type", "cly_queue", "cly_event"].forEach((key) => { + fs.rmSync(path.join(sdkRoot, "lib", `undefined__${key}.json`), { force: true }); + }); + }); + + it("1- exiting right after an SDK call keeps the stored queue", async() => { + const dir = makeTempDir("exit"); + const run = await runScript("exit_after_call.js", { STORAGE_DIR: dir }, 60000); + assert.equal(run.timedOut, false, run.stderr); + const queue = readStoredFile(path.join(dir, "__cly_queue.json"), "cly_queue"); + assert.ok(Array.isArray(queue), "the stored queue file was left empty or unreadable"); + assert.equal(queue.length, 301); + assert.equal(JSON.parse(queue[300].user_details).name, "last before exit"); + assert.deepStrictEqual(fs.readdirSync(dir).filter((name) => name.endsWith(".tmp")), [], "temporary files were left behind"); + }); + + it("2- stores data in .countly-storage inside the working folder by default", async() => { + const cwd = makeTempDir("default-location"); + const run = await runScript("storage_location.js", { DEVICE_ID: "default-user" }, 20000, cwd); + const output = lastLine(run.stdout); + assert.equal(output.storagePath, path.join(cwd, ".countly-storage")); + assert.equal(readStoredFile(path.join(cwd, ".countly-storage", "__cly_id.json"), "cly_id"), "default-user"); + assert.equal(fs.existsSync(path.join(oldDefaultDir, "__cly_id.json")), false); + }); + + it("3- resolves a relative storage_path against the working folder", async() => { + const cwd = makeTempDir("relative-location"); + await runScript("storage_location.js", { DEVICE_ID: "relative-user", STORAGE_PATH: "relative_store/" }, 20000, cwd); + assert.equal(readStoredFile(path.join(cwd, "relative_store", "__cly_id.json"), "cly_id"), "relative-user"); + }); + + it("4- keeps files inside a storage_path given without a trailing slash", async() => { + const cwd = makeTempDir("no-slash"); + const folder = path.join(cwd, "abs_store"); + await runScript("storage_location.js", { DEVICE_ID: "folder-user", STORAGE_PATH: folder }, 20000, cwd); + assert.equal(readStoredFile(path.join(folder, "__cly_id.json"), "cly_id"), "folder-user"); + assert.equal(fs.existsSync(`${folder}__cly_id.json`), false); + }); + + it("5- moves data from the old default location, so the device ID survives the upgrade", async() => { + fs.mkdirSync(oldDefaultDir, { recursive: true }); + fs.writeFileSync(path.join(oldDefaultDir, "__cly_id.json"), JSON.stringify({ cly_id: "legacy-device" })); + fs.writeFileSync(path.join(oldDefaultDir, "__cly_id_type.json"), JSON.stringify({ cly_id_type: 1 })); + const cwd = makeTempDir("migration"); + const run = await runScript("storage_location.js", {}, 20000, cwd); + assert.equal(lastLine(run.stdout).deviceId, "legacy-device"); + assert.equal(readStoredFile(path.join(cwd, ".countly-storage", "__cly_id.json"), "cly_id"), "legacy-device"); + assert.equal(readStoredFile(path.join(cwd, ".countly-storage", "__cly_id_type.json"), "cly_id_type"), 1); + assert.equal(fs.existsSync(path.join(oldDefaultDir, "__cly_id.json")), false); + assert.equal(fs.existsSync(path.join(oldDefaultDir, "__cly_id.json.migrated")), true); + }); + + it("6- moves files that older versions wrote next to a folder given without a trailing slash", async() => { + const cwd = makeTempDir("sibling-migration"); + const folder = path.join(cwd, "abs_store"); + fs.writeFileSync(`${folder}__cly_id.json`, JSON.stringify({ cly_id: "sibling-device" })); + const run = await runScript("storage_location.js", { STORAGE_PATH: folder }, 20000, cwd); + assert.equal(lastLine(run.stdout).deviceId, "sibling-device"); + assert.equal(readStoredFile(path.join(folder, "__cly_id.json"), "cly_id"), "sibling-device"); + }); + + it("7- creating a bulk instance does not change where the SDK stores its own data", async() => { + const dir = makeTempDir("core-bulk"); + Countly.init({ app_key: "YOUR_APP_KEY", url: "http://127.0.0.1:1", storage_path: `${dir}${path.sep}`, test_mode: true, device_id: "core-user" }); + const bulk = new Countly.Bulk({ app_key: "BULK_APP", url: "http://127.0.0.1:1", storage_type: StorageTypes.MEMORY }); + assert.equal(CountlyStorage.getStorageType(), StorageTypes.FILE); + assert.equal(CountlyStorage.getStoragePath(), dir); + assert.equal(bulk._getStorage().getStorageType(), StorageTypes.MEMORY); + Countly.add_event({ key: "after_bulk" }); + await sleep(300); + const stored = readStoredFile(path.join(dir, "__cly_event.json"), "cly_event"); + assert.ok(Array.isArray(stored), "the SDK stopped writing its event file"); + assert.deepStrictEqual(stored.map((e) => e.key), ["after_bulk"]); + }); + + it("8- two bulk instances keep their apps' data apart", async() => { + const server = await startMockServer(); + const bulkA = new CountlyBulk({ app_key: "APP_A", url: server.url, interval: 100 }); + bulkA.add_user({ device_id: "userA" }).add_event({ key: "a_event" }); + const bulkB = new CountlyBulk({ app_key: "APP_B", url: server.url, interval: 100 }); + bulkB.add_user({ device_id: "userB" }).add_event({ key: "b_event" }); + bulkA.start(); + bulkB.start(); + await server.waitFor((reqs) => bulkInnerRequests(reqs).length >= 2, 5000); + await sleep(500); + bulkA.stop(); + bulkB.stop(); + await server.close(); + const inner = bulkInnerRequests(server.requests); + assert.equal(inner.length, 2); + const byUser = {}; + inner.forEach((request) => { + byUser[request.device_id] = request; + }); + assert.equal(byUser.userA.app_key, "APP_A"); + assert.deepStrictEqual(byUser.userA.events.map((e) => e.key), ["a_event"]); + assert.equal(byUser.userB.app_key, "APP_B"); + assert.deepStrictEqual(byUser.userB.events.map((e) => e.key), ["b_event"]); + }); + + it("9- memory storage never writes files, also when a fatal crash forces a save", () => { + CountlyStorage.initStorage(null, StorageTypes.MEMORY); + CountlyStorage.storeSet("cly_queue", [{ device_id: "private-user" }]); + CountlyStorage.forceStore(); + assert.equal(fs.existsSync(path.join(sdkRoot, "lib", "undefined__cly_queue.json")), false); + CountlyStorage.resetStorage(); + }); + + it("10- removes temporary files that ended processes left behind, and keeps those of running processes", () => { + const dir = makeTempDir("stale-tmp"); + const stale = `__cly_queue.json.${deadPid()}.tmp`; + const running = `__cly_queue.json.${process.ppid}.tmp`; + fs.writeFileSync(path.join(dir, stale), "[]"); + fs.writeFileSync(path.join(dir, running), "[]"); + CountlyStorage.initStorage(`${dir}${path.sep}`, StorageTypes.FILE); + assert.equal(fs.existsSync(path.join(dir, stale)), false); + assert.equal(fs.existsSync(path.join(dir, running)), true); + CountlyStorage.resetStorage(); + }); + + it("11- warns about a shared storage folder only for instances whose files would collide", () => { + const dir = makeTempDir("shared"); + const warnings = []; + const originalWarn = console.warn; + console.warn = (...args) => { + warnings.push(args.join(" ")); + }; + try { + Countly.init({ app_key: "YOUR_APP_KEY", url: "http://127.0.0.1:1", storage_path: `${dir}${path.sep}`, test_mode: true, device_id: "shared-core" }); + const first = new CountlyBulk({ app_key: "SHARED_APP", url: "http://127.0.0.1:1", storage_path: `${dir}${path.sep}`, storage_type: StorageTypes.FILE }); + assert.equal(warnings.filter((w) => w.indexOf("overwrite") !== -1).length, 0, "the SDK and a bulk instance use different files"); + const second = new CountlyBulk({ app_key: "SHARED_APP", url: "http://127.0.0.1:1", storage_path: `${dir}${path.sep}`, storage_type: StorageTypes.FILE }); + assert.ok(first && second); + assert.equal(warnings.filter((w) => w.indexOf("overwrite") !== -1).length, 1); + } + finally { + console.warn = originalWarn; + } + }); + + it("12- keeps the data in the old location when the new storage folder cannot be created, so it survives a restart", async() => { + const cwd = makeTempDir("blocked-default"); + fs.writeFileSync(path.join(cwd, ".countly-storage"), "a file where the storage folder would go"); + const first = await runScript("storage_location.js", { DEVICE_ID: "fallback-user" }, 20000, cwd); + assert.equal(lastLine(first.stdout).storagePath, oldDefaultDir); + assert.equal(readStoredFile(path.join(oldDefaultDir, "__cly_id.json"), "cly_id"), "fallback-user"); + const second = await runScript("storage_location.js", {}, 20000, cwd); + assert.equal(lastLine(second.stdout).deviceId, "fallback-user"); + }); + + it("13- warns when no storage folder can be written, and still sends the data", async() => { + const blocker = path.join(makeTempDir("blocked-everywhere"), "blocker"); + fs.writeFileSync(blocker, "a file where a folder would go"); + const server = await startMockServer(); + const warnings = []; + const originalWarn = console.warn; + console.warn = (...args) => { + warnings.push(args.join(" ")); + }; + try { + Countly.halt(false); + Countly.init({ app_key: "YOUR_APP_KEY", url: server.url, storage_path: path.join(blocker, "store"), device_id: "no-disk-user", interval: 100 }); + } + finally { + console.warn = originalWarn; + } + Countly.add_event({ key: "kept_in_memory" }); + const delivered = await Countly.flush(5); + Countly.halt(true); + await server.close(); + assert.ok(warnings.some((w) => w.indexOf("not writable") !== -1), "no warning about the storage folder was printed"); + assert.strictEqual(delivered, true); + assert.ok(receivedEvents(server.requests).some((e) => e.key === "kept_in_memory" && e.device_id === "no-disk-user")); + }); + + it("14- retries a storage file that is briefly locked, for example by an antivirus, and still replaces it in one step", async() => { + const dir = makeTempDir("locked-briefly"); + const target = path.join(dir, "__cly_id.json"); + const storage = CountlyStorage.createStorage(); + storage.initStorage(`${dir}${path.sep}`, StorageTypes.FILE); + const originalRename = fs.renameSync; + let attempts = 0; + fs.renameSync = (from, to) => { + if (to === target) { + attempts++; + if (attempts <= 2) { + throw Object.assign(new Error("EPERM: operation not permitted, rename"), { code: "EPERM" }); + } + } + return originalRename(from, to); + }; + let error; + try { + error = await new Promise((resolve) => { + storage.storeSet("cly_id", "locked-user", resolve); + }); + } + finally { + fs.renameSync = originalRename; + } + storage.resetStorage(); + assert.equal(error, null); + assert.equal(attempts, 3, "the locked file was not retried until the rename succeeded"); + assert.equal(readStoredFile(target, "cly_id"), "locked-user"); + assert.deepStrictEqual(fs.readdirSync(dir).filter((name) => name.endsWith(".tmp")), []); + }); + + it("15- still saves a storage file that stays locked, by writing it directly", async() => { + const dir = makeTempDir("locked-for-good"); + const target = path.join(dir, "__cly_id.json"); + const storage = CountlyStorage.createStorage(); + storage.initStorage(`${dir}${path.sep}`, StorageTypes.FILE); + const originalRename = fs.renameSync; + let attempts = 0; + fs.renameSync = (from, to) => { + if (to === target) { + attempts++; + throw Object.assign(new Error("EBUSY: resource busy or locked, rename"), { code: "EBUSY" }); + } + return originalRename(from, to); + }; + let error; + try { + error = await new Promise((resolve) => { + storage.storeSet("cly_id", "stuck-user", resolve); + }); + } + finally { + fs.renameSync = originalRename; + } + storage.resetStorage(); + assert.equal(error, null); + assert.equal(attempts, 4); + assert.equal(readStoredFile(target, "cly_id"), "stuck-user"); + await waitUntil(() => !fs.readdirSync(dir).some((name) => name.endsWith(".tmp")), 1000); + assert.deepStrictEqual(fs.readdirSync(dir).filter((name) => name.endsWith(".tmp")), []); + }); + + it("16- keeps using the old files when moving them to the new folder fails midway, so nothing is lost", () => { + const cwd = makeTempDir("interrupted-migration"); + const folder = path.join(cwd, "abs_store"); + fs.writeFileSync(`${folder}__cly_id.json`, JSON.stringify({ cly_id: "legacy-user" })); + fs.writeFileSync(`${folder}__cly_queue.json`, JSON.stringify({ cly_queue: [{ device_id: "legacy-user", begin_session: 1 }] })); + const originalCopy = fs.copyFileSync; + let copies = 0; + fs.copyFileSync = (from, to) => { + copies++; + if (copies === 2) { + throw Object.assign(new Error("ENOSPC: no space left on device, copyfile"), { code: "ENOSPC" }); + } + return originalCopy(from, to); + }; + const storage = CountlyStorage.createStorage(); + try { + storage.initStorage(folder, StorageTypes.FILE); + } + finally { + fs.copyFileSync = originalCopy; + } + const storagePath = storage.getStoragePath(); + const deviceId = storage.storeGet("cly_id"); + const queue = storage.storeGet("cly_queue"); + storage.resetStorage(); + assert.equal(copies, 2); + assert.equal(storagePath, cwd); + assert.equal(deviceId, "legacy-user"); + assert.equal(queue.length, 1); + assert.deepStrictEqual(fs.readdirSync(folder), [], "files copied before the failure were left in the new folder"); + assert.equal(fs.existsSync(`${folder}__cly_id.json.migrated`), false); + }); +}); diff --git a/test/tests_user_switch.js b/test/tests_user_switch.js new file mode 100644 index 0000000..58c6af1 --- /dev/null +++ b/test/tests_user_switch.js @@ -0,0 +1,216 @@ +/* global runthis */ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const Countly = require("../lib/countly"); +const hp = require("./helpers/helper_functions"); +const { startMockServer } = require("./helpers/mock_server"); +const { makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function initMain(config) { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + interval: 10000, + max_events: -1, + storage_path: `${makeTempDir("switch")}${path.sep}`, + ...config, + }); +} + +function queuedEvents() { + const events = []; + hp.readRequestQueue().forEach((request) => { + if (request.events) { + JSON.parse(request.events).forEach((event) => { + event.device_id = request.device_id; + events.push(event); + }); + } + }); + return events; +} + +describe("User switch and consent tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + afterEach(() => { + Countly.halt(true); + }); + after(() => { + removeTempDirs(); + }); + + it("1- removing consent tells the server", async() => { + initMain({ require_consent: true, device_id: "consent-user" }); + Countly.add_consent(["events", "users"]); + await sleep(1200); + Countly.remove_consent("events"); + await sleep(1200); + const consents = hp.readRequestQueue().filter((r) => r.consent).map((r) => JSON.parse(r.consent)); + assert.equal(consents.length, 2); + assert.equal(consents[0].events, true); + assert.equal(consents[1].events, false); + assert.equal(consents[1].users, true); + }); + + it("2- removing consent for a feature group clears the group as well", () => { + initMain({ require_consent: true, device_id: "group-user" }); + Countly.group_features({ activity: ["sessions", "events"] }); + Countly.add_consent("activity"); + assert.equal(Countly.check_consent("activity"), true); + Countly.remove_consent("activity"); + assert.equal(Countly.check_consent("activity"), false); + assert.equal(Countly.check_consent("events"), false); + }); + + it("3- change_id ignores empty IDs and keeps tracking under the current one", () => { + initMain({ device_id: "alice" }); + Countly.change_id(undefined); + Countly.change_id(null); + Countly.change_id(""); + Countly.change_id(NaN); + Countly.change_id(Infinity); + assert.equal(Countly.get_device_id(), "alice"); + Countly.user_details({ name: "still alice" }); + assert.equal(hp.readRequestQueue()[0].device_id, "alice"); + Countly.change_id(12345); + assert.equal(Countly.get_device_id(), "12345"); + }); + + it("4- an empty device ID stored by an older version is ignored at start", () => { + const dir = makeTempDir("empty-id"); + fs.writeFileSync(path.join(dir, "__cly_id.json"), JSON.stringify({ cly_id: "" })); + initMain({ device_id: "carol", storage_path: `${dir}${path.sep}` }); + assert.equal(Countly.get_device_id(), "carol"); + }); + + it("5- switching users without merge keeps the previous user's view, profile changes and breadcrumbs with that user", async() => { + initMain({ device_id: "alice" }); + Countly.begin_session(); + Countly.track_view("Alice invoices"); + Countly.userData.set("plan", "pro"); + Countly.add_log("alice opened invoice 4711"); + await sleep(100); + Countly.change_id("bob", false); + try { + runthis(); + } + catch (ex) { + Countly.log_error(ex); + } + await sleep(100); + + const durationEvents = queuedEvents().filter((e) => e.key === "[CLY]_view" && typeof e.dur !== "undefined"); + assert.equal(durationEvents.length, 1); + assert.equal(durationEvents[0].device_id, "alice"); + assert.equal(durationEvents[0].segmentation.name, "Alice invoices"); + assert.equal(hp.readEventQueue().length, 0, "an event recorded for alice is still waiting to be sent under bob"); + + const profiles = hp.readRequestQueue().filter((r) => r.user_details); + assert.equal(profiles.length, 1); + assert.equal(profiles[0].device_id, "alice"); + assert.equal(JSON.parse(profiles[0].user_details).custom.plan, "pro"); + + const crash = hp.readRequestQueue().find((r) => r.crash); + assert.equal(crash.device_id, "bob"); + assert.equal(typeof JSON.parse(crash.crash)._logs, "undefined"); + }); + + it("6- a view held back for missing consent is not replayed for the next user", async() => { + initMain({ device_id: "alice", require_consent: true }); + Countly.track_view("Alice secret page"); + Countly.change_id("bob", false); + Countly.add_consent(["views", "events"]); + await sleep(100); + const views = queuedEvents().concat(hp.readEventQueue()).filter((e) => e.key === "[CLY]_view"); + assert.equal(views.length, 0); + }); + + it("7- remote config values of the previous user are cleared on a switch without merge, and kept on a merge", async() => { + const server = await startMockServer((req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ flag: "alice-variant" })); + }); + initMain({ device_id: "alice", url: server.url }); + await new Promise((resolve) => { + Countly.fetch_remote_config(() => resolve()); + }); + assert.equal(Countly.get_remote_config("flag"), "alice-variant"); + Countly.change_id("alice-login", true); + assert.equal(Countly.get_remote_config("flag"), "alice-variant"); + Countly.change_id("bob", false); + assert.deepStrictEqual(Countly.get_remote_config(), {}); + await server.close(); + }); + + it("8- remote config downloaded for the previous user is not used when it arrives after a switch", async() => { + const server = await startMockServer((req, res, entry) => { + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ discount: `${entry.params.device_id}-only-discount` })); + }, 400); + }); + const dir = makeTempDir("rc-race"); + initMain({ device_id: "alice", url: server.url, storage_path: `${dir}${path.sep}` }); + const callbackResult = new Promise((resolve) => { + Countly.fetch_remote_config((err, values) => resolve({ err, values })); + }); + await sleep(100); + Countly.change_id("bob", false); + const result = await callbackResult; + await sleep(100); + await server.close(); + assert.ok(result.err instanceof Error, "the callback did not report that the values were not used"); + assert.deepStrictEqual(Countly.get_remote_config(), {}); + assert.deepStrictEqual(readStoredFile(path.join(dir, "__cly_remote_configs.json"), "cly_remote_configs"), {}); + }); + + it("9- with automatic remote config, a slower answer for the previous user does not replace the new user's values", async() => { + const server = await startMockServer((req, res, entry) => { + setTimeout(() => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ discount: `${entry.params.device_id}-only-discount` })); + }, entry.params.device_id === "alice" ? 500 : 50); + }); + initMain({ device_id: "alice", url: server.url, remote_config: true }); + await sleep(100); + Countly.change_id("bob", false); + await sleep(900); + await server.close(); + assert.deepStrictEqual(Countly.get_remote_config(), { discount: "bob-only-discount" }); + }); + + it("10- a consent change made right before a device ID change is sent under the previous user's ID", async() => { + const server = await startMockServer(); + Countly.halt(false); + initMain({ url: server.url, interval: 100, require_consent: true, device_id: "consent-before-switch", disable_sdk_behavior_settings_updates: true, disable_health_check: true }); + Countly.add_consent("events"); + Countly.change_id("consent-after-switch", false); + await sleep(1500); + await Countly.flush(5); + await server.close(); + const consents = server.requests.filter((r) => r.params.consent).map((r) => [r.params.device_id, JSON.parse(r.params.consent).events]); + assert.deepStrictEqual(consents, [["consent-before-switch", true]]); + }); + + it("11- flush also sends a consent change made right before it", async() => { + const server = await startMockServer(); + Countly.halt(false); + initMain({ url: server.url, interval: 100, require_consent: true, device_id: "consent-flush", disable_sdk_behavior_settings_updates: true, disable_health_check: true }); + Countly.add_consent("events"); + Countly.add_event({ key: "with_consent" }); + const delivered = await Countly.flush(5); + const consents = server.requests.filter((r) => r.params.consent).map((r) => JSON.parse(r.params.consent).events); + await server.close(); + assert.strictEqual(delivered, true); + assert.deepStrictEqual(consents, [true]); + }); +});