diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9d5bb5f..000a60a 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -33,6 +33,9 @@ jobs: - name: Install dependencies run: "npm install" + - name: Run type checks + run: "npm run tsc" + - name: Run tests run: npm test env: diff --git a/.prettierrc.json b/.prettierrc.json index 0967ef4..757fd64 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1 +1,3 @@ -{} +{ + "trailingComma": "es5" +} diff --git a/jsconfig.json b/jsconfig.json index b67cd9d..ac7fa1f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { - "noImplicitAny": true, "checkJs": true, "target": "es2022", + "maxNodeModuleJsDepth": 0, "module": "NodeNext", - "moduleResolution": "nodenext" - } + "moduleResolution": "nodenext", + "strict": true + }, + "exclude": ["node_modules", "mdn-observatory-webext"] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3ee6312..015cdef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,13 @@ "@fastify/postgres": "^5.2.2", "@fastify/static": "^7.0.4", "@sentry/node": "^8.20.0", + "@types/chai": "^4.3.16", + "@types/convict": "^6.1.6", + "@types/ip": "^1.1.3", + "@types/jsdom": "^21.1.7", + "@types/mocha": "^10.0.7", + "@types/pg-format": "^1.0.5", + "@types/tough-cookie": "^4.0.5", "axios": "^1.7.2", "axios-cookiejar-support": "^5.0.2", "change-case": "^5.4.4", @@ -31,7 +38,8 @@ "postgrator": "^7.2.0", "postgrator-cli": "^8.1.0", "tldts": "^6.1.36", - "tough-cookie": "^4.1.4" + "tough-cookie": "^4.1.4", + "typescript": "^5.5.4" }, "devDependencies": { "@faker-js/faker": "^8.4.1", @@ -40,7 +48,8 @@ "chai": "^5.1.1", "json-schema-to-jsdoc": "^1.1.1", "mocha": "^10.7.0", - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "prettier-eslint": "^16.3.0" }, "engines": { "node": ">=20.0.0", @@ -67,6 +76,106 @@ "node": ">=12.17" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@faker-js/faker": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", @@ -211,6 +320,63 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -300,6 +466,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -308,6 +486,41 @@ "node": ">=8" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -800,6 +1013,12 @@ "node": ">=14.18" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@supercharge/promise-pool": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@supercharge/promise-pool/-/promise-pool-3.2.0.tgz", @@ -809,6 +1028,11 @@ "node": ">=8" } }, + "node_modules/@types/chai": { + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", + "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==" + }, "node_modules/@types/connect": { "version": "3.4.36", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", @@ -817,6 +1041,32 @@ "@types/node": "*" } }, + "node_modules/@types/convict": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", + "integrity": "sha512-1B6jqWHWQud+7yyWAqbxnPmzlHrrOtJzZr1DhhYJ/NbpS4irfZSnq+N5Fm76J9LNRlUZvCmYxTVhhohWRvtqHw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ip": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.3.tgz", + "integrity": "sha512-64waoJgkXFTYnCYDUWgSATJ/dXEBanVkaP5d4Sbk7P6U7cTTMhxVyROTckc6JKdwCrgnAjZMn0k3177aQxtDEA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/mocha": { "version": "10.0.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", @@ -849,6 +1099,11 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/pg-format": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/pg-format/-/pg-format-1.0.5.tgz", + "integrity": "sha512-i+oEEJEC+1I3XAhgqtVp45Faj8MBbV0Aoq4rHsHD7avgLjyDkaWKObd514g0Q/DOUkdxU0P4CQ0iq2KR4SoJcw==" + }, "node_modules/@types/pg-pool": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.4.tgz", @@ -862,6 +1117,135 @@ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -912,6 +1296,15 @@ "acorn": "^8" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -1017,6 +1410,15 @@ "node": ">=6" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1171,6 +1573,15 @@ "node": ">=10.16.0" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -1375,6 +1786,15 @@ "node": ">=18" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1499,6 +1919,12 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1524,6 +1950,36 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1571,9 +2027,207 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "engines": { "node": ">=6" @@ -1602,6 +2256,28 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, "node_modules/fast-json-stringify": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.15.0.tgz", @@ -1632,6 +2308,12 @@ } } }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-querystring": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", @@ -1713,6 +2395,18 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1774,6 +2468,26 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -1911,6 +2625,68 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2050,12 +2826,37 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-in-the-middle": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.10.0.tgz", @@ -2067,6 +2868,24 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2158,6 +2977,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -2262,6 +3090,12 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-pointer": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", @@ -2296,6 +3130,34 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/light-my-request": { "version": "5.13.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.13.0.tgz", @@ -2373,6 +3235,93 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/loupe": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", @@ -2393,6 +3342,28 @@ "node": ">=10" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -2497,6 +3468,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/nodemon": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", @@ -2665,6 +3642,23 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2706,6 +3700,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -2770,6 +3776,15 @@ "node": "14 || >=16.14" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -3039,6 +4054,91 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-eslint": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", + "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "prettier-plugin-svelte": "^3.0.0", + "svelte-eslint-parser": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3093,6 +4193,26 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -3107,6 +4227,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -3172,6 +4298,12 @@ "node": ">=8.6.0" } }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -3193,6 +4325,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ret": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", @@ -3215,11 +4356,93 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3356,6 +4579,15 @@ "node": ">=10" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/sonic-boom": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", @@ -3533,6 +4765,12 @@ "node": ">=12.17" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -3622,6 +4860,54 @@ "node": ">=18" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -3666,6 +4952,30 @@ "requires-port": "^1.0.0" } }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -3730,6 +5040,15 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrapjs": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", diff --git a/package.json b/package.json index 5d8a7a1..6e04b76 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test": "CONFIG_FILE=conf/config-test.json mocha", "test:nodb": "CONFIG_FILE=conf/config-test.json SKIP_DB_TESTS=1 mocha", "test:compare": "CONFIG_FILE=conf/config-test.json COMPARE_RESULT_TESTS=1 mocha test/compare-results.test.js", + "tsc": "tsc -p jsconfig.json", "scan": "node src/cli/index.js", "updateHsts": "node src/retrieve-hsts.js", "refreshMaterializedViews": "node src/maintenance/index.js", @@ -25,10 +26,18 @@ "@faker-js/faker": "^8.4.1", "@supercharge/promise-pool": "^3.2.0", "@types/mocha": "^10.0.7", + "@types/chai": "^4.3.16", + "@types/convict": "^6.1.6", + "@types/ip": "^1.1.3", + "@types/jsdom": "^21.1.7", + "@types/pg-format": "^1.0.5", + "@types/tough-cookie": "^4.0.5", "chai": "^5.1.1", "json-schema-to-jsdoc": "^1.1.1", "mocha": "^10.7.0", - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "prettier-eslint": "^16.3.0", + "typescript": "^5.5.4" }, "dependencies": { "@fastify/cors": "^9.0.1", diff --git a/src/analyzer/cspParser.js b/src/analyzer/cspParser.js index 01cbe4d..8272d33 100644 --- a/src/analyzer/cspParser.js +++ b/src/analyzer/cspParser.js @@ -29,14 +29,10 @@ export function parseCspMeta(cspList) { * @returns {Map>} */ export function parseCsp(cspList) { - const cleanCspList = cspList.flatMap((scpString) => - scpString - ? scpString - .split(",") // NodeJS joins multiple headers with a comma - .map((scpString) => scpString.replace(/[\r\n]/g, "").trim()) - : [""] + const cleanCspList = cspList.map((cspString) => + cspString.replaceAll(/[\r\n]/g, "").trim() ); - if (!cleanCspList) { + if (cleanCspList.length === 0) { return new Map(); } diff --git a/src/analyzer/tests/cookies.js b/src/analyzer/tests/cookies.js index 21a6534..f0a28d5 100644 --- a/src/analyzer/tests/cookies.js +++ b/src/analyzer/tests/cookies.js @@ -1,6 +1,7 @@ +import { SET_COOKIE } from "../../headers.js"; import { Requests, BaseOutput } from "../../types.js"; import { Expectation } from "../../types.js"; -import { onlyIfWorse } from "../utils.js"; +import { getHttpHeaders, onlyIfWorse } from "../utils.js"; import { strictTransportSecurityTest } from "./strict-transport-security.js"; import { Cookie } from "tough-cookie"; @@ -25,7 +26,7 @@ const COOKIES_TO_DELETE = ["heroku-session-affinity"]; export class CookiesOutput extends BaseOutput { /** @type {CookieMap | null} */ - data; + data = null; // Store whether or not we saw SameSite cookies, if cookies were set /** @type {boolean | null} */ sameSite = null; @@ -77,25 +78,27 @@ export function cookiesTest( let hasMissingSameSite = false; // Check if we got a malformed SameSite on the raw headers - const rawCookies = requests.responses.auto.headers["set-cookie"]; - if (rawCookies) { - for (const rawCookie of rawCookies) { - if (containsInvalidSameSiteCookie(rawCookie)) { - output.result = onlyIfWorse( - Expectation.CookiesSamesiteFlagInvalid, - output.result, - goodness - ); + if (requests.responses.auto) { + const rawCookies = getHttpHeaders(requests.responses.auto, SET_COOKIE); + if (rawCookies) { + for (const rawCookie of rawCookies) { + if (containsInvalidSameSiteCookie(rawCookie)) { + output.result = onlyIfWorse( + Expectation.CookiesSamesiteFlagInvalid, + output.result, + goodness + ); + } } } } // get ALL the cookies from the store with serializeSync instead of using getCookiesSync - const allCookies = requests.session.jar + const allCookies = requests.session?.jar .serializeSync() .cookies.filter(filterCookies); - if (!allCookies.length) { + if (!allCookies?.length) { output.result = Expectation.CookiesNotFound; output.data = null; } else { @@ -233,7 +236,7 @@ function containsInvalidSameSiteCookie(cookieString) { } /** - * @param {Cookie} cookie + * @param {Cookie.Serialized} cookie */ function filterCookies(cookie) { return !COOKIES_TO_DELETE.includes(cookie.key); diff --git a/src/analyzer/tests/cors.js b/src/analyzer/tests/cors.js index 862bf05..4c132d5 100644 --- a/src/analyzer/tests/cors.js +++ b/src/analyzer/tests/cors.js @@ -1,5 +1,11 @@ +import { + ACCESS_CONTROL_ALLOW_CREDENTIALS, + ACCESS_CONTROL_ALLOW_ORIGIN, + ORIGIN, +} from "../../headers.js"; import { BaseOutput, Requests } from "../../types.js"; import { Expectation } from "../../types.js"; +import { getFirstHttpHeader, getHttpHeaders } from "../utils.js"; export class CorsOutput extends BaseOutput { /** @type {string | null} */ @@ -34,29 +40,32 @@ export function crossOriginResourceSharingTest( ) { const output = new CorsOutput(expectation); output.result = Expectation.CrossOriginResourceSharingNotImplemented; - const resp = requests.responses.auto; const accessControlAllowOrigin = requests.responses.cors; - if ( - accessControlAllowOrigin && - accessControlAllowOrigin.headers["access-control-allow-origin"] - ) { - output.data = accessControlAllowOrigin.headers[ - "access-control-allow-origin" - ] - .slice(0, 256) - .trim() - .toLowerCase(); + const acaoHeader = getFirstHttpHeader( + accessControlAllowOrigin, + ACCESS_CONTROL_ALLOW_ORIGIN + ); + const originHeader = getFirstHttpHeader( + accessControlAllowOrigin?.request, + ORIGIN + ); + const credentialsHeader = getFirstHttpHeader( + accessControlAllowOrigin, + ACCESS_CONTROL_ALLOW_CREDENTIALS + ); + + if (accessControlAllowOrigin && acaoHeader) { + output.data = acaoHeader.slice(0, 256).trim().toLowerCase(); if (output.data === "*") { output.result = Expectation.CrossOriginResourceSharingImplementedWithPublicAccess; } else if ( - accessControlAllowOrigin.request?.headers?.["origin"] === - accessControlAllowOrigin.headers["access-control-allow-origin"] && - accessControlAllowOrigin.headers["access-control-allow-credentials"] && - accessControlAllowOrigin.headers["access-control-allow-credentials"] - .toLowerCase() - .trim() === "true" + originHeader && + acaoHeader && + originHeader === acaoHeader && + credentialsHeader && + credentialsHeader.toLowerCase().trim() === "true" ) { output.result = Expectation.CrossOriginResourceSharingImplementedWithUniversalAccess; diff --git a/src/analyzer/tests/cross-origin-resource-policy.js b/src/analyzer/tests/cross-origin-resource-policy.js index ba7ea5a..f02e495 100644 --- a/src/analyzer/tests/cross-origin-resource-policy.js +++ b/src/analyzer/tests/cross-origin-resource-policy.js @@ -1,5 +1,7 @@ +import { CROSS_ORIGIN_RESOURCE_POLICY } from "../../headers.js"; import { BaseOutput, Requests } from "../../types.js"; import { Expectation } from "../../types.js"; +import { getFirstHttpHeader } from "../utils.js"; export class CrossOriginResourcePolicyOutput extends BaseOutput { /** @type {string | null} */ @@ -36,24 +38,33 @@ export function crossOriginResourcePolicyTest( expectation = Expectation.CrossOriginResourcePolicyImplementedWithSameSite ) { const output = new CrossOriginResourcePolicyOutput(expectation); + output.result = Expectation.CrossOriginResourcePolicyNotImplemented; + const resp = requests.responses.auto; + if (!resp) { + return output; + } - output.result = Expectation.CrossOriginResourcePolicyNotImplemented; + const httpHeader = getFirstHttpHeader(resp, CROSS_ORIGIN_RESOURCE_POLICY); + const equivHeaders = + resp.httpEquiv?.get(CROSS_ORIGIN_RESOURCE_POLICY) ?? null; // Store whether the header or the meta tag were present - output.http = !!resp.headers["cross-origin-resource-policy"]; - output.meta = !!resp.httpEquiv?.get("cross-origin-resource-policy"); + output.http = !!httpHeader; + output.meta = equivHeaders ? equivHeaders.length > 0 : false; // If it is both a header and a http-equiv, http-equiv has precedence (last value) let corpHeader; - if (output.http) { - corpHeader = resp.headers["cross-origin-resource-policy"] - .slice(0, 256) - .trim() - .toLowerCase(); + if (output.http && httpHeader) { + corpHeader = httpHeader.slice(0, 256).trim().toLowerCase(); } else if (output.meta) { - const headers = resp.httpEquiv.get("cross-origin-resource-policy"); - corpHeader = headers[headers.length - 1].slice(0, 256).trim().toLowerCase(); + // const headers = resp.httpEquiv?.get("cross-origin-resource-policy"); + if (equivHeaders && equivHeaders.length) { + corpHeader = equivHeaders[equivHeaders.length - 1] + .slice(0, 256) + .trim() + .toLowerCase(); + } } if (corpHeader) { @@ -79,7 +90,7 @@ export function crossOriginResourcePolicyTest( Expectation.CrossOriginResourcePolicyImplementedWithSameSite, Expectation.CrossOriginResourcePolicyImplementedWithSameOrigin, Expectation.CrossOriginResourcePolicyImplementedWithCrossOrigin, - ].includes(output.result); + ].includes(output.result ?? ""); return output; } diff --git a/src/analyzer/tests/csp.js b/src/analyzer/tests/csp.js index b908cbc..619f91a 100644 --- a/src/analyzer/tests/csp.js +++ b/src/analyzer/tests/csp.js @@ -5,6 +5,7 @@ import { import { Requests, Policy, BaseOutput } from "../../types.js"; import { Expectation } from "../../types.js"; import { parseCsp, parseCspMeta } from "../cspParser.js"; +import { getHttpHeaders } from "../utils.js"; const DANGEROUSLY_BROAD = new Set([ "ftp:", @@ -83,11 +84,17 @@ export function contentSecurityPolicyTest( const output = new CspOutput(expectation); const response = requests.responses.auto; const url = requests.session?.url; - // @ts-ignore - const httpCspHeader = response.headers.get(CONTENT_SECURITY_POLICY) ?? null; - const equivCspHeader = response.httpEquiv.get(CONTENT_SECURITY_POLICY) ?? []; - output.numPolicies = - equivCspHeader.length + (httpCspHeader?.split(",").length || 0); + + if (!response || !url) { + output.result = Expectation.CspNotImplemented; + return output; + } + + const httpCspHeader = getHttpHeaders(response, CONTENT_SECURITY_POLICY); + const equivCspHeader = + response?.httpEquiv?.get(CONTENT_SECURITY_POLICY) ?? []; + + output.numPolicies = equivCspHeader.length + httpCspHeader.length; /** @type {Map>} */ let csp; @@ -98,7 +105,7 @@ export function contentSecurityPolicyTest( try { csp = parseCsp( - [httpCspHeader, ...equivCspHeader].filter((x) => x !== null) + [...httpCspHeader, ...equivCspHeader].filter((x) => x !== null) ); } catch (e) { output.result = Expectation.CspHeaderInvalid; @@ -106,7 +113,7 @@ export function contentSecurityPolicyTest( } try { - httpHeaderOnlyCsp = parseCsp([httpCspHeader]); + httpHeaderOnlyCsp = parseCsp(httpCspHeader); } catch (e) { httpHeaderOnlyCsp = new Map(); } diff --git a/src/analyzer/tests/referrer-policy.js b/src/analyzer/tests/referrer-policy.js index adcab56..1243701 100644 --- a/src/analyzer/tests/referrer-policy.js +++ b/src/analyzer/tests/referrer-policy.js @@ -1,5 +1,8 @@ +import { REFERRER_POLICY } from "../../headers.js"; import { Requests, BaseOutput } from "../../types.js"; import { Expectation } from "../../types.js"; +import { getFirstHttpHeader, getHttpHeaders } from "../utils.js"; + export class ReferrerOutput extends BaseOutput { /** @type {string | null} */ data = null; @@ -49,21 +52,21 @@ export function referrerPolicyTest( const valid = goodness.concat(badness); const response = requests.responses.auto; + if (!response) { + output.result = Expectation.ReferrerPolicyNotImplemented; + return output; + } + + const httpHeaders = getHttpHeaders(response, REFERRER_POLICY); + const equivHeaders = response.httpEquiv?.get(REFERRER_POLICY) ?? []; // Store whether the header or the meta tag were present - output.http = !!response.headers["referrer-policy"]; - output.meta = !!response.httpEquiv?.get("referrer-policy"); + output.http = httpHeaders.length > 0; + output.meta = equivHeaders ? equivHeaders?.length > 0 : false; // If it is both a header and a http-equiv, http-equiv has precedence (last value) - if (output.http && output.meta) { - output.data = [ - response.headers["referrer-policy"], - response.httpEquiv?.get("referrer-policy")?.join(", "), - ].join(", "); - } else if (output.http) { - output.data = response.headers["referrer-policy"]; - } else if (output.meta) { - output.data = response.httpEquiv.get("referrer-policy").join(","); + if (output.http || output.meta) { + output.data = [...httpHeaders, ...equivHeaders].join(", "); } else { output.result = Expectation.ReferrerPolicyNotImplemented; output.pass = true; @@ -71,12 +74,13 @@ export function referrerPolicyTest( } // Find the last known valid policy value in the referrer policy - let policy = output.data - .split(",") - .filter((e) => valid.includes(e.toLowerCase().trim())) - .reverse()[0] - ?.toLowerCase() - .trim(); + let policy = + output.data + ?.split(",") + .filter((e) => valid.includes(e.toLowerCase().trim())) + .reverse()[0] + ?.toLowerCase() + .trim() ?? ""; if (goodness.includes(policy)) { output.result = Expectation.ReferrerPolicyPrivate; diff --git a/src/analyzer/tests/strict-transport-security.js b/src/analyzer/tests/strict-transport-security.js index e2e74c3..ad263e4 100644 --- a/src/analyzer/tests/strict-transport-security.js +++ b/src/analyzer/tests/strict-transport-security.js @@ -1,6 +1,8 @@ +import { STRICT_TRANSPORT_SECURITY } from "../../headers.js"; import { Requests, BaseOutput } from "../../types.js"; import { Expectation } from "../../types.js"; import { isHstsPreloaded } from "../hsts.js"; +import { getHttpHeaders } from "../utils.js"; export class StrictTransportSecurityOutput extends BaseOutput { /** @type {string | null} */ data = null; @@ -51,8 +53,9 @@ export function strictTransportSecurityTest( } else if (!response.verified) { // Also need a valid certificate chain for HSTS output.result = Expectation.HstsInvalidCert; - } else if (response.headers["strict-transport-security"]) { - output.data = response.headers["strict-transport-security"].slice(0, 1024); // code against malicious headers + } else if (getHttpHeaders(response, STRICT_TRANSPORT_SECURITY).length > 0) { + const header = getHttpHeaders(response, STRICT_TRANSPORT_SECURITY)[0]; + output.data = header.slice(0, 1024); // code against malicious headers try { let sts = output.data.split(";").map((i) => i.trim().toLowerCase()); diff --git a/src/analyzer/tests/subresource-integrity.js b/src/analyzer/tests/subresource-integrity.js index 9182734..9a70d8c 100644 --- a/src/analyzer/tests/subresource-integrity.js +++ b/src/analyzer/tests/subresource-integrity.js @@ -2,7 +2,8 @@ import { BaseOutput, HTML_TYPES, Requests } from "../../types.js"; import { Expectation } from "../../types.js"; import { JSDOM } from "jsdom"; import { parse } from "tldts"; -import { onlyIfWorse } from "../utils.js"; +import { getFirstHttpHeader, onlyIfWorse } from "../utils.js"; +import { CONTENT_TYPE } from "../../headers.js"; export class SubresourceIntegrityOutput extends BaseOutput { /** @type {import("../../types.js").ScriptMap} */ @@ -49,8 +50,15 @@ export function subresourceIntegrityTest( Expectation.SriNotImplementedAndExternalScriptsNotLoadedSecurely, Expectation.SriNotImplementedResponseNotHtml, ]; + const resp = requests.responses.auto; - const mime = resp.headers["content-type"]?.split(";")[0]; + + if (!resp) { + output.result = Expectation.SriNotImplementedButNoScriptsLoaded; + return output; + } + + const mime = (getFirstHttpHeader(resp, CONTENT_TYPE) ?? "").split(";")[0]; if (!HTML_TYPES.has(mime)) { // If the content isn't HTML, there's no scripts to load; this is okay output.result = Expectation.SriNotImplementedResponseNotHtml; @@ -58,7 +66,7 @@ export function subresourceIntegrityTest( // Try to parse the HTML let dom; try { - dom = new JSDOM(requests.resources.path); + dom = new JSDOM(requests.resources.path || ""); } catch (e) { // severe parser error output.result = Expectation.HtmlNotParseable; @@ -111,7 +119,7 @@ export function subresourceIntegrityTest( let secureScheme = false; if ( scheme === "https:" || - (relativeOrigin && requests.session.url.protocol === "https:") + (relativeOrigin && requests.session?.url.protocol === "https:") ) { secureScheme = true; } @@ -157,21 +165,19 @@ export function subresourceIntegrityTest( if (scripts.length === 0) { output.result = Expectation.SriNotImplementedButNoScriptsLoaded; - } else if ( - scripts.length > 0 && - !scriptsOnForeignOrigin && - !output.result - ) { - output.result = - Expectation.SriNotImplementedButAllScriptsLoadedFromSecureOrigin; - } else if (scripts.length > 0 && scriptsOnForeignOrigin && !output.result) { - output.result = onlyIfWorse( - Expectation.SriImplementedAndExternalScriptsLoadedSecurely, - output.result, - goodness - ); + } else { + if (!output.result) { + if (scriptsOnForeignOrigin) { + output.result = + Expectation.SriImplementedAndExternalScriptsLoadedSecurely; + } else { + output.result = + Expectation.SriNotImplementedButAllScriptsLoadedFromSecureOrigin; + } + } } } + // Code defensively on the size of the data output.data = JSON.stringify(output.data).length < 32768 ? output.data : {}; // Check to see if the test passed or failed diff --git a/src/analyzer/tests/x-content-type-options.js b/src/analyzer/tests/x-content-type-options.js index 080b2a2..3aa5f02 100644 --- a/src/analyzer/tests/x-content-type-options.js +++ b/src/analyzer/tests/x-content-type-options.js @@ -1,5 +1,7 @@ +import { X_CONTENT_TYPE_OPTIONS } from "../../headers.js"; import { BaseOutput, Requests } from "../../types.js"; import { Expectation } from "../../types.js"; +import { getFirstHttpHeader } from "../utils.js"; export class XContentTypeOptionsOutput extends BaseOutput { /** @type {string | null} */ @@ -34,8 +36,15 @@ export function xContentTypeOptionsTest( const output = new XContentTypeOptionsOutput(expectation); const resp = requests.responses.auto; - if (resp.headers["x-content-type-options"]) { - output.data = resp.headers["x-content-type-options"].slice(0, 256); + if (!resp) { + output.result = Expectation.XContentTypeOptionsNotImplemented; + return output; + } + + const header = getFirstHttpHeader(resp, X_CONTENT_TYPE_OPTIONS); + + if (header) { + output.data = header.slice(0, 256); if (output.data.trim().toLowerCase() === "nosniff") { output.result = Expectation.XContentTypeOptionsNosniff; } else { diff --git a/src/analyzer/tests/x-frame-options.js b/src/analyzer/tests/x-frame-options.js index f45480b..4650410 100644 --- a/src/analyzer/tests/x-frame-options.js +++ b/src/analyzer/tests/x-frame-options.js @@ -1,5 +1,7 @@ +import { X_FRAME_OPTIONS } from "../../headers.js"; import { BaseOutput, Requests } from "../../types.js"; import { Expectation } from "../../types.js"; +import { getFirstHttpHeader } from "../utils.js"; import { contentSecurityPolicyTest } from "./csp.js"; export class XFrameOptionsOutput extends BaseOutput { @@ -37,8 +39,15 @@ export function xFrameOptionsTest( const output = new XFrameOptionsOutput(expectation); const resp = requests.responses.auto; - if (resp.headers["x-frame-options"]) { - output.data = resp.headers["x-frame-options"].slice(0, 1024); + if (!resp) { + output.result = Expectation.XFrameOptionsNotImplemented; + return output; + } + + const header = getFirstHttpHeader(resp, X_FRAME_OPTIONS); + + if (header) { + output.data = header.slice(0, 1024); const xfo = output.data.trim().toLowerCase(); if (["deny", "sameorigin"].includes(xfo)) { output.result = Expectation.XFrameOptionsSameoriginOrDeny; @@ -58,7 +67,6 @@ export function xFrameOptionsTest( } // Check to see if the test passed or failed - if ( [ Expectation.XFrameOptionsAllowFromOrigin, diff --git a/src/analyzer/utils.js b/src/analyzer/utils.js index 93f0a80..96fd444 100644 --- a/src/analyzer/utils.js +++ b/src/analyzer/utils.js @@ -3,7 +3,7 @@ import { Expectation } from "../types.js"; /** * Return the new result if it's worse than the existing result, otherwise just the current result. * @param {Expectation} newResult - The new result to compare. - * @param {Expectation} oldResult - The existing result to compare against. + * @param {Expectation | null} oldResult - The existing result to compare against. * @param {Expectation[]} order - An array defining the order of results from best to worst. * @returns {Expectation} - The worse of the two results. */ @@ -16,3 +16,38 @@ export function onlyIfWorse(newResult, oldResult, order) { return oldResult; } } + +/** + * @param {import("../types.js").Response | null} response + * @param {string} name + * @returns {string[]} + */ +export function getHttpHeaders(response, name) { + if (!response) { + return []; + } + const axiosHeaders = response.headers; + if (!axiosHeaders) { + return []; + } + const lcName = name.toLowerCase(); + const headers = Object.entries(axiosHeaders) + .filter(([headerName, _value]) => { + return headerName.toLowerCase() === lcName; + }) + .map(([_headerName, value]) => value) + .flat(); + return headers; +} + +/** + * @param {import("../types.js").Response | null} response + * @param {string} name + * @returns {string | null} + */ +export function getFirstHttpHeader(response, name) { + if (!response) { + return null; + } + return getHttpHeaders(response, name)[0] ?? null; +} diff --git a/src/api/v2/analyze/index.js b/src/api/v2/analyze/index.js index caedf39..0d10043 100644 --- a/src/api/v2/analyze/index.js +++ b/src/api/v2/analyze/index.js @@ -81,8 +81,19 @@ async function executeScan(pool, hostname) { try { scanResult = await scan(hostname); } catch (e) { - await updateScanState(pool, scanId, ScanState.FAILED, e.message); - throw new ScanFailedError(e); + if (e instanceof Error) { + await updateScanState(pool, scanId, ScanState.FAILED, e.message); + throw new ScanFailedError(e); + } else { + const unknownError = new Error("Unknown error occurred"); + await updateScanState( + pool, + scanId, + ScanState.FAILED, + unknownError.message + ); + throw new ScanFailedError(unknownError); + } } scanRow = await insertTestResults(pool, siteId, scanId, scanResult); return scanRow; diff --git a/src/api/v2/schemas.js b/src/api/v2/schemas.js index 4395c15..e798a9a 100644 --- a/src/api/v2/schemas.js +++ b/src/api/v2/schemas.js @@ -245,6 +245,7 @@ export class PolicyResponse { }; /** @typedef {PolicyItem} */ strictDynamic = { + /** @type {boolean | null} */ pass: false, description: `

Uses CSP3's 'strict-dynamic' directive to allow dynamic script loading (optional)

`, info: `

'strict-dynamic' lets you use a JavaScript shim loader to load all your site's JavaScript dynamically, without having to track script-src origins.

`, diff --git a/src/api/v2/utils.js b/src/api/v2/utils.js index bd37e8b..a263fd2 100644 --- a/src/api/v2/utils.js +++ b/src/api/v2/utils.js @@ -69,14 +69,16 @@ export async function validHostname(hostname) { } // Check if we can look up the host - await new Promise((resolve, reject) => { - dns.lookup(hostname, (err, address, family) => { - if (err) { - reject(new InvalidHostNameLookupError(hostname)); - } - resolve(); - }); - }); + await /** @type {Promise} */ ( + new Promise((resolve, reject) => { + dns.lookup(hostname, (err, address, family) => { + if (err) { + reject(new InvalidHostNameLookupError(hostname)); + } + resolve(); + }); + }) + ); return hostname; } @@ -134,7 +136,6 @@ export async function testsForScan(pool, scanId) { const key = snakeCase(k); value[key] = v; } - delete test.output; acc[test.name] = value; return acc; }, /** @type {any} */ ({})); diff --git a/src/cli/index.js b/src/cli/index.js index 424d0fc..6bd735b 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -16,7 +16,9 @@ program const result = await scan(hostname); console.log(JSON.stringify(result, null, 2)); } catch (e) { - console.log(JSON.stringify({ error: e.message })); + if (e instanceof Error) { + console.log(JSON.stringify({ error: e.message })); + } } }); diff --git a/src/config.js b/src/config.js index 9fc8ddb..06e37bc 100644 --- a/src/config.js +++ b/src/config.js @@ -117,13 +117,13 @@ const SCHEMA = { format: "String", default: "", env: "SENTRY_DSN", - } - } + }, + }, }; /** * - * @param {string} configFile + * @param {string | undefined} configFile * @returns */ export function load(configFile) { diff --git a/src/database/migrate.js b/src/database/migrate.js index fadf228..e6c02d2 100644 --- a/src/database/migrate.js +++ b/src/database/migrate.js @@ -25,6 +25,9 @@ export async function migrateDatabase(version, pool) { if (owned_pool) { pool = createPool(); } + if (!pool) { + throw new Error("Pool is invalid"); + } try { const postgrator = new Postgrator({ diff --git a/src/database/repository.js b/src/database/repository.js index eeb73d9..c44f439 100644 --- a/src/database/repository.js +++ b/src/database/repository.js @@ -92,6 +92,7 @@ export async function insertTestResults(pool, siteId, scanId, scanResult) { // prepare our test data, remove all standard fields from test and lift it to the top level, // encode the rest for the JSON data field. const testValues = Object.entries(scanResult.tests).map(([name, test]) => { + /** @type {any} */ const t = { ...test }; const expectation = t.expectation; delete t.expectation; @@ -343,7 +344,7 @@ export async function selectTestResults(pool, scanId) { * @param {Pool} pool * @param {number} scanId * @param {ScanState} state - * @param {string} error + * @param {string | null} error */ export async function updateScanState(pool, scanId, state, error = null) { if (error) { diff --git a/src/grader/grader.js b/src/grader/grader.js index 989babb..fba0052 100644 --- a/src/grader/grader.js +++ b/src/grader/grader.js @@ -16,7 +16,12 @@ export function getGradeForScore(score) { score = Math.max(score, 0); // If score>100, just use the grade for 100, otherwise round down to the nearest multiple of 5 - const grade = GRADE_CHART.get(Math.min(score - (score % 5), 100)); + const scoreMapKey = Math.min(score - (score % 5), 100); + const grade = GRADE_CHART.get(scoreMapKey); + + if (!grade) { + throw new Error(`Score of ${scoreMapKey} did not map to a grade`); + } return { score, @@ -29,7 +34,7 @@ export function getGradeForScore(score) { * @returns {string} */ export function getScoreDescription(expectation) { - return SCORE_TABLE.get(expectation)?.description; + return SCORE_TABLE.get(expectation)?.description ?? ""; } /** @@ -37,7 +42,7 @@ export function getScoreDescription(expectation) { * @returns {string} */ export function getRecommendation(expectation) { - return SCORE_TABLE.get(expectation)?.recommendation; + return SCORE_TABLE.get(expectation)?.recommendation ?? ""; } /** @@ -45,7 +50,7 @@ export function getRecommendation(expectation) { * @returns {string} */ export function getTopicLink(testName) { - return TEST_TOPIC_LINKS.get(testName); + return TEST_TOPIC_LINKS.get(testName) ?? ""; } /** @@ -53,7 +58,7 @@ export function getTopicLink(testName) { * @returns {number} */ export function getScoreModifier(expectation) { - return SCORE_TABLE.get(expectation).modifier; + return SCORE_TABLE.get(expectation)?.modifier ?? 0; } // diff --git a/src/headers.js b/src/headers.js index b192629..d9da36b 100644 --- a/src/headers.js +++ b/src/headers.js @@ -1,4 +1,14 @@ -export const CONTENT_SECURITY_POLICY = "Content-Security-Policy".toLowerCase(); +export const CONTENT_SECURITY_POLICY = "content-security-policy"; export const CONTENT_SECURITY_POLICY_REPORT_ONLY = - "Content-Security-Policy-Report-Only".toLowerCase(); -export const REFERRER_POLICY = "Referrer-Policy".toLowerCase(); + "content-security-policy-report-only"; +export const REFERRER_POLICY = "referrer-policy"; +export const SET_COOKIE = "set-cookie"; +export const ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin"; +export const ORIGIN = "origin"; +export const ACCESS_CONTROL_ALLOW_CREDENTIALS = + "access-control-allow-credentials"; +export const CROSS_ORIGIN_RESOURCE_POLICY = "cross-origin-resource-policy"; +export const STRICT_TRANSPORT_SECURITY = "strict-transport-security"; +export const CONTENT_TYPE = "content-type"; +export const X_CONTENT_TYPE_OPTIONS = "x-content-type-options"; +export const X_FRAME_OPTIONS = "x-frame-options"; diff --git a/src/retriever/retriever.js b/src/retriever/retriever.js index 52eeb2a..7bb5553 100644 --- a/src/retriever/retriever.js +++ b/src/retriever/retriever.js @@ -64,7 +64,7 @@ export async function retrieve(hostname, options = {}) { if (cors_resp) { retrievals.responses.cors = { ...cors_resp, - verified: retrievals.session.response.verified, + verified: retrievals.session.response?.verified ?? false, }; } else { retrievals.responses.cors = null; diff --git a/src/retriever/session.js b/src/retriever/session.js index a840dcb..054b0f4 100644 --- a/src/retriever/session.js +++ b/src/retriever/session.js @@ -46,12 +46,12 @@ export class Session { /** @type {URL} */ url; - /** @type {import("axios").AxiosInstance} */ + /** @type {import("axios").AxiosInstance | null} */ clientInstanceRecordingRedirects; - /** @type {import("axios").AxiosInstance} */ + /** @type {import("axios").AxiosInstance | null} */ clientInstance; - /** @type {import("../types.js").HttpResponse} | null */ - response; + /** @type {import("../types.js").HttpResponse | null} */ + response = null; /** @type {import("../types.js").RedirectEntry[]} */ redirectHistory; /** @type {number} */ @@ -92,7 +92,9 @@ export class Session { // Add an interceptor to record and request all redirections we encounter const ic = this.createInterceptor(); - axiosInstance.interceptors.response.use(ic.response, ic.error); + if (ic.response) { + axiosInstance.interceptors.response.use(ic.response, ic.error); + } this.clientInstanceRecordingRedirects = axiosInstance; // used for additional resourece requests, without recording redirects @@ -109,19 +111,13 @@ export class Session { /** @type { import("axios").AxiosResponse} */ response ) { // push our url to the redirection chain - const url = response.config.url; - // console.log("INTERCEPTOR PUSHING URL", url); + const url = response.config.url ?? that.url; + that.redirectHistory.push({ url: new URL(url), status: response.status, }); - // console.log( - // "INTERCEPTOR CONDITION", - // that.redirectCount, - // response.status, - // REDIRECT_STATUS_CODES.includes(response.status), - // response.headers - // ); + if ( that.redirectCount < MAX_REDIRECTS && response.status && @@ -132,7 +128,9 @@ export class Session { const redirectUrl = response.headers.location; const newUrl = new URL(redirectUrl, url); that.redirectCount++; - // console.log("INTERCEPTOR NEW URL", newUrl.href); + if (!that.clientInstanceRecordingRedirects) { + throw new Error("clientInstanceRecordingRedirects is null"); + } return that.clientInstanceRecordingRedirects.get(newUrl.href, { timeout: CLIENT_TIMEOUT, }); @@ -166,9 +164,16 @@ export class Session { } catch (e) { // Check for a cert error and replace the httpsAgent with // a non-verifying one + let code; + if (e && typeof e === "object" && "code" in e) { + const code = e.code; + } else { + code = null; + } + if ( - e.code && - CERT_ERROR_CODES.indexOf(e.code) !== -1 && + code && + CERT_ERROR_CODES.indexOf(code) !== -1 && this.clientInstanceRecordingRedirects.defaults.httpsAgent.options .rejectUnauthorized ) { @@ -188,6 +193,9 @@ export class Session { ic.response, ic.error ); + if (!this.clientInstance) { + throw new Error("clientInstance is null"); + } this.clientInstance = axios.create({ ...this.clientInstance.defaults, signal: AbortSignal.timeout(ABORT_TIMEOUT), @@ -201,9 +209,6 @@ export class Session { await this.init(); return this; } - // console.error( - // `Testing connection to ${this.url.href} failed with: ${e.code}` - // ); this.clientInstance = null; this.clientInstanceRecordingRedirects = null; this.response = null; diff --git a/src/retriever/utils.js b/src/retriever/utils.js index 74761c5..fcd5e56 100644 --- a/src/retriever/utils.js +++ b/src/retriever/utils.js @@ -19,15 +19,17 @@ export function parseHttpEquivHeaders(html, baseUrl) { if (meta.hasAttribute("http-equiv") && meta.hasAttribute("content")) { const httpEquiv = meta.getAttribute("http-equiv")?.toLowerCase().trim(); const content = meta.getAttribute("content"); - if (httpEquiv === CONTENT_SECURITY_POLICY) { - httpEquivHeaders.get(CONTENT_SECURITY_POLICY).push(content); + if (content && httpEquiv === CONTENT_SECURITY_POLICY) { + httpEquivHeaders.get(CONTENT_SECURITY_POLICY)?.push(content); } } else if ( // Technically not HTTP Equiv, but we're treating it that way - meta.getAttribute("name")?.toLowerCase().trim() === "referrer" && - meta.hasAttribute("content") + meta.getAttribute("name")?.toLowerCase().trim() === "referrer" ) { - httpEquivHeaders.set(REFERRER_POLICY, [meta.getAttribute("content")]); + const attr = meta.getAttribute("content"); + if (attr) { + httpEquivHeaders.set(REFERRER_POLICY, [attr]); + } } } } catch (e) { diff --git a/src/scanner/index.js b/src/scanner/index.js index 2518866..5cdcf4d 100644 --- a/src/scanner/index.js +++ b/src/scanner/index.js @@ -39,7 +39,7 @@ export async function scan(hostname, options) { // Run all the tests on the result /** @type {Output[]} */ const results = ALL_TESTS.map((test) => { - return test.apply(this, [r]); + return test(r); }); /** @type {StringMap} */ @@ -57,12 +57,14 @@ export async function scan(hostname, options) { let uncurvedScore = scoreWithExtraCredit; results.forEach((result) => { - result.scoreDescription = getScoreDescription(result.result); - result.scoreModifier = getScoreModifier(result.result); - testsPassed += result.pass ? 1 : 0; - scoreWithExtraCredit += result.scoreModifier; - if (result.scoreModifier < 0) { - uncurvedScore += result.scoreModifier; + if (result.result) { + result.scoreDescription = getScoreDescription(result.result); + result.scoreModifier = getScoreModifier(result.result); + testsPassed += result.pass ? 1 : 0; + scoreWithExtraCredit += result.scoreModifier; + if (result.scoreModifier < 0) { + uncurvedScore += result.scoreModifier; + } } }); diff --git a/src/types.js b/src/types.js index 8aebdae..0d9b4b9 100644 --- a/src/types.js +++ b/src/types.js @@ -230,7 +230,7 @@ export const HTML_TYPES = new Set(["text/html", "application/xhtml+xml"]); */ /** - * @typedef {{[key: string]: {crossorigin: string, integrity: string}}} ScriptMap + * @typedef {{[key: string]: {crossorigin: string | null, integrity: string | null}}} ScriptMap */ export class Policy { diff --git a/test/analyzer-utils.test.js b/test/analyzer-utils.test.js new file mode 100644 index 0000000..2efb194 --- /dev/null +++ b/test/analyzer-utils.test.js @@ -0,0 +1,76 @@ +import { assert } from "chai"; +import { AxiosHeaders } from "axios"; +import { getFirstHttpHeader, getHttpHeaders } from "../src/analyzer/utils.js"; + +function emptyResponse() { + return { + headers: new AxiosHeaders("Content-Type: text/html"), + request: { + headers: new AxiosHeaders(), + }, + status: 200, + statusText: "OK", + verified: true, + data: "", + config: { + headers: new AxiosHeaders(), + }, + }; +} + +describe("getHttpHeaders", () => { + it("gets all http headers for a header name", function () { + const response = emptyResponse(); + let headers = getHttpHeaders(response, "content-type"); + assert.isArray(headers); + assert.lengthOf(headers, 1); + assert.equal(headers[0], "text/html"); + headers = getHttpHeaders(response, "Content-Type"); + assert.isArray(headers); + assert.lengthOf(headers, 1); + assert.equal(headers[0], "text/html"); + headers = getHttpHeaders(response, "Non-Existing"); + assert.isArray(headers); + assert.lengthOf(headers, 0); + }); + + it("gets headers correctly when set multiple times", function () { + const response = emptyResponse(); + response.headers.set("X-Test", "hello"); + let headers = getHttpHeaders(response, "x-test"); + assert.isArray(headers); + assert.lengthOf(headers, 1); + response.headers.set("X-Test", ["hello", "world", "1234"]); + headers = getHttpHeaders(response, "x-test"); + assert.isArray(headers); + assert.lengthOf(headers, 3); + assert.equal(headers[0], "hello"); + assert.equal(headers[1], "world"); + assert.equal(headers[2], "1234"); + }); + + it("returns an empty array if the passed in value is `null`", function () { + const headers = getHttpHeaders(null, "content-type"); + assert.isArray(headers); + assert.lengthOf(headers, 0); + }); +}); + +describe("getFirstHttpHeader", () => { + it("gets the first header", function () { + const response = emptyResponse(); + const header = getFirstHttpHeader(response, "content-Type"); + assert.isNotNull(header); + assert.isString(header); + assert.equal(header, "text/html"); + }); + + it("gets the first header on multiple values", function () { + const response = emptyResponse(); + response.headers.set("X-test", ["hello", "world", "1234"]); + const header = getFirstHttpHeader(response, "x-test"); + assert.isNotNull(header); + assert.isString(header); + assert.equal(header, "hello"); + }); +}); diff --git a/test/compare-results.test.js b/test/compare-results.test.js index e78ae56..7adde46 100644 --- a/test/compare-results.test.js +++ b/test/compare-results.test.js @@ -60,7 +60,7 @@ describeOrSkip("Old and New Comparison", () => { ), ]); } catch (error) { - console.log("ERROR", error.message); + console.log("ERROR", error); continue; } const resultNew = JSON.parse(response.body); diff --git a/test/cookies.test.js b/test/cookies.test.js index ede6a9f..13b7e54 100644 --- a/test/cookies.test.js +++ b/test/cookies.test.js @@ -35,12 +35,13 @@ describe("Cookies", () => { ]; const reqs = emptyRequests(); setCookieStrings(reqs, cookieStrings); - + assert.isNotNull(reqs.session); const retrieved = reqs.session.jar.getCookiesSync("https://mozilla.org", { http: true, }); const res = cookiesTest(reqs); + assert.isNotNull(res.data); for (const [key, c] of Object.entries(res.data)) { switch (key) { case "SESSIONID_SAMESITE_STRICT": @@ -72,6 +73,7 @@ describe("Cookies", () => { setCookieStrings(reqs, cookieStrings); const res = cookiesTest(reqs); + assert.isNotNull(res.data); for (const [key, c] of Object.entries(res.data)) { switch (key) { case "SESSIONID_SAMESITE_STRICT": @@ -149,6 +151,7 @@ describe("Cookies", () => { const reqs = emptyRequests(); setCookieStrings(reqs, cookieStrings); + assert.isNotNull(reqs.responses.https); reqs.responses.https.headers["strict-transport-security"] = "max-age=15768000"; @@ -168,6 +171,7 @@ describe("Cookies", () => { const reqs = emptyRequests(); setCookieStrings(reqs, cookieStrings); + assert.isNotNull(reqs.responses.https); reqs.responses.https.headers["strict-transport-security"] = "max-age=15768000"; @@ -234,9 +238,12 @@ describe("Cookies", () => { * @param {string[]} cookieStrings */ function setCookieStrings(reqs, cookieStrings) { - reqs.responses.auto.headers["set-cookie"] = cookieStrings; + assert.isNotNull(reqs.responses.auto); + reqs.responses.auto.headers["Set-Cookie"] = cookieStrings; for (const cookieString of cookieStrings) { const cookie = Cookie.parse(cookieString); + assert(cookie); + assert.isNotNull(reqs.session); reqs.session.jar.setCookieSync(cookie, reqs.session.url.href, { http: true, }); diff --git a/test/cors.test.js b/test/cors.test.js index 915081a..6013476 100644 --- a/test/cors.test.js +++ b/test/cors.test.js @@ -20,6 +20,7 @@ describe("Cross Origin Resource Sharing", () => { }); it("checks for public access", function () { + assert.isNotNull(reqs.responses.cors); reqs.responses.cors.headers["access-control-allow-origin"] = "*"; const result = crossOriginResourceSharingTest(reqs); assert.equal( @@ -31,6 +32,7 @@ describe("Cross Origin Resource Sharing", () => { }); it("checks for restricted access", function () { + assert.isNotNull(reqs.responses.cors); reqs.responses.cors.request.headers["origin"] = "https://http-observatory.security.mozilla.org"; reqs.responses.cors.headers["access-control-allow-origin"] = @@ -45,6 +47,7 @@ describe("Cross Origin Resource Sharing", () => { }); it("checks for universal access", function () { + assert.isNotNull(reqs.responses.cors); reqs.responses.cors.request.headers["origin"] = "https://http-observatory.security.mozilla.org"; reqs.responses.cors.headers["access-control-allow-origin"] = diff --git a/test/cross-origin-resource-policy.test.js b/test/cross-origin-resource-policy.test.js index 57439f8..8fbee04 100644 --- a/test/cross-origin-resource-policy.test.js +++ b/test/cross-origin-resource-policy.test.js @@ -21,6 +21,7 @@ describe("Cross Origin Resource Policy", () => { it("checks header validity", function () { const values = ["whimsy"]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["cross-origin-resource-policy"] = value; const result = crossOriginResourcePolicyTest(reqs); @@ -34,6 +35,7 @@ describe("Cross Origin Resource Policy", () => { it("checks for same-site", function () { const values = ["same-site"]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["cross-origin-resource-policy"] = value; const result = crossOriginResourcePolicyTest(reqs); @@ -46,6 +48,7 @@ describe("Cross Origin Resource Policy", () => { }); it("checks for same-origin", function () { const values = ["same-origin"]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["cross-origin-resource-policy"] = value; const result = crossOriginResourcePolicyTest(reqs); @@ -58,6 +61,7 @@ describe("Cross Origin Resource Policy", () => { }); it("checks for cross-origin", function () { const values = ["cross-origin"]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["cross-origin-resource-policy"] = value; const result = crossOriginResourcePolicyTest(reqs); diff --git a/test/analyzer.csp.test.js b/test/csp.test.js similarity index 91% rename from test/analyzer.csp.test.js rename to test/csp.test.js index 45dbc13..38dfbdf 100644 --- a/test/analyzer.csp.test.js +++ b/test/csp.test.js @@ -56,6 +56,7 @@ describe("Content Security Policy", () => { Expectation.CspImplementedWithUnsafeInline ); assert.isFalse(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["unsafeInline"]); } }); @@ -78,6 +79,7 @@ describe("Content Security Policy", () => { Expectation.CspImplementedWithInsecureScheme ); assert.isFalse(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["insecureSchemeActive"]); } }); @@ -103,6 +105,7 @@ describe("Content Security Policy", () => { Expectation.CspImplementedWithInsecureSchemeInPassiveContentOnly ); assert.isTrue(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["insecureSchemePassive"]); } }); @@ -138,6 +141,7 @@ describe("Content Security Policy", () => { Expectation.CspImplementedWithUnsafeInline ); assert.isFalse(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["unsafeInline"]); } }); @@ -153,8 +157,10 @@ describe("Content Security Policy", () => { const result = contentSecurityPolicyTest(requests); assert.equal(result["result"], Expectation.CspImplementedWithUnsafeEval); + assert.isNotNull(result.data); assert.deepEqual(result.data["script-src"], ["'unsafe-eval'"]); assert.isFalse(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["unsafeEval"]); }); it("unsafe inline in style src only", async () => { @@ -182,6 +188,7 @@ describe("Content Security Policy", () => { Expectation.CspImplementedWithUnsafeInlineInStyleSrcOnly ); assert.isTrue(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["unsafeInlineStyle"]); } }); @@ -255,6 +262,7 @@ describe("Content Security Policy", () => { assert.isTrue(result["http"]); assert.isFalse(result["meta"]); assert.isTrue(result["pass"]); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["defaultNone"]); } @@ -353,6 +361,7 @@ describe("Content Security Policy", () => { result["result"], Expectation.CspImplementedWithNoUnsafeDefaultSrcNone ); + assert.isNotNull(result["policy"]); assert.isTrue(result["policy"]["strictDynamic"]); } }); @@ -367,9 +376,9 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isFalse( - contentSecurityPolicyTest(requests)["policy"]["antiClickjacking"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isFalse(policy["antiClickjacking"]); } // Now test where anticlickjacking is enabled @@ -379,9 +388,9 @@ describe("Content Security Policy", () => { "Content-Security-Policy", "default-src *; frame-ancestors 'none'" ); - assert.isTrue( - contentSecurityPolicyTest(requests)["policy"]["antiClickjacking"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isTrue(policy["antiClickjacking"]); // Test unsafeObjects and insecureBaseUri values = [ @@ -393,12 +402,10 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isTrue( - contentSecurityPolicyTest(requests)["policy"]["insecureBaseUri"] - ); - assert.isTrue( - contentSecurityPolicyTest(requests)["policy"]["unsafeObjects"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isTrue(policy["insecureBaseUri"]); + assert.isTrue(policy["unsafeObjects"]); } // Other tests for insecureBaseUri @@ -411,9 +418,9 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isFalse( - contentSecurityPolicyTest(requests)["policy"]["insecureBaseUri"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isFalse(policy["insecureBaseUri"]); } // Test for insecureSchemePassive @@ -427,9 +434,9 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isTrue( - contentSecurityPolicyTest(requests)["policy"]["insecureSchemePassive"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isTrue(policy["insecureSchemePassive"]); } // Test for insecureFormAction @@ -443,9 +450,9 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isFalse( - contentSecurityPolicyTest(requests)["policy"]["insecureFormAction"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isFalse(policy["insecureFormAction"]); } values = ["default-src *", "default-src 'none'", "form-action https:"]; @@ -453,9 +460,9 @@ describe("Content Security Policy", () => { for (const value of values) { const requests = emptyRequests(); setHeader(requests.responses.auto, "Content-Security-Policy", value); - assert.isTrue( - contentSecurityPolicyTest(requests)["policy"]["insecureFormAction"] - ); + const policy = contentSecurityPolicyTest(requests)["policy"]; + assert.isNotNull(policy); + assert.isTrue(policy["insecureFormAction"]); } }); it("report only", async () => { diff --git a/test/helpers.js b/test/helpers.js index 615cea5..f060541 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -9,13 +9,14 @@ import { parseHttpEquivHeaders } from "../src/retriever/utils.js"; /** * - * @param {import("../src/types.js").Response} response + * @param {import("../src/types.js").Response | null} response * @param {string} header * @param {string} value */ export function setHeader(response, header, value) { - // @ts-ignore - response.headers.set(header, value); + if (typeof response?.headers.set === "function") { + response.headers.set(header, value); + } } /** @@ -37,13 +38,19 @@ export function emptyRequests(httpEquivFile = null) { req.resources.path = html; } - // @ts-ignore - req.responses.auto = {}; - req.responses.auto.headers = new AxiosHeaders("Content-Type: text/html"); - req.responses.auto.request = {}; - req.responses.auto.request.headers = new AxiosHeaders(); - req.responses.auto.status = 200; - req.responses.auto.verified = true; + req.responses.auto = { + headers: new AxiosHeaders("Content-Type: text/html"), + request: { + headers: new AxiosHeaders(), + }, + status: 200, + statusText: "OK", + verified: true, + data: "", + config: { + headers: new AxiosHeaders(), + }, + }; req.responses.cors = structuredClone(req.responses.auto); req.responses.http = structuredClone(req.responses.auto); diff --git a/test/referrer-policy.test.js b/test/referrer-policy.test.js index 750a2c3..cff2faa 100644 --- a/test/referrer-policy.test.js +++ b/test/referrer-policy.test.js @@ -19,9 +19,9 @@ describe("ReferrerPolicy", () => { "STRICT-ORIGIN", "strict-origin-when-cross-origin", ]; - + assert.isNotNull(reqs.responses.auto); for (const v of privValues) { - reqs.responses.auto.headers["referrer-policy"] = v; + reqs.responses.auto.headers["Referrer-Policy"] = v; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyPrivate); assert.isTrue(result.http); @@ -42,6 +42,7 @@ describe("ReferrerPolicy", () => { { // The meta/http-equiv header has precendence over the http header + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = "unsafe-url"; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyPrivate); @@ -59,6 +60,7 @@ describe("ReferrerPolicy", () => { }); it("checks for invalid referrer header", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = "whimsy"; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyHeaderInvalid); @@ -68,6 +70,7 @@ describe("ReferrerPolicy", () => { it("checks for unsafe referrer header", function () { const policies = ["origin", "origin-when-cross-origin", "unsafe-url"]; for (const policy of policies) { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = policy; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyUnsafe); @@ -81,6 +84,7 @@ describe("ReferrerPolicy", () => { "no-referrer, unsafe-url", // safe first value ]; for (const policy of valid_but_unsafe_policies) { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = policy; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyUnsafe); @@ -91,6 +95,7 @@ describe("ReferrerPolicy", () => { it("checks for multiple referrer headers with valid and invalid mixed", function () { const mixed_valid_invalid_policies = ["no-referrer, whimsy"]; for (const policy of mixed_valid_invalid_policies) { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = policy; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyPrivate); @@ -101,6 +106,7 @@ describe("ReferrerPolicy", () => { it("checks for multiple invalid referrer headers", function () { const invalid_policies = ["whimsy, whimsy1, whimsy2"]; for (const policy of invalid_policies) { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["referrer-policy"] = policy; const result = referrerPolicyTest(reqs); assert.equal(result.result, Expectation.ReferrerPolicyHeaderInvalid); diff --git a/test/retriever.test.js b/test/retriever.test.js index 51962b2..077641a 100644 --- a/test/retriever.test.js +++ b/test/retriever.test.js @@ -8,7 +8,7 @@ describe("TestRetriever", () => { it("test retrieve non-existent domain", async function () { const domain = Array(223) - .fill() + .fill(0) .map(() => String.fromCharCode(Math.random() * 26 + 97)) .join("") + ".net"; const requests = await retrieve(domain); @@ -16,6 +16,7 @@ describe("TestRetriever", () => { assert.isNull(requests.responses.cors); assert.isNull(requests.responses.http); assert.isNull(requests.responses.https); + assert.isNotNull(requests.session); assert.isNull(requests.session.response); assert.equal(domain, requests.hostname); assert.deepEqual(new Resources(), requests.resources); @@ -25,6 +26,8 @@ describe("TestRetriever", () => { const requests = await retrieve("developer.mozilla.org"); assert.isNotNull(requests.resources.path); assert.isNotNull(requests.responses.auto); + assert.isNotNull(requests.responses.http); + assert.isNotNull(requests.responses.https); assert.isNumber(requests.responses.http.status); assert.isNumber(requests.responses.https.status); assert.instanceOf(requests.session, Session); @@ -46,6 +49,7 @@ describe("TestRetriever", () => { // test site seems to have outage from time to time, disable for now it.skip("test_retrieve_invalid_cert", async function () { const reqs = await retrieve("expired.badssl.com"); + assert.isNotNull(reqs.responses.auto); assert.isFalse(reqs.responses.auto.verified); }).timeout(10000); }); diff --git a/test/scanner.test.js b/test/scanner.test.js index 8712006..33ce8a2 100644 --- a/test/scanner.test.js +++ b/test/scanner.test.js @@ -7,7 +7,7 @@ describe("Scanner", () => { it("returns an error on an unknown host", async function () { const domain = Array(223) - .fill() + .fill(0) .map(() => String.fromCharCode(Math.random() * 26 + 97)) .join("") + ".net"; @@ -15,7 +15,11 @@ describe("Scanner", () => { const scanResult = await scan(domain); throw new Error("scan should throw"); } catch (e) { - assert.equal(e.message, "The site seems to be down."); + if (e instanceof Error) { + assert.equal(e.message, "The site seems to be down."); + } else { + throw new Error("Unexpected error type"); + } } }); diff --git a/test/strict-transport-security.test.js b/test/strict-transport-security.test.js index 0263419..df3ee47 100644 --- a/test/strict-transport-security.test.js +++ b/test/strict-transport-security.test.js @@ -19,6 +19,7 @@ describe("Strict Transport Security", () => { }); it("header invalid", function () { + assert.isNotNull(reqs.responses.https); reqs.responses.https.headers["strict-transport-security"] = "includeSubDomains; preload"; let result = strictTransportSecurityTest(reqs); @@ -33,8 +34,10 @@ describe("Strict Transport Security", () => { }); it("no https", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["strict-transport-security"] = "max-age=15768000"; + assert.isNotNull(reqs.responses.http); reqs.responses.http.headers["strict-transport-security"] = "max-age=15768000"; reqs.responses.https = null; @@ -45,6 +48,7 @@ describe("Strict Transport Security", () => { }); it("invalid cert", function () { + assert.isNotNull(reqs.responses.https); reqs.responses.https.headers["strict-transport-security"] = "max-age=15768000; includeSubDomains; preload"; reqs.responses.https.verified = false; @@ -55,7 +59,8 @@ describe("Strict Transport Security", () => { }); it("max age too low", function () { - reqs.responses.https.headers["strict-transport-security"] = "max-age=86400"; + assert.isNotNull(reqs.responses.https); + reqs.responses.https.headers["Strict-Transport-Security"] = "max-age=86400"; const result = strictTransportSecurityTest(reqs); assert.equal( @@ -66,6 +71,7 @@ describe("Strict Transport Security", () => { }); it("implemented", function () { + assert.isNotNull(reqs.responses.https); reqs.responses.https.headers["strict-transport-security"] = "max-age=15768000; includeSubDomains; preload"; diff --git a/test/subresource-integrity.test.js b/test/subresource-integrity.test.js index 69f5f2b..df3db73 100644 --- a/test/subresource-integrity.test.js +++ b/test/subresource-integrity.test.js @@ -28,6 +28,7 @@ describe("Subresource Integrity", () => { it("checks for not html", function () { reqs.resources.path = `{"foo": "bar"}`; + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["content-type"] = "application/json"; const result = subresourceIntegrityTest(reqs); assert.equal(result.result, Expectation.SriNotImplementedResponseNotHtml); @@ -62,6 +63,7 @@ describe("Subresource Integrity", () => { assert.isTrue(result.pass); // And the same, but with a 404 status code + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.status = 404; result = subresourceIntegrityTest(reqs); assert.equal( diff --git a/test/x-content-type-options.test.js b/test/x-content-type-options.test.js index 301ee38..d735f4f 100644 --- a/test/x-content-type-options.test.js +++ b/test/x-content-type-options.test.js @@ -18,6 +18,7 @@ describe("X-Content-Type-Options", () => { it("checks header validity", function () { const values = ["whimsy", "nosniff, nosniff"]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["x-content-type-options"] = value; const result = xContentTypeOptionsTest(reqs); @@ -28,6 +29,7 @@ describe("X-Content-Type-Options", () => { it("checks for nosniff", function () { const values = ["nosniff", "nosniff "]; + assert.isNotNull(reqs.responses.auto); for (const value of values) { reqs.responses.auto.headers["x-content-type-options"] = value; const result = xContentTypeOptionsTest(reqs); diff --git a/test/x-frame-options.test.js b/test/x-frame-options.test.js index 6d7ad18..217c1d4 100644 --- a/test/x-frame-options.test.js +++ b/test/x-frame-options.test.js @@ -17,6 +17,7 @@ describe("X-Frame-Options", () => { }); it("checks validity", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["x-frame-options"] = "whimsy"; let result = xFrameOptionsTest(reqs); assert.equal(result.result, Expectation.XFrameOptionsHeaderInvalid); @@ -30,6 +31,7 @@ describe("X-Frame-Options", () => { }); it("checks allow from origin", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["x-frame-options"] = "ALLOW-FROM https://mozilla.org"; const result = xFrameOptionsTest(reqs); @@ -38,6 +40,7 @@ describe("X-Frame-Options", () => { }); it("checks deny", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["x-frame-options"] = "DENY"; let result = xFrameOptionsTest(reqs); assert.equal(result.result, Expectation.XFrameOptionsSameoriginOrDeny); @@ -50,6 +53,7 @@ describe("X-Frame-Options", () => { }); it("checks implemented via CSP", function () { + assert.isNotNull(reqs.responses.auto); reqs.responses.auto.headers["x-frame-options"] = "DENY"; reqs.responses.auto.headers["content-security-policy"] = "frame-ancestors https://mozilla.org";