From cae9213b88c23601b330855ad66da003d80ff771 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:49:08 +0900 Subject: [PATCH 1/7] Dependency Update --- .eslintrc.js | 382 +- bulk_data/.gitignore | 4 - data/.gitignore | 4 - package-lock.json | 5292 +++++++++++++++--------- package.json | 16 +- test/customStorageDirectory/.gitignore | 4 - 6 files changed, 3504 insertions(+), 2198 deletions(-) delete mode 100644 bulk_data/.gitignore delete mode 100644 data/.gitignore delete mode 100644 test/customStorageDirectory/.gitignore 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/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/package-lock.json b/package-lock.json index 0a497a8..45f36b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,23 +10,23 @@ "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..8fe521a 100644 --- a/package.json +++ b/package.json @@ -32,14 +32,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/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 From 08ab255724d0ef79f5d325fc041df7360e4abed2 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:48:23 +0900 Subject: [PATCH 2/7] fixes --- .github/workflows/node.js.yml | 19 ++-- CHANGELOG.md | 5 + lib/countly-bulk.js | 12 +-- lib/countly.js | 37 ++++++-- test/helpers/helper_functions.js | 12 +-- test/tests_bulk.js | 46 +++++++++- test/tests_crashes.js | 23 +++++ test/tests_device_id_type.js | 4 +- test/tests_internal_limits.js | 30 +++--- test/tests_request_queue.js | 151 +++++++++++++++++++++++++++++++ 10 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 test/tests_request_queue.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5a729d8..3329cd9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,13 +9,18 @@ on: pull_request: branches: [ master, staging ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest strategy: matrix: - node: [14, 16, 18, 20, 22, 23] + # mocha 11 requires Node 18.18+, 24 is the active LTS + node: [18, 20, 22, 24] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node }} @@ -23,21 +28,21 @@ jobs: with: node-version: ${{ matrix.node }} cache: 'npm' - - run: npm install - - run: npm run build --if-present + - run: npm ci - run: npm test - + deploy: runs-on: ubuntu-latest needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + - name: Use Node.js 24.x uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 24.x cache: 'npm' - - run: npm install + - run: npm ci - name: Deploy documentation shell: bash env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b6a01..4c73e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Next Release +- Fixed a bug where calling "track_errors" repeatedly would register duplicate error handlers, causing a single error to be recorded multiple times +- Fixed a bug in CountlyBulk where explicitly provided "hour", "dow" and "tz" values of 0 were overridden with current time values in "add_request" and "add_bulk_request" +- SDK now removes its process and cluster event listeners when "halt" is called + ## 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/lib/countly-bulk.js b/lib/countly-bulk.js index 3ecdc93..704e28b 100644 --- a/lib/countly-bulk.js +++ b/lib/countly-bulk.js @@ -147,9 +147,9 @@ 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."); @@ -196,9 +196,9 @@ 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); } diff --git a/lib/countly.js b/lib/countly.js index f0a0181..f691d4b 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -75,6 +75,9 @@ Countly.Bulk = Bulk; var maxStackTraceLineLength = 200; var deviceIdType = null; var heartBeatTimer = null; + var uncaughtExceptionHandler = null; + var unhandledRejectionHandler = null; + var clusterForkHandler = null; /** * Array with list of available features that you can require consent for */ @@ -275,9 +278,10 @@ Countly.Bulk = Bulk; } } // handle future workers - cluster.on("fork", (worker) => { + clusterForkHandler = (worker) => { worker.on("message", handleWorkerMessage); - }); + }; + cluster.on("fork", clusterForkHandler); if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } @@ -331,6 +335,18 @@ Countly.Bulk = Bulk; clearInterval(heartBeatTimer); heartBeatTimer = null; } + if (clusterForkHandler) { + cluster.removeListener("fork", clusterForkHandler); + clusterForkHandler = null; + } + if (uncaughtExceptionHandler) { + process.removeListener("uncaughtException", uncaughtExceptionHandler); + uncaughtExceptionHandler = null; + } + if (unhandledRejectionHandler) { + process.removeListener("unhandledRejection", unhandledRejectionHandler); + unhandledRejectionHandler = null; + } // cc DEBUG cc.debug = false; @@ -1100,7 +1116,14 @@ Countly.Bulk = Bulk; 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) { CountlyStorage.forceStore(); @@ -1110,9 +1133,10 @@ 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) { @@ -1122,7 +1146,8 @@ Countly.Bulk = Bulk; console.error(`${(new Date()).toUTCString()} unhandledRejection:`, err.message); // eslint-disable-next-line no-console console.error(err.stack); - }); + }; + process.on('unhandledRejection', unhandledRejectionHandler); }; /** diff --git a/test/helpers/helper_functions.js b/test/helpers/helper_functions.js index 361771e..ae55de8 100644 --- a/test/helpers/helper_functions.js +++ b/test/helpers/helper_functions.js @@ -172,8 +172,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 +190,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 +278,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/tests_bulk.js b/test/tests_bulk.js index d0a6724..89343ff 100644 --- a/test/tests_bulk.js +++ b/test/tests_bulk.js @@ -1,11 +1,15 @@ /* eslint-disable no-console */ /* global runthis */ const assert = require("assert"); +const cluster = require("cluster"); 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"; @@ -75,13 +79,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); } @@ -342,7 +347,7 @@ describe("Bulk Tests", () => { }); // 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, @@ -362,4 +367,37 @@ describe("Bulk Tests", () => { 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_crashes.js b/test/tests_crashes.js index 12f2688..98e5696 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"); @@ -57,3 +58,25 @@ describe("Unhandled Error logic", () => { }, 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_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_internal_limits.js b/test/tests_internal_limits.js index 82f0a10..bb85596 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); }); @@ -102,9 +102,9 @@ describe("Testing internal limits", () => { 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_request_queue.js b/test/tests_request_queue.js new file mode 100644 index 0000000..790a092 --- /dev/null +++ b/test/tests_request_queue.js @@ -0,0 +1,151 @@ +/* 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); + }); +}); From cbc6ac237d53a1795a4917f5e2857bf4f43ca5b7 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:37:59 +0900 Subject: [PATCH 3/7] Bug fixes --- .gitignore | 3 +- CHANGELOG.md | 35 + lib/countly-bulk-user.js | 18 +- lib/countly-bulk.js | 183 ++-- lib/countly-common.js | 101 +- lib/countly-storage.js | 957 +++++++++++++----- lib/countly.js | 557 ++++++++-- test/helpers/helper_functions.js | 38 +- test/helpers/mock_server.js | 86 ++ test/helpers/run_script.js | 79 ++ test/helpers/scripts/bigint_values.js | 21 + test/helpers/scripts/bulk_consent_replay.js | 17 + test/helpers/scripts/cluster_app.js | 46 + test/helpers/scripts/cluster_disconnected.js | 19 + .../scripts/cluster_worker_disconnects.js | 17 + test/helpers/scripts/exit_after_call.js | 35 + test/helpers/scripts/flush_callback_throws.js | 17 + test/helpers/scripts/rejection.js | 17 + test/helpers/scripts/script_exits.js | 12 + test/helpers/scripts/storage_location.js | 21 + test/tests_bulk.js | 71 +- test/tests_bulk_fixes.js | 152 +++ test/tests_cluster.js | 72 ++ test/tests_crash_safety.js | 84 ++ test/tests_crashes.js | 3 +- test/tests_delivery.js | 186 ++++ test/tests_request_queue.js | 24 + test/tests_storage.js | 31 +- test/tests_storage_safety.js | 204 ++++ test/tests_user_switch.js | 190 ++++ 30 files changed, 2797 insertions(+), 499 deletions(-) create mode 100644 test/helpers/mock_server.js create mode 100644 test/helpers/run_script.js create mode 100644 test/helpers/scripts/bigint_values.js create mode 100644 test/helpers/scripts/bulk_consent_replay.js create mode 100644 test/helpers/scripts/cluster_app.js create mode 100644 test/helpers/scripts/cluster_disconnected.js create mode 100644 test/helpers/scripts/cluster_worker_disconnects.js create mode 100644 test/helpers/scripts/exit_after_call.js create mode 100644 test/helpers/scripts/flush_callback_throws.js create mode 100644 test/helpers/scripts/rejection.js create mode 100644 test/helpers/scripts/script_exits.js create mode 100644 test/helpers/scripts/storage_location.js create mode 100644 test/tests_bulk_fixes.js create mode 100644 test/tests_cluster.js create mode 100644 test/tests_crash_safety.js create mode 100644 test/tests_delivery.js create mode 100644 test/tests_storage_safety.js create mode 100644 test/tests_user_switch.js 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 4c73e59..5ff7f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,39 @@ ## Next Release +- ! Minor breaking change ! The SDK now stores its data (device ID, unsent requests and events) in a ".countly-storage" folder in the app's working directory instead of a "data" folder inside the SDK package, where every reinstall or upgrade of the package deleted it and the next start became a new user: + - A relative "storage_path" is now resolved against the app's working directory instead of the SDK's own folder + - Files are stored inside the given "storage_path" folder also when the path has no trailing slash + - Data found at the old location is moved to the new one once, and the old files are kept with a ".migrated" suffix. If the new folder is not writable, the old location stays in use + - CountlyBulk with file storage and no "storage_path" now stores its data in ".countly-storage/bulk/" + - Add ".countly-storage/" to your ".gitignore". Command line tools started from different folders, and containers, should set an absolute "storage_path" (for example a folder in the user's home or a mounted volume) to keep one device ID + - The SDK now prints a warning, also when logging is off, when the storage folder is not writable (for example on read-only file systems such as AWS Lambda) and when two instances of the same kind store their files in the same folder +- ! Minor breaking change ! The SDK now keeps the Node.js process running only while it has data to send and the server is answering. A script that records data now ends by itself once the data is delivered; data that cannot be sent yet stays stored and is sent on the next start +- ! Minor breaking change ! Requests now fail when the server sends nothing for 30 seconds, and are sent again later. The time can be set with the new "request_timeout" init option, in seconds (also for CountlyBulk) +- ! Minor breaking change ! With "track_errors", an unhandled promise rejection is now recorded as a non-fatal error, as the process keeps running after it. Uncaught exceptions are still recorded as fatal crashes +- ! Minor breaking change ! Changing the device ID without merge now also clears the stored remote config values, breadcrumbs and calls waiting for consent, and sends the previous user's open view duration and unsaved custom property changes under the previous ID before switching. Remote config values that were still downloading for the previous device ID are not used; the "fetch_remote_config" callback then gets an error +- ! Minor breaking change ! In a cluster, a worker now asks the main process once whether it runs the SDK, and the main process answers. These messages have a "cly" property, and your own "message" handlers in the main process and in workers receive them as well +- Added a new method "flush" that sends all recorded data right away and tells when it is delivered, for example before a script ends or in a shutdown handler before "process.exit()" +- Added a new init time flag "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 stores its data in its own "worker-" folder, numbered by PM2's instance number, or else by the cluster worker ID (which changes when a worker is restarted, so pass "device_id" to keep one ID) +- A cluster worker whose main process does not run the SDK now prints a warning that its data is not sent, also when logging is off +- CountlyBulk now prints a warning when "salt" is set, as Countly servers drop bulk data of apps that have a salt configured +- Fixed a bug where one request that got no answer, or whose answer was cut off, stopped all further sending until the app restarted (also in CountlyBulk) +- Fixed a bug where calling "process.exit()" right after an SDK call could leave the storage file empty and lose all stored data or the device ID. Files are now replaced in one step, and data not yet written is saved when the process exits +- Fixed a bug where a request was removed from the stored queue before the server confirmed it, so it was lost if the process ended meanwhile +- Fixed a bug where creating a CountlyBulk instance changed where, or whether, the SDK and other CountlyBulk instances stored their data, and where two CountlyBulk instances could send each other's data under the wrong app +- Fixed a bug where removing consent was never reported to the server +- Fixed a bug where consent changes of CountlyBulk users were never sent +- Fixed a bug where a BigInt value or an object that refers to itself in event, view, user property or crash data crashed the app. BigInt values are now sent as text and self references are left out +- Fixed a bug where "change_id" with an empty value stopped all tracking. Such values are now ignored with a warning, and an empty device ID stored by an older version is ignored at start +- Fixed a bug where the previous user's open view, custom property changes and breadcrumbs were sent under the new device ID after "change_id" without merge +- Fixed a bug where headers added in "http_options" made every GET request fail, so nothing was sent +- Fixed a bug where "request" cut every value to 256 characters, which broke raw event and user payloads. Raw requests now also count towards the request queue size +- Fixed a bug where requests containing an apostrophe failed the checksum check of Countly 26.01 and newer servers when "salt" is set. The characters ! ' ( ) * are now percent-encoded +- Fixed a bug where a CountlyBulk user created with "ip_address" threw an error on most calls +- Fixed a bug where giving consent to a CountlyBulk user after a "begin_session" or "report_view" call that waited for consent crashed the app. The waiting call is now sent +- Fixed a bug where the CountlyBulk "start" callback reported an empty queue while data was still waiting for a retry or being sent +- Queued requests are now sent one after another without waiting for the next heartbeat, and all recorded events are packed into requests at each heartbeat (also in CountlyBulk) +- File storage now combines repeated writes of the same data, so a busy app no longer rewrites the whole queue file for every recorded item +- Memory storage no longer writes files when a fatal crash is recorded +- Requests that were running when "halt" was called no longer change the SDK's state afterwards - Fixed a bug where calling "track_errors" repeatedly would register duplicate error handlers, causing a single error to be recorded multiple times - Fixed a bug in CountlyBulk where explicitly provided "hour", "dow" and "tz" values of 0 were overridden with current time values in "add_request" and "add_bulk_request" - SDK now removes its process and cluster event listeners when "halt" is called 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 704e28b..c482a65 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) @@ -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."); @@ -153,7 +160,7 @@ function CountlyBulk(conf) { 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) { @@ -202,7 +210,7 @@ function CountlyBulk(conf) { 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..e11e21b 100644 --- a/lib/countly-common.js +++ b/lib/countly-common.js @@ -3,12 +3,79 @@ */ 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: {}, + /** + * 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 +161,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 +186,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 +222,10 @@ 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. * @param {Object} params - object with query parameters * @returns {String} URL encoded query string */ @@ -147,7 +234,8 @@ 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 value = encodeURIComponent(params[key]).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); + str.push(`${key}=${value}`); } return str.join("&"); }, @@ -235,8 +323,11 @@ var cc = { */ log: function log(level, message, ...args) { 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..03b768e 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; + } + if (isCancelled && isCancelled()) { + fs.unlink(tmp, () => { + callback(null); + }); + 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.`); + 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,545 @@ 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); - - createDirectory(path.resolve(__dirname, storagePath)); -}; +function createStorage() { + var storagePath; + var legacyBase; + var useLegacyLayout = false; + var registeredFolder = null; + var storageMethod = {}; + var cache = {}; + var writes = {}; + var generation = 0; + var instance = {}; -/** - * 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 + * @param {String} key - key for file + * @returns {varies} value in file + */ + function readFile(key) { + var file = getFilePath(key); + var data; try { - fs.writeFileSync(dir, JSON.stringify(ob)); + data = fs.readFileSync(file, 'utf8'); } catch (ex) { - // tried to save whats possible - cc.log(cc.logLevelEnums.ERROR, `forceStore, Saving files failed. Error: [${ex}].`); + cc.log(cc.logLevelEnums.WARNING, `readFile, File not found for key: [${key}]. 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 { + data = JSON.parse(data); + } + catch (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, + 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 f691d4b..3b680be 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -78,6 +78,15 @@ Countly.Bulk = Bulk; 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; /** * Array with list of available features that you can require consent for */ @@ -109,7 +118,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 @@ -155,6 +166,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; @@ -181,7 +202,12 @@ Countly.Bulk = Bulk; // 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) { @@ -232,12 +258,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) { @@ -271,21 +297,26 @@ Countly.Bulk = Bulk; eventQueue = CountlyStorage.storeGet("cly_event", []); remoteConfigs = CountlyStorage.storeGet("cly_remote_configs", {}); 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 - clusterForkHandler = (worker) => { - worker.on("message", handleWorkerMessage); - }; - cluster.on("fork", clusterForkHandler); if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } } + else { + startPrimaryCheck(); + } } } }; @@ -320,7 +351,8 @@ Countly.Bulk = Bulk; failTimeoutAmount = 60; sessionUpdate = 60; maxEventBatch = 100; - readyToProcess = !preventRequestProcessing; + readyToProcess = true; + processingDisabled = !!preventRequestProcessing; trackTime = true; metrics = {}; lastParams = {}; @@ -339,6 +371,16 @@ Countly.Bulk = Bulk; cluster.removeListener("fork", clusterForkHandler); clusterForkHandler = null; } + workerListeners.forEach((entry) => { + entry.worker.removeListener("message", entry.listener); + }); + workerListeners = []; + stopPrimaryCheck(); + sdkGeneration++; + inFlightRequest = null; + settleFlushWaiters(false); + requestTimeout = 30; + standaloneWorker = false; if (uncaughtExceptionHandler) { process.removeListener("uncaughtException", uncaughtExceptionHandler); uncaughtExceptionHandler = null; @@ -542,13 +584,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(); } } @@ -671,16 +715,28 @@ 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 (ownsQueue()) { if (Countly.device_id !== newId) { if (!merge) { + closeCurrentUser(); // empty event queue if (eventQueue.length > 0) { toRequestQueue({ events: JSON.stringify(eventQueue) }); @@ -711,20 +767,40 @@ 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); } } } 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 @@ -780,24 +856,25 @@ Countly.Bulk = Bulk; cc.log(cc.logLevelEnums.ERROR, "add_cly_events, Event must have 'key' property."); return; } - if (cluster.isMaster) { + if (ownsQueue()) { 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); 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") } }); } } @@ -868,7 +945,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; @@ -1040,7 +1117,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 = {}; }, @@ -1109,7 +1186,10 @@ 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) { @@ -1125,7 +1205,7 @@ Countly.Bulk = Bulk; } uncaughtExceptionHandler = (err) => { recordError(err, false); - if (cluster.isMaster) { + if (ownsQueue()) { CountlyStorage.forceStore(); } // eslint-disable-next-line no-console @@ -1138,10 +1218,7 @@ Countly.Bulk = Bulk; 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 @@ -1178,7 +1255,8 @@ 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 @@ -1210,7 +1288,15 @@ Countly.Bulk = Bulk; } } prepareRequest(request); + var generation = sdkGeneration; makeRequest(Countly.url, readPath, request, (err, params, responseText) => { + 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); + } + return; + } try { var configs = JSON.parse(responseText); if (request.keys || request.omit_keys) { @@ -1223,7 +1309,7 @@ Countly.Bulk = Bulk; // we replace config remoteConfigs = configs; } - if (cluster.isMaster) { + if (ownsQueue()) { CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); cc.log(cc.logLevelEnums.INFO, `fetch_remote_config, Fetched remote config: [${remoteConfigs}].`); } @@ -1394,21 +1480,27 @@ 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 (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."); @@ -1416,6 +1508,56 @@ 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; + } + 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 * */ @@ -1482,7 +1624,7 @@ Countly.Bulk = Bulk; * @param {Object} request - object with request parameters */ function toRequestQueue(request) { - if (cluster.isMaster) { + if (ownsQueue()) { if (!Countly.app_key || !Countly.device_id) { cc.log(cc.logLevelEnums.ERROR, "toRequestQueue, app_key or device_id is missing."); return; @@ -1491,12 +1633,13 @@ Countly.Bulk = Bulk; 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(); } else { cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Sending message to the parent process. Adding request to the queue."); @@ -1517,37 +1660,243 @@ Countly.Bulk = Bulk; } } - // process event queue - if (eventQueue.length > 0) { - if (eventQueue.length <= maxEventBatch) { - toRequestQueue({ events: JSON.stringify(eventQueue) }); - eventQueue = []; + packEvents(); + processRequestQueue(); + + 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)) }); + } + 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) { + return; + } + readyToProcess = false; + var params = requestQueue[0]; + var generation = sdkGeneration; + inFlightRequest = params; + cc.log(cc.logLevelEnums.DEBUG, "Processing the request:", params); + makeRequest(Countly.url, apiPath, params, (err, res) => { + 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}]`); } 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); } - 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) { + 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); + /** + * 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; + } } /** @@ -1658,7 +2007,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 @@ -1667,6 +2018,26 @@ 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 of a successful request + */ + function finish(failed, responseText) { + if (finished) { + return; + } + finished = true; + if (typeof callback === "function") { + if (failed) { + callback(true, params); + } + else { + callback(false, params, responseText); + } + } + } try { info = info || "general"; isBroad = isBroad || false; @@ -1679,6 +2050,7 @@ Countly.Bulk = Bulk; port: serverOptions.port, path: api, method: "GET", + headers: {}, }; if (params && params.picturePath) { @@ -1764,10 +2136,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); } } @@ -1780,6 +2150,7 @@ Countly.Bulk = Bulk; } var req = protocol.request(options, (res) => { var str = ""; + res.setEncoding("utf8"); res.on("data", (chunk) => { str += chunk; }); @@ -1795,34 +2166,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.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); } } @@ -1863,10 +2238,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/test/helpers/helper_functions.js b/test/helpers/helper_functions.js index ae55de8..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); diff --git a/test/helpers/mock_server.js b/test/helpers/mock_server.js new file mode 100644 index 0000000..683bec9 --- /dev/null +++ b/test/helpers/mock_server.js @@ -0,0 +1,86 @@ +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; +} + +module.exports = { startMockServer, receivedEvents }; 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/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_bulk.js b/test/tests_bulk.js index 89343ff..67381ba 100644 --- a/test/tests_bulk.js +++ b/test/tests_bulk.js @@ -2,9 +2,9 @@ /* 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 @@ -14,6 +14,11 @@ 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); @@ -107,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); @@ -124,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); }); @@ -167,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); }); @@ -185,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); }); @@ -208,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); }); @@ -228,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); }); @@ -246,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); }); @@ -267,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); }); @@ -291,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); }); @@ -312,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); }); @@ -331,17 +336,17 @@ 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); }); @@ -353,17 +358,17 @@ describe("Bulk Tests", () => { 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); }); diff --git a/test/tests_bulk_fixes.js b/test/tests_bulk_fixes.js new file mode 100644 index 0000000..5939735 --- /dev/null +++ b/test/tests_bulk_fixes.js @@ -0,0 +1,152 @@ +/* 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"); + }); +}); diff --git a/test/tests_cluster.js b/test/tests_cluster.js new file mode 100644 index 0000000..857e658 --- /dev/null +++ b/test/tests_cluster.js @@ -0,0 +1,72 @@ +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) { + const server = await startMockServer(); + const dir = makeTempDir(`cluster-${mode}`); + 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"); + }); +}); 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 98e5696..2bccd3b 100644 --- a/test/tests_crashes.js +++ b/test/tests_crashes.js @@ -53,7 +53,8 @@ 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); }); diff --git a/test/tests_delivery.js b/test/tests_delivery.js new file mode 100644 index 0000000..5f0f74a --- /dev/null +++ b/test/tests_delivery.js @@ -0,0 +1,186 @@ +const assert = require("assert"); +const crypto = require("crypto"); +const path = require("path"); +const Countly = require("../lib/countly"); +const hp = require("./helpers/helper_functions"); +const { startMockServer, receivedEvents } = 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"}'); +} + +// the checksum check of 26.01 servers: the query is rebuilt with the WHATWG URL parser before hashing +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(); +} + +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 }); + 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 }); + 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); + }); +}); diff --git a/test/tests_request_queue.js b/test/tests_request_queue.js index 790a092..0480916 100644 --- a/test/tests_request_queue.js +++ b/test/tests_request_queue.js @@ -148,4 +148,28 @@ describe("Request queue tests", () => { }, 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_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..5e56ee1 --- /dev/null +++ b/test/tests_storage_safety.js @@ -0,0 +1,204 @@ +/* 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 } = 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); + }); +} + +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; + } + }); +}); diff --git a/test/tests_user_switch.js b/test/tests_user_switch.js new file mode 100644 index 0000000..a656043 --- /dev/null +++ b/test/tests_user_switch.js @@ -0,0 +1,190 @@ +/* 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" }); + }); +}); From 793466a7a0f13f1f182c918510f2be7ecceb19dd Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:51:12 +0900 Subject: [PATCH 4/7] ci --- .github/dependabot.yml | 19 ++ .github/scripts/check_release.js | 58 ++++++ .github/workflows/codeql-analysis.yml | 23 +-- .github/workflows/node.js.yml | 162 ++++++++++++++-- .github/workflows/release_notice.yml | 59 ++++-- CHANGELOG.md | 14 +- lib/countly-common.js | 6 +- package.json | 9 +- test/compat/compat_check.js | 267 ++++++++++++++++++++++++++ test/tests_bulk_fixes.js | 36 ++++ test/tests_consents.js | 3 +- test/tests_delivery.js | 71 +++++++ test/tests_package.js | 16 ++ test/tests_storage_safety.js | 153 ++++++++++++++- 14 files changed, 840 insertions(+), 56 deletions(-) create mode 100644 .github/scripts/check_release.js create mode 100644 test/compat/compat_check.js create mode 100644 test/tests_package.js 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 3329cd9..bac79f7 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,36 +9,76 @@ on: pull_request: branches: [ master, staging ] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: - - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: - matrix: - # mocha 11 requires Node 18.18+, 24 is the active LTS - node: [18, 20, 22, 24] + 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@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 ci - - run: npm test + - 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: + fail-fast: false + matrix: + node: [ 10, 12, 14, 16 ] + 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 }} + # 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 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js 24.x - uses: actions/setup-node@v4 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x cache: 'npm' @@ -48,3 +88,101 @@ jobs: 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 + + release: + runs-on: ubuntu-latest + needs: version + if: needs.version.outputs.new == 'true' + # the npm environment holds the approval step, and npm's trusted publisher setting names it + environment: npm + permissions: + contents: write + id-token: write + outputs: + created: ${{ steps.release.outputs.created }} + url: ${{ steps.release.outputs.url }} + notes: ${{ steps.release.outputs.notes }} + 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 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Publish to 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, publishing is skipped" + else + npm publish + fi + - name: Create the GitHub release + id: release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.version.outputs.version }} + run: | + node .github/scripts/check_release.js --notes > release-notes.md + if gh release view "$VERSION" > /dev/null 2>&1; then + echo "::notice::The GitHub release $VERSION exists already, so it is not created again and no notice is sent" + echo "created=false" >> "$GITHUB_OUTPUT" + else + gh release create "$VERSION" --title "Countly NodeJS SDK $VERSION" --notes-file release-notes.md --target "$GITHUB_SHA" + echo "created=true" >> "$GITHUB_OUTPUT" + fi + URL=$(gh release view "$VERSION" --json url --jq .url) + echo "url=$URL" >> "$GITHUB_OUTPUT" + { + echo "notes<> "$GITHUB_OUTPUT" + + # a release created with the workflow token does not start other workflows, so the notice is called directly + notify: + needs: [ version, release ] + if: needs.release.outputs.created == 'true' + uses: ./.github/workflows/release_notice.yml + with: + tag_name: ${{ needs.version.outputs.version }} + html_url: ${{ needs.release.outputs.url }} + body: ${{ needs.release.outputs.notes }} + secrets: inherit diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml index e17315a..823beec 100644 --- a/.github/workflows/release_notice.yml +++ b/.github/workflows/release_notice.yml @@ -3,37 +3,64 @@ on: release: types: [published] workflow_dispatch: + inputs: + tag_name: + description: "Version to announce, for example 26.1.0" + type: string + required: false + html_url: + description: "Link to the GitHub release" + type: string + required: false + body: + description: "Release notes" + type: string + required: false + workflow_call: + inputs: + tag_name: + type: string + required: true + html_url: + type: string + required: true + body: + type: string + required: false + default: "" +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: | { "repository": "${{ github.repository }}", - "tag_name": "${{ github.event.release.tag_name }}", + "tag_name": "${{ inputs.tag_name || github.event.release.tag_name }}", "actor": "${{ github.actor }}", - "body": ${{ toJSON(github.event.release.body) }}, - "html_url": "${{ github.event.release.html_url }}" + "body": ${{ toJSON(inputs.body || github.event.release.body) }}, + "html_url": "${{ inputs.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 }} + title: New ${{ github.repository }} version ${{ inputs.tag_name || 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. - `${{ github.event.release.body }}` + Release URL: ${{ inputs.html_url || github.event.release.html_url }} + Click [here](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md) to view the change log. + `${{ inputs.body || github.event.release.body }}` diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff7f58..21210ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Next Release -- ! Minor breaking change ! The SDK now stores its data (device ID, unsent requests and events) in a ".countly-storage" folder in the app's working directory instead of a "data" folder inside the SDK package, where every reinstall or upgrade of the package deleted it and the next start became a new user: +- ! Minor breaking change ! The SDK now stores its data (device ID, unsent requests and events) 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 became a new user - A relative "storage_path" is now resolved against the app's working directory instead of the SDK's own folder - Files are stored inside the given "storage_path" folder also when the path has no trailing slash - Data found at the old location is moved to the new one once, and the old files are kept with a ".migrated" suffix. If the new folder is not writable, the old location stays in use @@ -9,10 +10,16 @@ - ! Minor breaking change ! The SDK now keeps the Node.js process running only while it has data to send and the server is answering. A script that records data now ends by itself once the data is delivered; data that cannot be sent yet stays stored and is sent on the next start - ! Minor breaking change ! Requests now fail when the server sends nothing for 30 seconds, and are sent again later. The time can be set with the new "request_timeout" init option, in seconds (also for CountlyBulk) - ! Minor breaking change ! With "track_errors", an unhandled promise rejection is now recorded as a non-fatal error, as the process keeps running after it. Uncaught exceptions are still recorded as fatal crashes -- ! Minor breaking change ! Changing the device ID without merge now also clears the stored remote config values, breadcrumbs and calls waiting for consent, and sends the previous user's open view duration and unsaved custom property changes under the previous ID before switching. Remote config values that were still downloading for the previous device ID are not used; the "fetch_remote_config" callback then gets an error +- ! Minor breaking change ! Changing the device ID without merge now also: + - Clears the stored remote config values, breadcrumbs and calls waiting for consent + - Sends the previous user's open view duration and unsaved custom property changes under the previous ID before switching + - Ignores remote config values that were still downloading for the previous device ID. The "fetch_remote_config" callback then gets an error - ! Minor breaking change ! In a cluster, a worker now asks the main process once whether it runs the SDK, and the main process answers. These messages have a "cly" property, and your own "message" handlers in the main process and in workers receive them as well +- The npm package now contains only the SDK code, README, LICENSE and CHANGELOG. Tests, examples and repository configuration files are no longer included - Added a new method "flush" that sends all recorded data right away and tells when it is delivered, for example before a script ends or in a shutdown handler before "process.exit()" -- Added a new init time flag "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 stores its data in its own "worker-" folder, numbered by PM2's instance number, or else by the cluster worker ID (which changes when a worker is restarted, so pass "device_id" to keep one ID) +- Added a new init time flag "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 stores 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 is restarted, so pass "device_id" to keep one ID - A cluster worker whose main process does not run the SDK now prints a warning that its data is not sent, also when logging is off - CountlyBulk now prints a warning when "salt" is set, as Countly servers drop bulk data of apps that have a salt configured - Fixed a bug where one request that got no answer, or whose answer was cut off, stopped all further sending until the app restarted (also in CountlyBulk) @@ -27,6 +34,7 @@ - Fixed a bug where headers added in "http_options" made every GET request fail, so nothing was sent - Fixed a bug where "request" cut every value to 256 characters, which broke raw event and user payloads. Raw requests now also count towards the request queue size - Fixed a bug where requests containing an apostrophe failed the checksum check of Countly 26.01 and newer servers when "salt" is set. The characters ! ' ( ) * are now percent-encoded +- Fixed a bug where a value containing half of an emoji, for example a device ID cut in the middle of an emoji, could never be sent and stopped all further sending, also after a restart. Such half characters are now sent as the replacement character "�" - Fixed a bug where a CountlyBulk user created with "ip_address" threw an error on most calls - Fixed a bug where giving consent to a CountlyBulk user after a "begin_session" or "report_view" call that waited for consent crashed the app. The waiting call is now sent - Fixed a bug where the CountlyBulk "start" callback reported an empty queue while data was still waiting for a retry or being sent diff --git a/lib/countly-common.js b/lib/countly-common.js index e11e21b..e14c8b7 100644 --- a/lib/countly-common.js +++ b/lib/countly-common.js @@ -226,6 +226,9 @@ var cc = { * 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 */ @@ -234,7 +237,8 @@ var cc = { var keys = Object.keys(params || {}); for (var i = 0; i < keys.length; i++) { var key = keys[i]; - var value = encodeURIComponent(params[key]).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); + 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("&"); diff --git a/package.json b/package.json index 8fe521a..c7a1154 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "version": "24.10.4", "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", diff --git a/test/compat/compat_check.js b/test/compat/compat_check.js new file mode 100644 index 0000000..583c158 --- /dev/null +++ b/test/compat/compat_check.js @@ -0,0 +1,267 @@ +/** + * 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 + * @param {Boolean} ignoreFirst - true to leave the first request unanswered + * @returns {Promise} server url, received requests and a close function + */ +function startServer(ignoreFirst) { + return new Promise((resolve) => { + var requests = []; + 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]; + }); + requests.push({ path: req.url.split("?")[0], params: params }); + if (ignoreFirst && requests.length === 1) { + return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + 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); +} + +/** + * 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); + 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/tests_bulk_fixes.js b/test/tests_bulk_fixes.js index 5939735..ccf9076 100644 --- a/test/tests_bulk_fixes.js +++ b/test/tests_bulk_fixes.js @@ -149,4 +149,40 @@ describe("Bulk fix tests", () => { 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_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_delivery.js b/test/tests_delivery.js index 5f0f74a..5a2b4db 100644 --- a/test/tests_delivery.js +++ b/test/tests_delivery.js @@ -2,6 +2,7 @@ const assert = require("assert"); const crypto = require("crypto"); const path = require("path"); const Countly = require("../lib/countly"); +const cc = require("../lib/countly-common"); const hp = require("./helpers/helper_functions"); const { startMockServer, receivedEvents } = require("./helpers/mock_server"); const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); @@ -183,4 +184,74 @@ describe("Delivery tests", () => { 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 }); + 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_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_storage_safety.js b/test/tests_storage_safety.js index 5e56ee1..6fe71ff 100644 --- a/test/tests_storage_safety.js +++ b/test/tests_storage_safety.js @@ -7,7 +7,7 @@ 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 } = require("./helpers/mock_server"); +const { startMockServer, receivedEvents } = require("./helpers/mock_server"); const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); const { StorageTypes } = Countly; @@ -23,6 +23,26 @@ function sleep(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]); @@ -201,4 +221,135 @@ describe("Storage safety tests", () => { 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); + }); }); From 117af6242ebb7ebd0497dc81de4bcd011bf6cf11 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:49:54 +0900 Subject: [PATCH 5/7] sbs and hc --- CHANGELOG.md | 13 + lib/countly-behavior.js | 222 +++++++++ lib/countly-common.js | 5 + lib/countly-storage.js | 11 +- lib/countly.js | 604 +++++++++++++++++++++++-- test/compat/compat_check.js | 67 ++- test/helpers/behavior_settings.js | 150 ++++++ test/helpers/mock_server.js | 19 +- test/helpers/scripts/health_on_exit.js | 17 + test/tests_behavior_settings.js | 499 ++++++++++++++++++++ test/tests_cluster.js | 29 +- test/tests_delivery.js | 19 +- test/tests_health_check.js | 186 ++++++++ test/tests_internal_limits.js | 2 +- test/tests_salt.js | 6 + test/tests_user_switch.js | 26 ++ 16 files changed, 1798 insertions(+), 77 deletions(-) create mode 100644 lib/countly-behavior.js create mode 100644 test/helpers/behavior_settings.js create mode 100644 test/helpers/scripts/health_on_exit.js create mode 100644 test/tests_behavior_settings.js create mode 100644 test/tests_health_check.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 21210ce..df50a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ - Sends the previous user's open view duration and unsaved custom property changes under the previous ID before switching - Ignores remote config values that were still downloading for the previous device ID. The "fetch_remote_config" callback then gets an error - ! Minor breaking change ! In a cluster, a worker now asks the main process once whether it runs the SDK, and the main process answers. These messages have a "cly" property, and your own "message" handlers in the main process and in workers receive them as well +- ! Minor breaking change ! Added SDK behavior settings. The SDK now fetches the settings set in the dashboard's SDK Manager at start, every 4 hours (or at the refresh interval set there) and after a device ID change, and follows them: + - The settings can turn off all tracking, all sending, or sessions, views, custom events, crashes and location one by one, change the queue, batch and length limits, and turn the consent requirement on or off, also one set with "require_consent", as on Android and Web. Without that setting the "require_consent" value applies + - A setting set in SDK Manager takes precedence over the matching init option. A setting removed there goes back to the init option, or else to the default + - The fetched settings are stored and used from the next start on, also before the server answers. The new init option "behavior_settings" provides settings, in the format SDK Manager exports, for the time before the first successful fetch + - While tracking is turned off nothing new is recorded, including consent changes and "request" calls, and data recorded before is still sent. While sending is turned off data is kept and sent once sending is turned on again, "flush" resolves false right away and "fetch_remote_config" calls its callback with an error + - The settings request is sent also when consent is required and not given, and while sending is turned off, so that changes reach the SDK. Set the new init option "disable_sdk_behavior_settings_updates" to not fetch the settings; stored and provided settings still apply + - After a device ID change with merge, the settings are fetched once the merge request is delivered +- ! Minor breaking change ! Added a health check. Once per start, after the settings fetch, the SDK sends the number of errors and warnings it logged (also when logging is off), the status code and the first 1000 characters of the answer of the last request the server refused, and how often sending paused, together with the app version. The numbers are kept until the server confirms them. The health check is sent also when consent is required and not given, and not while sending is turned off. Set the new init option "disable_health_check" to not send it +- ! Minor breaking change ! After a request the server answered slowly (in 10 seconds or more by default), the SDK now pauses sending for 60 seconds when the queue is at most half full and the request was recorded within the last 24 hours, so that a busy server can recover. SDK Manager can change these values, and "flush" does not wait for the pause. Set the new init option "disable_backoff_mechanism" to never pause +- SDK Manager can now set a maximum age for queued requests. Older requests are then dropped instead of sent, and a "flush" call waiting for them resolves false. This is off by default +- A missing storage file, which is normal before the SDK first writes it, is no longer logged as a warning - The npm package now contains only the SDK code, README, LICENSE and CHANGELOG. Tests, examples and repository configuration files are no longer included - Added a new method "flush" that sends all recorded data right away and tells when it is delivered, for example before a script ends or in a shutdown handler before "process.exit()" - Added a new init time flag "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: @@ -27,6 +38,7 @@ - Fixed a bug where a request was removed from the stored queue before the server confirmed it, so it was lost if the process ended meanwhile - Fixed a bug where creating a CountlyBulk instance changed where, or whether, the SDK and other CountlyBulk instances stored their data, and where two CountlyBulk instances could send each other's data under the wrong app - Fixed a bug where removing consent was never reported to the server +- Fixed a bug where a consent change made less than a second before a device ID change was reported under the new device ID or not at all, and where a consent change made right before "flush" and "process.exit()" was lost. Such a change is now sent right away, under the device ID of the user who made it - Fixed a bug where consent changes of CountlyBulk users were never sent - Fixed a bug where a BigInt value or an object that refers to itself in event, view, user property or crash data crashed the app. BigInt values are now sent as text and self references are left out - Fixed a bug where "change_id" with an empty value stopped all tracking. Such values are now ignored with a warning, and an empty device ID stored by an older version is ignored at start @@ -35,6 +47,7 @@ - Fixed a bug where "request" cut every value to 256 characters, which broke raw event and user payloads. Raw requests now also count towards the request queue size - Fixed a bug where requests containing an apostrophe failed the checksum check of Countly 26.01 and newer servers when "salt" is set. The characters ! ' ( ) * are now percent-encoded - Fixed a bug where a value containing half of an emoji, for example a device ID cut in the middle of an emoji, could never be sent and stopped all further sending, also after a restart. Such half characters are now sent as the replacement character "�" +- Fixed a bug where the key length and segmentation limits also cut Countly's own events, so the server no longer recognized them. For example, with a key length limit of 8 a view was sent as "[CLY]_vi". Views, star ratings, feedback and other internal events now keep their key and segmentation; the segmentation given to "track_view" is still limited - Fixed a bug where a CountlyBulk user created with "ip_address" threw an error on most calls - Fixed a bug where giving consent to a CountlyBulk user after a "begin_session" or "report_view" call that waited for consent crashed the app. The waiting call is now sent - Fixed a bug where the CountlyBulk "start" callback reported an empty queue while data was still waiting for a retry or being sent 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-common.js b/lib/countly-common.js index e14c8b7..7d88748 100644 --- a/lib/countly-common.js +++ b/lib/countly-common.js @@ -62,6 +62,8 @@ var cc = { 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. @@ -326,6 +328,9 @@ 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] instanceof Error) { args[0] = args[0].stack || String(args[0]); diff --git a/lib/countly-storage.js b/lib/countly-storage.js index 03b768e..dbc8392 100644 --- a/lib/countly-storage.js +++ b/lib/countly-storage.js @@ -440,7 +440,8 @@ function createStorage() { } /** - * Read value from file + * 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 */ @@ -451,7 +452,12 @@ function createStorage() { data = fs.readFileSync(file, 'utf8'); } catch (ex) { - cc.log(cc.logLevelEnums.WARNING, `readFile, File not found for key: [${key}]. Returning null.`); + 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; } @@ -761,6 +767,7 @@ module.exports = { storeRemove: defaultStorage.storeRemove, writeFile: defaultStorage.writeFile, forceStore: defaultStorage.forceStore, + flushSync: defaultStorage.flushSync, getStoragePath: defaultStorage.getStoragePath, setStoragePath: defaultStorage.setStoragePath, resetStorage: defaultStorage.resetStorage, diff --git a/lib/countly.js b/lib/countly.js index 3b680be..a4cb726 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"); @@ -87,6 +88,19 @@ Countly.Bulk = Bulk; 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 */ @@ -98,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 @@ -142,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}", @@ -197,6 +225,21 @@ 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 @@ -296,6 +339,13 @@ 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(); if (cluster.isMaster) { // listen to current workers @@ -313,6 +363,7 @@ Countly.Bulk = Bulk; if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } + fetchBehaviorSettings(sendHealthCheck); } else { startPrimaryCheck(); @@ -379,6 +430,27 @@ Countly.Bulk = Bulk; 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) { @@ -417,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(); }; @@ -610,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."); @@ -672,6 +772,7 @@ Countly.Bulk = Bulk; reportViewDuration(); sessionStarted = false; toRequestQueue({ end_session: 1, session_duration: sec }); + saveHealthCounters(true); } } }; @@ -733,13 +834,17 @@ Countly.Bulk = Bulk; } 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 (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); } @@ -757,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; @@ -774,6 +879,12 @@ Countly.Bulk = Bulk; if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } + if (mergeQueued) { + settingsFetchAfterMerge = true; + } + else { + fetchBehaviorSettings(); + } } } else { @@ -857,11 +968,17 @@ Countly.Bulk = Bulk; return; } 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.makeJsonSafe(cc.getProperties(event, props), "add_cly_event"); e.timestamp = getMsTimestamp(); @@ -1262,6 +1379,19 @@ Countly.Bulk = Bulk; * @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", @@ -1297,26 +1427,28 @@ Countly.Bulk = Bulk; } return; } - 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 (!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}].`); } } - 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}].`); + catch (ex) { + // silent catch } } - catch (ex) { - // silent catch - } if (typeof callback === "function") { callback(err, remoteConfigs); } @@ -1375,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}].`); @@ -1493,6 +1629,10 @@ Countly.Bulk = Bulk; return; } 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(); @@ -1534,6 +1674,13 @@ Countly.Bulk = Bulk; 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); @@ -1597,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; } @@ -1622,12 +1769,17 @@ 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) { + 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); @@ -1640,11 +1792,58 @@ Countly.Bulk = Bulk; 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; } /** @@ -1662,6 +1861,7 @@ Countly.Bulk = Bulk; packEvents(); processRequestQueue(); + saveHealthCounters(false); heartBeatTimer = setTimeout(heartBeat, beatInterval); updateKeepAlive(); @@ -1678,7 +1878,7 @@ Countly.Bulk = Bulk; var events = eventQueue; eventQueue = []; for (var i = 0; i < events.length; i += batchSize) { - toRequestQueue({ events: JSON.stringify(events.slice(i, i + batchSize)) }); + toRequestQueue({ events: JSON.stringify(events.slice(i, i + batchSize)) }, true); } CountlyStorage.storeSet("cly_event", eventQueue); } @@ -1689,15 +1889,23 @@ Countly.Bulk = Bulk; * 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) { + 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) => { + makeRequest(Countly.url, apiPath, params, (err, res, responseText, statusCode) => { if (generation !== sdkGeneration) { return; } @@ -1706,6 +1914,12 @@ Countly.Bulk = Bulk; 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 index = requestQueue.indexOf(params); @@ -1713,6 +1927,11 @@ Countly.Bulk = Bulk; requestQueue.splice(index, 1); } CountlyStorage.storeSet("cly_queue", requestQueue); + checkBackoff(params, sentAt); + if (settingsFetchAfterMerge && params.old_device_id) { + settingsFetchAfterMerge = false; + fetchBehaviorSettings(); + } } readyToProcess = true; if (err) { @@ -1738,7 +1957,7 @@ Countly.Bulk = Bulk; return; } var hasWork = requestQueue.length > 0 || eventQueue.length > 0; - if (hasWork && readyToProcess && !processingDisabled && !Countly.test_mode && cc.getTimestamp() > failTimeout) { + if (hasWork && readyToProcess && !processingDisabled && !Countly.test_mode && cc.getTimestamp() > failTimeout && setting("networking") && !isBackingOff()) { heartBeatTimer.ref(); } else { @@ -1746,6 +1965,291 @@ Countly.Bulk = Bulk; } } + /** + * 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 @@ -1939,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 = ""; @@ -2022,20 +2530,16 @@ Countly.Bulk = Bulk; /** * Reports the outcome of the request to the callback, once * @param {Boolean} failed - true if the request failed - * @param {String=} responseText - response body of a successful request + * @param {String=} responseText - response body, when the server answered + * @param {Number=} statusCode - HTTP status code, when the server answered */ - function finish(failed, responseText) { + function finish(failed, responseText, statusCode) { if (finished) { return; } finished = true; if (typeof callback === "function") { - if (failed) { - callback(true, params); - } - else { - callback(false, params, responseText); - } + callback(failed, params, responseText, statusCode); } } try { @@ -2166,7 +2670,7 @@ Countly.Bulk = Bulk; // only JSON object with result can pass isResponseValidated = cc.isResponseValid(res.statusCode, str); } - finish(!isResponseValidated, str); + finish(!isResponseValidated, str, res.statusCode); }); res.on("error", (err) => { cc.log(cc.logLevelEnums.ERROR, "makeRequest, Connection failed while receiving the response. Error:", err); diff --git a/test/compat/compat_check.js b/test/compat/compat_check.js index 583c158..ce8a3e4 100644 --- a/test/compat/compat_check.js +++ b/test/compat/compat_check.js @@ -74,13 +74,16 @@ function removeFolder(folder) { } /** - * Starts a mock Countly server that records every request - * @param {Boolean} ignoreFirst - true to leave the first request unanswered + * 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) { +function startServer(ignoreFirst, settings) { return new Promise((resolve) => { var requests = []; + var ignored = false; var sockets = []; var server = http.createServer((req, res) => { var body = ""; @@ -93,11 +96,17 @@ function startServer(ignoreFirst) { Object.keys(fromBody).forEach((key) => { params[key] = fromBody[key]; }); - requests.push({ path: req.url.split("?")[0], params: params }); - if (ignoreFirst && requests.length === 1) { + 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.writeHead(200, { "Content-Type": "application/json" }); res.end("{\"result\":\"Success\"}"); }); }); @@ -238,6 +247,51 @@ async function checkBulk() { 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 @@ -256,6 +310,7 @@ async function runGroup(name, run) { await runGroup("tracking", checkTracking); await runGroup("unanswered request", checkUnansweredRequest); await runGroup("bulk", checkBulk); + await runGroup("behavior settings", checkBehaviorSettings); try { removeFolder(workDir); } 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/mock_server.js b/test/helpers/mock_server.js index 683bec9..ee83722 100644 --- a/test/helpers/mock_server.js +++ b/test/helpers/mock_server.js @@ -1,3 +1,4 @@ +const crypto = require("crypto"); const http = require("http"); /** @@ -83,4 +84,20 @@ function receivedEvents(requests) { return events; } -module.exports = { startMockServer, receivedEvents }; +/** + * 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/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/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_cluster.js b/test/tests_cluster.js index 857e658..7d284ce 100644 --- a/test/tests_cluster.js +++ b/test/tests_cluster.js @@ -5,9 +5,15 @@ 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) { - const server = await startMockServer(); +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 }; @@ -69,4 +75,23 @@ describe("Cluster tests", () => { 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_delivery.js b/test/tests_delivery.js index 5a2b4db..5edbe74 100644 --- a/test/tests_delivery.js +++ b/test/tests_delivery.js @@ -1,10 +1,9 @@ const assert = require("assert"); -const crypto = require("crypto"); const path = require("path"); const Countly = require("../lib/countly"); const cc = require("../lib/countly-common"); const hp = require("./helpers/helper_functions"); -const { startMockServer, receivedEvents } = require("./helpers/mock_server"); +const { startMockServer, receivedEvents, passesServerChecksum } = require("./helpers/mock_server"); const { runScript, makeTempDir, removeTempDirs, readStoredFile } = require("./helpers/run_script"); const appKey = "YOUR_APP_KEY"; @@ -14,16 +13,6 @@ function answerSuccess(res) { res.end('{"result":"Success"}'); } -// the checksum check of 26.01 servers: the query is rebuilt with the WHATWG URL parser before hashing -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(); -} - function initSending(config) { Countly.halt(false); Countly.init({ app_key: appKey, storage_path: `${makeTempDir("delivery")}${path.sep}`, ...config }); @@ -46,7 +35,7 @@ describe("Delivery tests", () => { answerSuccess(res); } }); - initSending({ url: server.url, device_id: "timeout-user", interval: 100, fail_timeout: 1, request_timeout: 1 }); + 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); @@ -66,7 +55,7 @@ describe("Delivery tests", () => { } answerSuccess(res); }); - initSending({ url: server.url, device_id: "cutoff-user", interval: 100, fail_timeout: 1 }); + 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(); @@ -227,7 +216,7 @@ describe("Delivery tests", () => { 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 }); + 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; 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 bb85596..17406cd 100644 --- a/test/tests_internal_limits.js +++ b/test/tests_internal_limits.js @@ -97,7 +97,7 @@ 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); 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_user_switch.js b/test/tests_user_switch.js index a656043..58c6af1 100644 --- a/test/tests_user_switch.js +++ b/test/tests_user_switch.js @@ -187,4 +187,30 @@ describe("User switch and consent tests", () => { 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]); + }); }); From 2c735a6c626c6a8af7531d95ea21770145ce668e Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:43:44 +0900 Subject: [PATCH 6/7] Version bump --- .github/workflows/node.js.yml | 61 +++++++-------- .github/workflows/release_notice.yml | 14 +--- CHANGELOG.md | 109 ++++++++++++--------------- lib/countly-bulk.js | 2 +- lib/countly.js | 2 +- package-lock.json | 4 +- package.json | 2 +- 7 files changed, 82 insertions(+), 112 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index bac79f7..56d275f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -124,65 +124,58 @@ jobs: if: steps.version.outputs.new == 'true' run: node .github/scripts/check_release.js - release: + stage: runs-on: ubuntu-latest needs: version if: needs.version.outputs.new == 'true' - # the npm environment holds the approval step, and npm's trusted publisher setting names it + # a maintainer approves this job through the npm environment, which npm's trusted publisher setting also names environment: npm permissions: - contents: write + contents: read id-token: write - outputs: - created: ${{ steps.release.outputs.created }} - url: ${{ steps.release.outputs.url }} - notes: ${{ steps.release.outputs.notes }} 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: Publish to npm + - 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, publishing is skipped" - else - npm publish + 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 - - name: Create the GitHub release - id: release + + # 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: | - node .github/scripts/check_release.js --notes > release-notes.md if gh release view "$VERSION" > /dev/null 2>&1; then - echo "::notice::The GitHub release $VERSION exists already, so it is not created again and no notice is sent" - echo "created=false" >> "$GITHUB_OUTPUT" + echo "::notice::The GitHub release $VERSION exists already" else - gh release create "$VERSION" --title "Countly NodeJS SDK $VERSION" --notes-file release-notes.md --target "$GITHUB_SHA" - echo "created=true" >> "$GITHUB_OUTPUT" + 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 "url=$URL" >> "$GITHUB_OUTPUT" { - echo "notes<> "$GITHUB_OUTPUT" - - # a release created with the workflow token does not start other workflows, so the notice is called directly - notify: - needs: [ version, release ] - if: needs.release.outputs.created == 'true' - uses: ./.github/workflows/release_notice.yml - with: - tag_name: ${{ needs.version.outputs.version }} - html_url: ${{ needs.release.outputs.url }} - body: ${{ needs.release.outputs.notes }} - secrets: inherit + 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 823beec..ce5853c 100644 --- a/.github/workflows/release_notice.yml +++ b/.github/workflows/release_notice.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: tag_name: - description: "Version to announce, for example 26.1.0" + description: "Version to announce, for example 26.8.0" type: string required: false html_url: @@ -16,18 +16,6 @@ on: description: "Release notes" type: string required: false - workflow_call: - inputs: - tag_name: - type: string - required: true - html_url: - type: string - required: true - body: - type: string - required: false - default: "" permissions: contents: read jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index df50a6f..9fc3230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,63 +1,52 @@ -## Next Release -- ! Minor breaking change ! The SDK now stores its data (device ID, unsent requests and events) 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 became a new user - - A relative "storage_path" is now resolved against the app's working directory instead of the SDK's own folder - - Files are stored inside the given "storage_path" folder also when the path has no trailing slash - - Data found at the old location is moved to the new one once, and the old files are kept with a ".migrated" suffix. If the new folder is not writable, the old location stays in use - - CountlyBulk with file storage and no "storage_path" now stores its data in ".countly-storage/bulk/" - - Add ".countly-storage/" to your ".gitignore". Command line tools started from different folders, and containers, should set an absolute "storage_path" (for example a folder in the user's home or a mounted volume) to keep one device ID - - The SDK now prints a warning, also when logging is off, when the storage folder is not writable (for example on read-only file systems such as AWS Lambda) and when two instances of the same kind store their files in the same folder -- ! Minor breaking change ! The SDK now keeps the Node.js process running only while it has data to send and the server is answering. A script that records data now ends by itself once the data is delivered; data that cannot be sent yet stays stored and is sent on the next start -- ! Minor breaking change ! Requests now fail when the server sends nothing for 30 seconds, and are sent again later. The time can be set with the new "request_timeout" init option, in seconds (also for CountlyBulk) -- ! Minor breaking change ! With "track_errors", an unhandled promise rejection is now recorded as a non-fatal error, as the process keeps running after it. Uncaught exceptions are still recorded as fatal crashes -- ! Minor breaking change ! Changing the device ID without merge now also: - - Clears the stored remote config values, breadcrumbs and calls waiting for consent - - Sends the previous user's open view duration and unsaved custom property changes under the previous ID before switching - - Ignores remote config values that were still downloading for the previous device ID. The "fetch_remote_config" callback then gets an error -- ! Minor breaking change ! In a cluster, a worker now asks the main process once whether it runs the SDK, and the main process answers. These messages have a "cly" property, and your own "message" handlers in the main process and in workers receive them as well -- ! Minor breaking change ! Added SDK behavior settings. The SDK now fetches the settings set in the dashboard's SDK Manager at start, every 4 hours (or at the refresh interval set there) and after a device ID change, and follows them: - - The settings can turn off all tracking, all sending, or sessions, views, custom events, crashes and location one by one, change the queue, batch and length limits, and turn the consent requirement on or off, also one set with "require_consent", as on Android and Web. Without that setting the "require_consent" value applies - - A setting set in SDK Manager takes precedence over the matching init option. A setting removed there goes back to the init option, or else to the default - - The fetched settings are stored and used from the next start on, also before the server answers. The new init option "behavior_settings" provides settings, in the format SDK Manager exports, for the time before the first successful fetch - - While tracking is turned off nothing new is recorded, including consent changes and "request" calls, and data recorded before is still sent. While sending is turned off data is kept and sent once sending is turned on again, "flush" resolves false right away and "fetch_remote_config" calls its callback with an error - - The settings request is sent also when consent is required and not given, and while sending is turned off, so that changes reach the SDK. Set the new init option "disable_sdk_behavior_settings_updates" to not fetch the settings; stored and provided settings still apply - - After a device ID change with merge, the settings are fetched once the merge request is delivered -- ! Minor breaking change ! Added a health check. Once per start, after the settings fetch, the SDK sends the number of errors and warnings it logged (also when logging is off), the status code and the first 1000 characters of the answer of the last request the server refused, and how often sending paused, together with the app version. The numbers are kept until the server confirms them. The health check is sent also when consent is required and not given, and not while sending is turned off. Set the new init option "disable_health_check" to not send it -- ! Minor breaking change ! After a request the server answered slowly (in 10 seconds or more by default), the SDK now pauses sending for 60 seconds when the queue is at most half full and the request was recorded within the last 24 hours, so that a busy server can recover. SDK Manager can change these values, and "flush" does not wait for the pause. Set the new init option "disable_backoff_mechanism" to never pause -- SDK Manager can now set a maximum age for queued requests. Older requests are then dropped instead of sent, and a "flush" call waiting for them resolves false. This is off by default -- A missing storage file, which is normal before the SDK first writes it, is no longer logged as a warning -- The npm package now contains only the SDK code, README, LICENSE and CHANGELOG. Tests, examples and repository configuration files are no longer included -- Added a new method "flush" that sends all recorded data right away and tells when it is delivered, for example before a script ends or in a shutdown handler before "process.exit()" -- Added a new init time flag "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 stores 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 is restarted, so pass "device_id" to keep one ID -- A cluster worker whose main process does not run the SDK now prints a warning that its data is not sent, also when logging is off -- CountlyBulk now prints a warning when "salt" is set, as Countly servers drop bulk data of apps that have a salt configured -- Fixed a bug where one request that got no answer, or whose answer was cut off, stopped all further sending until the app restarted (also in CountlyBulk) -- Fixed a bug where calling "process.exit()" right after an SDK call could leave the storage file empty and lose all stored data or the device ID. Files are now replaced in one step, and data not yet written is saved when the process exits -- Fixed a bug where a request was removed from the stored queue before the server confirmed it, so it was lost if the process ended meanwhile -- Fixed a bug where creating a CountlyBulk instance changed where, or whether, the SDK and other CountlyBulk instances stored their data, and where two CountlyBulk instances could send each other's data under the wrong app -- Fixed a bug where removing consent was never reported to the server -- Fixed a bug where a consent change made less than a second before a device ID change was reported under the new device ID or not at all, and where a consent change made right before "flush" and "process.exit()" was lost. Such a change is now sent right away, under the device ID of the user who made it -- Fixed a bug where consent changes of CountlyBulk users were never sent -- Fixed a bug where a BigInt value or an object that refers to itself in event, view, user property or crash data crashed the app. BigInt values are now sent as text and self references are left out -- Fixed a bug where "change_id" with an empty value stopped all tracking. Such values are now ignored with a warning, and an empty device ID stored by an older version is ignored at start -- Fixed a bug where the previous user's open view, custom property changes and breadcrumbs were sent under the new device ID after "change_id" without merge -- Fixed a bug where headers added in "http_options" made every GET request fail, so nothing was sent -- Fixed a bug where "request" cut every value to 256 characters, which broke raw event and user payloads. Raw requests now also count towards the request queue size -- Fixed a bug where requests containing an apostrophe failed the checksum check of Countly 26.01 and newer servers when "salt" is set. The characters ! ' ( ) * are now percent-encoded -- Fixed a bug where a value containing half of an emoji, for example a device ID cut in the middle of an emoji, could never be sent and stopped all further sending, also after a restart. Such half characters are now sent as the replacement character "�" -- Fixed a bug where the key length and segmentation limits also cut Countly's own events, so the server no longer recognized them. For example, with a key length limit of 8 a view was sent as "[CLY]_vi". Views, star ratings, feedback and other internal events now keep their key and segmentation; the segmentation given to "track_view" is still limited -- Fixed a bug where a CountlyBulk user created with "ip_address" threw an error on most calls -- Fixed a bug where giving consent to a CountlyBulk user after a "begin_session" or "report_view" call that waited for consent crashed the app. The waiting call is now sent -- Fixed a bug where the CountlyBulk "start" callback reported an empty queue while data was still waiting for a retry or being sent -- Queued requests are now sent one after another without waiting for the next heartbeat, and all recorded events are packed into requests at each heartbeat (also in CountlyBulk) -- File storage now combines repeated writes of the same data, so a busy app no longer rewrites the whole queue file for every recorded item -- Memory storage no longer writes files when a fatal crash is recorded -- Requests that were running when "halt" was called no longer change the SDK's state afterwards -- Fixed a bug where calling "track_errors" repeatedly would register duplicate error handlers, causing a single error to be recorded multiple times -- Fixed a bug in CountlyBulk where explicitly provided "hour", "dow" and "tz" values of 0 were overridden with current time values in "add_request" and "add_bulk_request" -- SDK now removes its process and cluster event listeners when "halt" is called +## 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/lib/countly-bulk.js b/lib/countly-bulk.js index c482a65..16ce0f4 100644 --- a/lib/countly-bulk.js +++ b/lib/countly-bulk.js @@ -61,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; diff --git a/lib/countly.js b/lib/countly.js index a4cb726..c5a1197 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -36,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; diff --git a/package-lock.json b/package-lock.json index 45f36b2..4552ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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", diff --git a/package.json b/package.json index c7a1154..d584fea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "countly-sdk-nodejs", - "version": "24.10.4", + "version": "26.8.0", "description": "Countly NodeJS SDK", "main": "lib/countly.js", "files": [ From 0e527e2b33a3176d579fa9e314e04f2e787d677e Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:29:19 +0900 Subject: [PATCH 7/7] release tag --- .github/workflows/release_notice.yml | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml index ce5853c..f3b09fe 100644 --- a/.github/workflows/release_notice.yml +++ b/.github/workflows/release_notice.yml @@ -3,19 +3,6 @@ on: release: types: [published] workflow_dispatch: - inputs: - tag_name: - description: "Version to announce, for example 26.8.0" - type: string - required: false - html_url: - description: "Link to the GitHub release" - type: string - required: false - body: - description: "Release notes" - type: string - required: false permissions: contents: read jobs: @@ -36,10 +23,10 @@ jobs: payload: | { "repository": "${{ github.repository }}", - "tag_name": "${{ inputs.tag_name || github.event.release.tag_name }}", + "tag_name": "${{ github.event.release.tag_name }}", "actor": "${{ github.actor }}", - "body": ${{ toJSON(inputs.body || github.event.release.body) }}, - "html_url": "${{ inputs.html_url || github.event.release.html_url }}" + "body": ${{ toJSON(github.event.release.body) }}, + "html_url": "${{ github.event.release.html_url }}" } - name: Send custom JSON data to Discord if: ${{ !cancelled() }} @@ -47,8 +34,8 @@ jobs: with: webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} nodetail: true - title: New ${{ github.repository }} version ${{ inputs.tag_name || github.event.release.tag_name }} published by ${{ github.actor }} + title: New ${{ github.repository }} version ${{ github.event.release.tag_name }} published by ${{ github.actor }} description: | - Release URL: ${{ inputs.html_url || github.event.release.html_url }} + Release URL: ${{ github.event.release.html_url }} Click [here](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md) to view the change log. - `${{ inputs.body || github.event.release.body }}` + `${{ github.event.release.body }}`