From 4e1f7d29c149cbb708c9bfe9928623bde38c8518 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sat, 24 Sep 2022 13:24:10 +0100 Subject: [PATCH 01/17] Update deps --- package-lock.json | 2633 +++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 2417 insertions(+), 218 deletions(-) diff --git a/package-lock.json b/package-lock.json index 143bf22..c34c9bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,2350 @@ { "name": "http-message-signatures", "version": "0.1.2", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "http-message-signatures", + "version": "0.1.2", + "license": "ISC", + "devDependencies": { + "@tsconfig/node12": "^12.1.0", + "@types/chai": "^4.3.3", + "@types/mocha": "^10.0.0", + "@types/node": "^12.20.55", + "@typescript-eslint/eslint-plugin": "^5.36.1", + "@typescript-eslint/parser": "^5.36.1", + "chai": "^4.3.6", + "eslint": "^8.24.0", + "mocha": "^10.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", + "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/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-12.1.0.tgz", + "integrity": "sha512-kdeyUs2jy6iHVq8H27ymIE1a/ci7LjEw9VvXtvP8FKr0QptiEoM30oPy8XFfcWiPdcd2VzYdCGMYwow+K+guXA==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/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": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "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", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "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/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "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/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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "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.2", + "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": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/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/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.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "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-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "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.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-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", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/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/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/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/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "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.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "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", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "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", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "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.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, "dependencies": { "@aashutoshrathi/word-wrap": { "version": "1.2.6", @@ -29,9 +2371,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.1.tgz", - "integrity": "sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, "@eslint/eslintrc": { @@ -81,15 +2423,15 @@ "dev": true }, "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "@jridgewell/trace-mapping": { @@ -147,9 +2489,9 @@ "dev": true }, "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, "@types/chai": { @@ -198,34 +2540,6 @@ "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - } - }, - "@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true - }, - "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - } - } } }, "@typescript-eslint/parser": { @@ -260,39 +2574,6 @@ "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" - }, - "dependencies": { - "@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - } - } } }, "@typescript-eslint/types": { @@ -330,49 +2611,6 @@ "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - } - }, - "@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - } - } } }, "@typescript-eslint/visitor-keys": { @@ -386,16 +2624,17 @@ } }, "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "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 + "dev": true, + "requires": {} }, "acorn-walk": { "version": "8.2.0", @@ -564,6 +2803,17 @@ "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "cliui": { @@ -631,9 +2881,9 @@ "dev": true }, "deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -732,12 +2982,6 @@ "text-table": "^0.2.0" }, "dependencies": { - "@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", - "dev": true - }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -748,26 +2992,11 @@ "estraverse": "^5.2.0" } }, - "eslint-visitor-keys": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", - "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", - "dev": true - }, "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true - }, - "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, - "requires": { - "is-glob": "^4.0.3" - } } } }, @@ -782,9 +3011,9 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true }, "espree": { @@ -796,20 +3025,6 @@ "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" - }, - "dependencies": { - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", - "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", - "dev": true - } } }, "esquery": { @@ -865,9 +3080,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -875,6 +3090,17 @@ "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "fast-json-stable-stringify": { @@ -890,9 +3116,9 @@ "dev": true }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -974,26 +3200,26 @@ "dev": true }, "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.1.1", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "globals": { @@ -1038,9 +3264,9 @@ "dev": true }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { @@ -1269,29 +3495,13 @@ "yargs-unparser": "2.0.0" }, "dependencies": { - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "balanced-match": "^1.0.0" } }, "minimatch": { @@ -1301,17 +3511,6 @@ "dev": true, "requires": { "brace-expansion": "^2.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - } } }, "ms": { @@ -1526,9 +3725,9 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1686,9 +3885,9 @@ "dev": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 329b729..674229c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "chai": "^4.3.6", - "eslint": "^8.23.0", + "eslint": "^8.24.0", "mocha": "^10.0.0", "ts-node": "^10.9.1", "typescript": "^5.0.3" From 38828a435302027ebb2677bd3c0c5bdbcd1c4437 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 27 Sep 2022 00:25:47 +0100 Subject: [PATCH 02/17] Rewrite signing logic to include signing of requests and responses including request-response binding --- README.md | 8 +- package-lock.json | 339 ++++++++++++ package.json | 10 +- src/httpbis/new.ts | 363 +++++++++++++ src/structured-header.ts | 91 ++++ test/bootstrap.ts | 4 + test/httpbis/new.spec.ts | 1108 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 1918 insertions(+), 5 deletions(-) create mode 100644 src/httpbis/new.ts create mode 100644 src/structured-header.ts create mode 100644 test/bootstrap.ts create mode 100644 test/httpbis/new.spec.ts diff --git a/README.md b/README.md index 36b6ed6..1de3e66 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ of HTTP messages before being sent. Two specifications are supported by this library: -1. [HTTPBIS](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-06#appendix-B.2) -2. [Cavage](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12) +1. [HTTPbis](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures) +2. [Cavage](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) ## Approach -As the cavage specification is now expired and superseded by the HTTPBIS one, this library takes a -"HTTPBIS-first" approach. This means that most support and maintenance will go into the HTTPBIS +As the cavage specification is now expired and superseded by the HTTPbis one, this library takes a +"HTTPbis-first" approach. This means that most support and maintenance will go into the HTTPbis implementation and syntax. The syntax is then back-ported to the Cavage implementation as much as possible. diff --git a/package-lock.json b/package-lock.json index c34c9bb..09c3713 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,24 @@ "name": "http-message-signatures", "version": "0.1.2", "license": "ISC", + "dependencies": { + "structured-headers": "^0.5.0" + }, "devDependencies": { "@tsconfig/node12": "^12.1.0", "@types/chai": "^4.3.3", "@types/mocha": "^10.0.0", "@types/node": "^12.20.55", + "@types/sinon": "^10.0.13", + "@types/sinon-chai": "^3.2.8", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "chai": "^4.3.6", "eslint": "^8.24.0", "mocha": "^10.0.0", + "mockdate": "^3.0.5", + "sinon": "^14.0.0", + "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", "typescript": "^5.0.3" } @@ -192,6 +200,50 @@ "node": ">= 8" } }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", + "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -246,6 +298,31 @@ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, + "node_modules/@types/sinon": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", + "integrity": "sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.9.tgz", + "integrity": "sha512-/19t63pFYU0ikrdbXKBWj9PCdnKyTd0Qkz0X91Ta081cYsq90OxYdcWwK/dwEoDa6dtXgj2HJfmzgq+QZTHdmQ==", + "dev": true, + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -1428,6 +1505,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1458,6 +1541,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1486,6 +1575,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1651,6 +1746,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mockdate": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", + "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1681,6 +1782,37 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1785,6 +1917,15 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2005,6 +2146,34 @@ "node": ">=8" } }, + "node_modules/sinon": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", + "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "peerDependencies": { + "chai": "^4.0.0", + "sinon": ">=4.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2052,6 +2221,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/structured-headers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.5.0.tgz", + "integrity": "sha512-oLnmXSsjhud+LxRJpvokwP8ImEB2wTg8sg30buwfVViKMuluTv3BlOJHUX9VW9pJ2nQOxmx87Z0kB86O4cphag==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2470,6 +2644,52 @@ "fastq": "^1.6.0" } }, + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/samsam": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", + "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2524,6 +2744,31 @@ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, + "@types/sinon": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", + "integrity": "sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinon-chai": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.9.tgz", + "integrity": "sha512-/19t63pFYU0ikrdbXKBWj9PCdnKyTd0Qkz0X91Ta081cYsq90OxYdcWwK/dwEoDa6dtXgj2HJfmzgq+QZTHdmQ==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -3355,6 +3600,12 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3382,6 +3633,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3401,6 +3658,12 @@ "p-locate": "^5.0.0" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3530,6 +3793,12 @@ } } }, + "mockdate": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", + "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3554,6 +3823,41 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3628,6 +3932,15 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3757,6 +4070,27 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "sinon": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", + "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + } + }, + "sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "requires": {} + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3789,6 +4123,11 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "structured-headers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.5.0.tgz", + "integrity": "sha512-oLnmXSsjhud+LxRJpvokwP8ImEB2wTg8sg30buwfVViKMuluTv3BlOJHUX9VW9pJ2nQOxmx87Z0kB86O4cphag==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 674229c..2499c97 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "npm run lint --silent -- --fix", "prepare": "npm run build", "preversion": "npm run lint", - "test": "mocha -r ts-node/register test/**/*.ts" + "test": "mocha -r ts-node/register -r test/bootstrap.ts test/**/*.ts" }, "repository": { "type": "git", @@ -35,12 +35,20 @@ "@types/chai": "^4.3.3", "@types/mocha": "^10.0.0", "@types/node": "^12.20.55", + "@types/sinon": "^10.0.13", + "@types/sinon-chai": "^3.2.8", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "chai": "^4.3.6", "eslint": "^8.24.0", "mocha": "^10.0.0", + "mockdate": "^3.0.5", + "sinon": "^14.0.0", + "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", "typescript": "^5.0.3" + }, + "dependencies": { + "structured-headers": "^0.5.0" } } diff --git a/src/httpbis/new.ts b/src/httpbis/new.ts new file mode 100644 index 0000000..156082b --- /dev/null +++ b/src/httpbis/new.ts @@ -0,0 +1,363 @@ +import { + BareItem, + parseDictionary, + parseItem, + serializeItem, + serializeList, + Dictionary as DictionaryType, + ByteSequence, + serializeDictionary, + parseList, + Parameters, +} from 'structured-headers'; +import { Dictionary, parseHeader } from '../structured-header'; + +export interface Request { + method: string; + url: string | URL; + headers: Record; +} + +export interface Response { + status: number; + headers: Record; +} + +export type Signer = (data: Buffer) => Promise; + +export interface SigningKey { + id?: string; + alg?: string; + sign: Signer; +} + +export interface SigningParameters { + created?: Date | null; + expires?: Date; + nonce?: string; + alg?: string; + keyid?: string; + context?: string; + [param: string]: string | Date | null | undefined; +} + +const defaultParams = [ + 'keyid', + 'alg', + 'created', + 'expires', +]; + +export interface SigningConfig { + key: SigningKey; + name?: string; + params?: string[]; + fields?: string[]; + paramValues?: SigningParameters, +} + +function isRequest(obj: Request | Response): obj is Request { + return !!(obj as Request).method; +} + +/** + * This allows consumers of the library to supply field specifications that aren't + * strictly "structured fields". Really a string must start with a `"` but that won't + * tend to happen in our configs. + * + * @param {string} input + * @returns {string} + */ +function quoteString(input: string): string { + // if it's not quoted, attempt to quote + if (!input.startsWith('"')) { + // try to split the structured field + const [name, ...rest] = input.split(';'); + // no params, just quote the whole thing + if (!rest.length) { + return `"${name}"`; + } + // quote the first part and put the rest back as it was + return `"${name}";${rest.join(';')}`; + } + return input; +} + +export function deriveComponent(component: string, res: Response, req?: Request): string[]; +export function deriveComponent(component: string, req: Request): string[]; + +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +export function deriveComponent(component: string, message: Request | Response, req?: Request): string[] { + const [componentName, params] = parseItem(quoteString(component)); + // switch the context of the signing data depending on if the `req` flag was passed + const context = params.has('req') ? req : message; + if (!context) { + throw new Error('Missing request in request-response bound component'); + } + switch (componentName.toString().toLowerCase()) { + case '@method': + if (!isRequest(context)) { + throw new Error('Cannot derive @method from response'); + } + return [context.method.toUpperCase()]; + case '@target-uri': { + if (!isRequest(context)) { + throw new Error('Cannot derive @target-url on response'); + } + return [context.url.toString()]; + } + case '@authority': { + if (!isRequest(context)) { + throw new Error('Cannot derive @authority on response'); + } + const { port, protocol, hostname } = typeof context.url === 'string' ? new URL(context.url) : context.url; + let authority = hostname.toLowerCase(); + if (port && (protocol === 'http:' && port !== '80' || protocol === 'https:' && port !== '443')) { + authority += `:${port}`; + } + return [authority]; + } + case '@scheme': { + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { protocol } = typeof context.url === 'string' ? new URL(context.url) : context.url; + return [protocol.slice(0, -1)]; + } + case '@request-target': { + if (!isRequest(context)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${pathname}${search}`]; + } + case '@path': { + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const {pathname} = typeof context.url === 'string' ? new URL(context.url) : context.url; + return [decodeURI(pathname)]; + } + case '@query': { + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.7 + // absent query params means use `?` + return [decodeURI(search) || '?']; + } + case '@status': { + if (isRequest(context)) { + throw new Error('Cannot obtain @status component for requests'); + } + return [context.status.toString()]; + } + case '@query-param': { + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { searchParams } = typeof context.url === 'string' ? new URL(context.url) : context.url; + if (!params.has('name')) { + throw new Error('@query-param must have a named parameter'); + } + const name = (params.get('name') as BareItem).toString(); + if (!searchParams.has(name)) { + throw new Error(`Expected query parameter "${name}" not found`); + } + return searchParams.getAll(name); + } + default: + throw new Error(`Unsupported component "${component}"`); + } +} + +export function extractHeader(header: string, res: Response, req?: Request): string[]; +export function extractHeader(header: string, req: Request): string[]; + +export function extractHeader(header: string, { headers }: Request | Response, req?: Request): string[] { + const [headerName, params] = parseItem(quoteString(header)); + const context = params.has('req') ? req?.headers : headers; + if (!context) { + throw new Error('Missing request in request-response bound component'); + } + const lcHeaderName = headerName.toString().toLowerCase(); + const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === lcHeaderName); + if (!headerTuple) { + throw new Error(`No header ${headerName} found in headers`); + } + const values = (Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]); + if (params.has('bs') && params.has('sf')) { + throw new Error('Invalid combination of parameters'); + } + if (params.has('sf') || params.has('key')) { + // strict encoding of field + // I think this is wrong as the values need to be combined first and then parsed, + // not parsed one-by-one + const value = values.join(', '); + const parsed = parseHeader(value); + if (params.has('key') && !(parsed instanceof Dictionary)) { + throw new Error('Unable to parse header as dictionary'); + } + if (params.has('key')) { + const key = (params.get('key') as BareItem).toString(); + if (!(parsed as Dictionary).has(key)) { + throw new Error(`Unable to find key "${key}" in structured field`); + } + return [(parsed as Dictionary).get(key) as string]; + } + return [parsed.toString()]; + } + if (params.has('bs')) { + return [values.map((val) => { + const encoded = Buffer.from(val.trim().replace(/\n\s*/gm, ' ')); + return `:${encoded.toString('base64')}:` + }).join(', ')]; + } + // raw encoding + return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} + +export function createSignatureBase(config: SigningConfig, res: Response, req?: Request): [string, string[]][]; +export function createSignatureBase(config: SigningConfig, req: Request): [string, string[]][]; + +export function createSignatureBase(config: SigningConfig, res: Request | Response, req?: Request): [string, string[]][] { + return (config.fields ?? []).reduce<[string, string[]][]>((base, fieldName) => { + const [field, params] = parseItem(quoteString(fieldName)); + const lcFieldName = field.toString().toLowerCase(); + if (lcFieldName !== '@signature-params') { + const value = fieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); + base.push([serializeItem([lcFieldName, params]), value]); + } + return base; + }, []); +} + +export function formatSignatureBase(base: [string, string[]][]): string { + return base.map(([key, value]) => { + const quotedKey = serializeItem(parseItem(quoteString(key))); + return value.map((val) => `${quotedKey}: ${val}`).join('\n'); + }).join('\n'); +} + +export function createSigningParameters(config: SigningConfig): Parameters { + const now = new Date(); + return (config.params ?? defaultParams).reduce((params, paramName) => { + let value: string | number = ''; + switch (paramName) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created: Date = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } + default: + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); + } else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName] as string; + } + } + if (value) { + params.set(paramName, value); + } + return params; + }, new Map()); +} + +export function augmentHeaders(headers: Record, signature: Buffer, signatureInput: string, name?: string): Record { + let signatureHeaderName = 'Signature'; + let signatureInputHeaderName = 'Signature-Input'; + let signatureHeader: DictionaryType = new Map(); + let inputHeader: DictionaryType = new Map(); + // check to see if there are already signature/signature-input headers + // if there are we want to store the current (case-sensitive) name of the header + // and we want to parse out the current values so we can append our new signature + for (const header in headers) { + switch (header.toLowerCase()) { + case 'signature': { + signatureHeaderName = header; + signatureHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); + break; + } + case 'signature-input': + signatureInputHeaderName = header; + inputHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); + break; + } + } + // find a unique signature name for the header. Check if any existing headers already use + // the name we intend to use, if there are, add incrementing numbers to the signature name + // until we have a unique name to use + let signatureName = name ?? 'sig'; + if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) { + let count = 0; + while (signatureHeader?.has(`${signatureName}${count}`) || inputHeader?.has(`${signatureName}${count}`)) { + count++; + } + signatureName += count.toString(); + } + // append our signature and signature-inputs to the headers and return + signatureHeader.set(signatureName, [new ByteSequence(signature.toString('base64')), new Map()]); + inputHeader.set(signatureName, parseList(signatureInput)[0]); + return { + ...headers, + [signatureHeaderName]: serializeDictionary(signatureHeader), + [signatureInputHeaderName]: serializeDictionary(inputHeader), + }; +} + +export async function signMessage(config: SigningConfig, res: T, req?: U): Promise; +export async function signMessage(config: SigningConfig, req: T): Promise; + +export async function signMessage(config: SigningConfig, message: T, req?: U): Promise { + const signingParameters = createSigningParameters(config); + const signatureBase = createSignatureBase(config, message as Response, req); + const signatureInput = serializeList([ + [ + signatureBase.map(([item]) => parseItem(item)), + signingParameters, + ], + ]); + signatureBase.push(['"@signature-params"', [signatureInput]]); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + return { + ...message, + headers: augmentHeaders({...message.headers}, signature, signatureInput, config.name), + }; +} diff --git a/src/structured-header.ts b/src/structured-header.ts new file mode 100644 index 0000000..6121aa9 --- /dev/null +++ b/src/structured-header.ts @@ -0,0 +1,91 @@ +import { + Dictionary as DictType, + isInnerList, + Item as ItemType, + List as ListType, + parseDictionary, + parseItem, + parseList, + serializeDictionary, + serializeInnerList, + serializeItem, + serializeList, +} from 'structured-headers'; + +export class Dictionary { + private readonly parsed: DictType; + private readonly raw: string; + constructor(input: string) { + this.raw = input; + this.parsed = parseDictionary(input); + } + + toString(): string { + return this.serialize(); + } + + serialize(): string { + return serializeDictionary(this.parsed); + } + + has(key: string): boolean { + return this.parsed.has(key); + } + + get(key: string): string | undefined { + const value = this.parsed.get(key); + if (!value) { + return value; + } + if (isInnerList(value)) { + return serializeInnerList(value); + } + return serializeItem(value); + } +} + +export class List { + private readonly parsed: ListType; + private readonly raw: string; + constructor(input: string) { + this.raw = input; + this.parsed = parseList(input); + } + + toString(): string { + return this.serialize(); + } + + serialize(): string { + return serializeList(this.parsed); + } +} + +export class Item { + private readonly parsed: ItemType; + private readonly raw: string; + constructor(input: string) { + this.raw = input; + this.parsed = parseItem(input); + } + + toString(): string { + return this.serialize(); + } + + serialize(): string { + return serializeItem(this.parsed); + } +} + +export function parseHeader(header: string): List | Dictionary | Item { + const classes = [List, Dictionary, Item]; + for (let i = 0; i < classes.length; i++) { + try { + return new classes[i](header); + } catch (e) { + // noop + } + } + throw new Error('Unable to parse header as structured field'); +} diff --git a/test/bootstrap.ts b/test/bootstrap.ts new file mode 100644 index 0000000..2d2b737 --- /dev/null +++ b/test/bootstrap.ts @@ -0,0 +1,4 @@ +import { use } from 'chai'; +import sinonChai from 'sinon-chai'; + +use(sinonChai); diff --git a/test/httpbis/new.spec.ts b/test/httpbis/new.spec.ts new file mode 100644 index 0000000..3e324c9 --- /dev/null +++ b/test/httpbis/new.spec.ts @@ -0,0 +1,1108 @@ +import * as httpbis from '../../src/httpbis/new'; +import { expect } from 'chai'; +import { describe } from 'mocha'; +import * as MockDate from 'mockdate'; +import { stub } from 'sinon'; + +describe('httpbis', () => { + // test the spec as per https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2 + describe('.deriveComponent', () => { + describe('unbound components', () => { + it('derives @method component', () => { + const req: httpbis.Request = { + method: 'get', + headers: {}, + url: 'https://example.com/test', + }; + // must be in uppercase + expect(httpbis.deriveComponent('@method', req)).to.deep.equal(['GET']); + expect(httpbis.deriveComponent('@method', { + ...req, + method: 'POST', + })).to.deep.equal(['POST']); + }); + it('derives @target-uri', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('@target-uri', req)).to.deep.equal([ + 'https://www.example.com/path?param=value', + ]); + }); + it('derives @authority', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('@authority', req)).to.deep.equal([ + 'www.example.com', + ]); + expect(httpbis.deriveComponent('@authority', { + ...req, + url: 'https://www.EXAMPLE.com/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', { + ...req, + url: 'https://www.example.com:8080/path?param=value', + })).to.deep.equal(['www.example.com:8080']); + expect(httpbis.deriveComponent('@authority', { + ...req, + url: 'https://www.example.com:443/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', { + ...req, + url: 'http://www.example.com:80/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', { + ...req, + url: 'https://www.example.com:80/path?param=value', + })).to.deep.equal(['www.example.com:80']); + }); + it('derives @scheme', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('@scheme', req)).to.deep.equal(['https']); + expect(httpbis.deriveComponent('@scheme', { + ...req, + url: 'http://example.com', + })).to.deep.equal(['http']); + }); + it('derives @request-target', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + // it is assumed in Node that the HTTP request is formed as + // GET /path?param=value HTTP/1.1 + // and not: + // GET https://www.example.com/path?param=value HTTP/1.1 + // it's not easy to determine this in Node when receiving messages + expect(httpbis.deriveComponent('@request-target', req)).to.deep.equal([ + '/path?param=value', + ]); + }); + it('derives @path', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('@path', req)).to.deep.equal([ + '/path', + ]); + }); + it('derives @query', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('@query', req)).to.deep.equal([ + '?param=value&foo=bar&baz=batman', + ]); + expect(httpbis.deriveComponent('@query', { + ...req, + url: 'https://www.example.com/path?queryString', + })).to.deep.equal([ + '?queryString', + ]); + expect(httpbis.deriveComponent('@query', { + ...req, + url: 'https://www.example.com/path', + })).to.deep.equal([ + '?', + ]); + }); + it('derives @query-param', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + headers: { + Host: 'www.example.com', + }, + }; + expect(httpbis.deriveComponent('"@query-param";name="baz"', req)).to.deep.equal([ + 'batman', + ]); + expect(httpbis.deriveComponent('"@query-param";name="qux"', req)).to.deep.equal([ + '', + ]); + expect(httpbis.deriveComponent('@query-param;name=param', req)).to.deep.equal([ + 'value', + ]); + expect(httpbis.deriveComponent('@query-param;name=param', { + ...req, + url: 'https://example.com/path?param=value¶m=value2', + })).to.deep.equal([ + 'value', + 'value2', + ]); + }); + it('derives @status', () => { + const req: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + headers: { + Host: 'www.example.com', + }, + }; + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@status', res, req)).to.deep.equal(['200']); + }); + }); + describe('request-response bound components', () => { + const req: httpbis.Request = { + method: 'get', + headers: { + Host: 'www.example.com', + }, + url: 'https://www.example.com/path?param=value', + }; + it('derives @method component', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + // must be in uppercase + expect(httpbis.deriveComponent('@method;req', res, req)).to.deep.equal(['GET']); + expect(httpbis.deriveComponent('@method;req', res, { + ...req, + method: 'POST', + })).to.deep.equal(['POST']); + }); + it('derives @target-uri', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@target-uri;req', res, req)).to.deep.equal([ + 'https://www.example.com/path?param=value', + ]); + }); + it('derives @authority', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@authority;req', res, req)).to.deep.equal([ + 'www.example.com', + ]); + expect(httpbis.deriveComponent('@authority;req', res, { + ...req, + url: 'https://www.EXAMPLE.com/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority;req', res, { + ...req, + url: 'https://www.example.com:8080/path?param=value', + })).to.deep.equal(['www.example.com:8080']); + expect(httpbis.deriveComponent('@authority;req', res, { + ...req, + url: 'https://www.example.com:443/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority;req', res, { + ...req, + url: 'http://www.example.com:80/path?param=value', + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority;req', res, { + ...req, + url: 'https://www.example.com:80/path?param=value', + })).to.deep.equal(['www.example.com:80']); + }); + it('derives @scheme', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@scheme;req', res, req)).to.deep.equal(['https']); + expect(httpbis.deriveComponent('@scheme;req', res, { + ...req, + url: 'http://example.com', + })).to.deep.equal(['http']); + }); + it('derives @request-target', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + // it is assumed in Node that the HTTP request is formed as + // GET /path?param=value HTTP/1.1 + // and not: + // GET https://www.example.com/path?param=value HTTP/1.1 + // it's not easy to determine this in Node when receiving messages + expect(httpbis.deriveComponent('@request-target;req', res, req)).to.deep.equal([ + '/path?param=value', + ]); + }); + it('derives @path', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@path;req', res, req)).to.deep.equal([ + '/path', + ]); + }); + it('derives @query', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('@query;req', res, req)).to.deep.equal([ + '?param=value', + ]); + expect(httpbis.deriveComponent('@query;req', res, { + ...req, + url: 'https://www.example.com/path?queryString', + })).to.deep.equal([ + '?queryString', + ]); + expect(httpbis.deriveComponent('@query;req', res, { + ...req, + url: 'https://www.example.com/path', + })).to.deep.equal([ + '?', + ]); + }); + it('derives @query-param', () => { + const res: httpbis.Response = { + status: 200, + headers: {}, + }; + expect(httpbis.deriveComponent('"@query-param";req;name="baz"', res, { + ...req, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + 'batman', + ]); + expect(httpbis.deriveComponent('"@query-param";req;name="qux"', res, { + ...req, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + '', + ]); + expect(httpbis.deriveComponent('@query-param;req;name=param', res, { + ...req, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + 'value', + ]); + expect(httpbis.deriveComponent('@query-param;req;name=param', res, { + ...req, + url: 'https://example.com/path?param=value¶m=value2', + })).to.deep.equal([ + 'value', + 'value2', + ]); + }); + }); + }); + describe('.extractHeader', () => { + describe('raw headers', () => { + const request: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'X-OWS-Header': ' Leading and trailing whitespace. ', + 'X-Obs-Fold-Header': 'Obsolete\n line folding.', + 'Cache-Control': ['max-age=60', ' must-revalidate'], + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c)', + 'X-Empty-Header': '', + }, + }; + it('parses raw fields', () => { + expect(httpbis.extractHeader('host', request)).to.deep.equal(['www.example.com']); + expect(httpbis.extractHeader('date', request)).to.deep.equal(['Tue, 20 Apr 2021 02:07:56 GMT']); + expect(httpbis.extractHeader('X-OWS-Header', request)).to.deep.equal(['Leading and trailing whitespace.']); + expect(httpbis.extractHeader('x-obs-fold-header', request)).to.deep.equal(['Obsolete line folding.']); + expect(httpbis.extractHeader('cache-control', request)).to.deep.equal(['max-age=60, must-revalidate']); + expect(httpbis.extractHeader('example-dict', request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); + expect(httpbis.extractHeader('x-empty-header', request)).to.deep.equal(['']); + }); + }); + describe('sf headers', () => { + const request: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'X-OWS-Header': ' Leading and trailing whitespace. ', + 'X-Obs-Fold-Header': 'Obsolete\n line folding.', + 'Cache-Control': ['max-age=60', ' must-revalidate'], + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c)', + 'X-Empty-Header': '', + }, + }; + it('serializes a dictionary', () => { + expect(httpbis.extractHeader('example-dict;sf', request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); + }); + }); + describe('key from structured header', () => { + const request: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c), d', + }, + }; + it('pulls out an integer key', () => { + expect(httpbis.extractHeader('example-dict;key="a"', request)).to.deep.equal(['1']); + }); + it('pulls out a boolean key', () => { + expect(httpbis.extractHeader('example-dict;key="d"', request)).to.deep.equal(['?1']); + }); + it('pulls out parameters', () => { + expect(httpbis.extractHeader('example-dict;key="b"', request)).to.deep.equal(['2;x=1;y=2']); + }); + it('pulls out an inner list', () => { + expect(httpbis.extractHeader('example-dict;key="c"', request)).to.deep.equal(['(a b c)']); + }); + }); + describe('bs from header', () => { + const request: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Example-Header': ['value, with, lots', 'of, commas'], + }, + }; + it('encodes multiple headers separately', () => { + expect(httpbis.extractHeader('Example-Header;bs', request)).to.deep.equal([':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']); + expect(httpbis.extractHeader('Example-Header;bs', { + ...request, + headers: { + ...request.headers, + 'Example-Header': 'value, with, lots, of, commas', + }, + })).to.deep.equal([':dmFsdWUsIHdpdGgsIGxvdHMsIG9mLCBjb21tYXM=:']); + }); + }); + describe('request-response bound header', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884475;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', + }, + }; + const response: httpbis.Response = { + status: 503, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + }, + }; + it('binds requests and responses', () => { + expect(httpbis.extractHeader('Signature;req;key=sig1', response, request)).to.deep.equal([ + ':LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', + ]); + }); + }); + }); + describe('.createSignatureBase', () => { + describe('header fields', () => { + const request: httpbis.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'X-OWS-Header': ' Leading and trailing whitespace. ', + 'X-Obs-Fold-Header': 'Obsolete\n line folding.', + 'Cache-Control': ['max-age=60', ' must-revalidate'], + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c)', + 'X-Empty-Header': '', + }, + }; + it('creates a signature base from raw headers', () => { + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'host', + 'date', + 'x-ows-header', + 'x-obs-fold-header', + 'cache-control', + 'example-dict', + ], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"host"', ['www.example.com']], + ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['"x-ows-header"', ['Leading and trailing whitespace.']], + ['"x-obs-fold-header"', ['Obsolete line folding.']], + ['"cache-control"', ['max-age=60, must-revalidate']], + ['"example-dict"', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ]); + }); + it('extracts an empty header', () => { + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'X-Empty-Header', + ], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"x-empty-header"', ['']], + ]); + }); + it('extracts strict formatted headers', () => { + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'example-dict;sf', + ], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"example-dict";sf', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ]); + }); + it('extracts keys from dictionary headers', () => { + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'example-dict;key="a"', + 'example-dict;key="d"', + 'example-dict;key="b"', + 'example-dict;key="c"', + ], + } as httpbis.SigningConfig, { + ...request, + headers: { + ...request.headers, + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c), d', + }, + })).to.deep.equal([ + ['"example-dict";key="a"', ['1']], + ['"example-dict";key="d"', ['?1']], + ['"example-dict";key="b"', ['2;x=1;y=2']], + ['"example-dict";key="c"', ['(a b c)']], + ]); + }); + it('extracts binary formatted headers', () => { + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'example-header;bs', + ], + } as httpbis.SigningConfig, { + ...request, + headers: { + 'Example-Header': ['value, with, lots', 'of, commas'], + }, + } as httpbis.Request)).to.deep.equal([ + ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']], + ]); + expect(httpbis.createSignatureBase({ + name: 'sig', + fields: [ + 'example-header;bs', + ], + } as httpbis.SigningConfig, { + ...request, + headers: { + 'Example-Header': ['value, with, lots, of, commas'], + }, + } as httpbis.Request)).to.deep.equal([ + ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHMsIG9mLCBjb21tYXM=:']], + ]); + }); + }); + describe('derived components', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + it('derives @method', () => { + expect(httpbis.createSignatureBase({ + fields: ['@method'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@method"', ['POST']], + ]); + }); + it('derives @target-uri', () => { + expect(httpbis.createSignatureBase({ + fields: ['@target-uri'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@target-uri"', ['https://www.example.com/path?param=value']], + ]); + }); + it('derives @authority', () => { + expect(httpbis.createSignatureBase({ + fields: ['@authority'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@authority"', ['www.example.com']], + ]); + }); + it('derives @scheme', () => { + expect(httpbis.createSignatureBase({ + fields: ['@scheme'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@scheme"', ['https']], + ]); + }); + it('derives @request-target', () => { + expect(httpbis.createSignatureBase({ + fields: ['@request-target'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@request-target"', ['/path?param=value']], + ]); + }); + it('derives @path', () => { + expect(httpbis.createSignatureBase({ + fields: ['@path'], + } as httpbis.SigningConfig, request)).to.deep.equal([ + ['"@path"', ['/path']], + ]); + }); + it('derives @query', () => { + expect(httpbis.createSignatureBase({ + fields: ['@query'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', + })).to.deep.equal([ + ['"@query"', ['?param=value&foo=bar&baz=batman']], + ]); + expect(httpbis.createSignatureBase({ + fields: ['@query'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path?queryString', + })).to.deep.equal([ + ['"@query"', ['?queryString']], + ]); + expect(httpbis.createSignatureBase({ + fields: ['@query'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path', + })).to.deep.equal([ + ['"@query"', ['?']], + ]); + }); + it('derives @query-param', () => { + expect(httpbis.createSignatureBase({ + fields: ['@query-param;name="baz"'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + ['"@query-param";name="baz"', ['batman']], + ]); + expect(httpbis.createSignatureBase({ + fields: ['@query-param;name="qux"'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + ['"@query-param";name="qux"', ['']], + ]); + expect(httpbis.createSignatureBase({ + fields: ['@query-param;name="param"'], + } as httpbis.SigningConfig, { + ...request, + url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', + })).to.deep.equal([ + ['"@query-param";name="param"', ['value']], + ]); + }); + it('derives @status', () => { + expect(httpbis.createSignatureBase({ + fields: ['@status'], + } as httpbis.SigningConfig, { + status: 200, + headers: {}, + }, request)).to.deep.equal([ + ['"@status"', ['200']], + ]); + }); + }); + describe('full example', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + }, + }; + it('produces a signature base for a request', () => { + expect(httpbis.createSignatureBase({ + key: { + sign: () => Promise.resolve(Buffer.from('')), + }, + fields: [ + '@method', + '@authority', + '@path', + 'content-digest', + 'content-length', + 'content-type', + ], + }, request)).to.deep.equal([ + ['"@method"', ['POST']], + ['"@authority"', ['example.com']], + ['"@path"', ['/foo']], + ['"content-digest"', ['sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:']], + ['"content-length"', ['18']], + ['"content-type"', ['application/json']], + // note that we don't add the `@signature-params until the signature is actually constructed + ]); + }); + }); + }); + describe('.formatSignatureBase', () => { + it('formats @method', () => { + expect(httpbis.formatSignatureBase([ + ['"@method"', ['POST']], + ])).to.equal('"@method": POST'); + }); + it('derives @target-uri', () => { + expect(httpbis.formatSignatureBase([ + ['"@target-uri"', ['https://www.example.com/path?param=value']], + ])).to.equal('"@target-uri": https://www.example.com/path?param=value'); + }); + it('derives @authority', () => { + expect(httpbis.formatSignatureBase([ + ['"@authority"', ['www.example.com']], + ])).to.equal('"@authority": www.example.com'); + }); + it('derives @scheme', () => { + expect(httpbis.formatSignatureBase([ + ['"@scheme"', ['https']], + ])).to.equal('"@scheme": https'); + }); + it('derives @request-target', () => { + expect(httpbis.formatSignatureBase([ + ['"@request-target"', ['/path?param=value']], + ])).to.equal('"@request-target": /path?param=value'); + }); + it('derives @path', () => { + expect(httpbis.formatSignatureBase([ + ['"@path"', ['/path']], + ])).to.equal('"@path": /path'); + }); + it('derives @query', () => { + expect(httpbis.formatSignatureBase([ + ['"@query"', ['?param=value&foo=bar&baz=batman']], + ])).to.equal('"@query": ?param=value&foo=bar&baz=batman'); + expect(httpbis.formatSignatureBase([ + ['"@query"', ['?queryString']], + ])).to.equal('"@query": ?queryString'); + expect(httpbis.formatSignatureBase([ + ['"@query"', ['?']], + ])).to.equal('"@query": ?'); + }); + it('derives @query-param', () => { + expect(httpbis.formatSignatureBase([ + ['"@query-param";name="baz"', ['batman']], + ])).to.equal('"@query-param";name="baz": batman'); + expect(httpbis.formatSignatureBase([ + ['"@query-param";name="qux"', ['']], + ])).to.equal('"@query-param";name="qux": '); + expect(httpbis.formatSignatureBase([ + ['"@query-param";name="param"', ['value']], + ])).to.equal('"@query-param";name="param": value'); + }); + it('derives @status', () => { + expect(httpbis.formatSignatureBase([ + ['"@status"', ['200']], + ])).to.equal('"@status": 200'); + }); + it('formats many headers', () => { + expect(httpbis.formatSignatureBase([ + ['"host"', ['www.example.com']], + ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['"x-ows-header"', ['Leading and trailing whitespace.']], + ['"x-obs-fold-header"', ['Obsolete line folding.']], + ['"cache-control"', ['max-age=60, must-revalidate']], + ['"example-dict"', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ['"x-empty-header"', ['']], + ])).to.equal('"host": www.example.com\n' + + '"date": Tue, 20 Apr 2021 02:07:56 GMT\n' + + '"x-ows-header": Leading and trailing whitespace.\n' + + '"x-obs-fold-header": Obsolete line folding.\n' + + '"cache-control": max-age=60, must-revalidate\n' + + '"example-dict": a=1, b=2;x=1;y=2, c=(a b c)\n' + + '"x-empty-header": '); + }); + it('formats strict formatted headers', () => { + expect(httpbis.formatSignatureBase([ + ['"example-dict";sf', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ])).to.equal('"example-dict";sf: a=1, b=2;x=1;y=2, c=(a b c)'); + }); + }); + describe('.createSigningParameters', () => { + before('mock date', () => { + MockDate.set(new Date('2022-09-27 08:34:12 GMT')); + }); + after('reset date', () => { + MockDate.reset(); + }); + describe('default params', () => { + it('creates a set of default parameters', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('omits created if null passed', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { created: null }, + }, ).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ]); + }); + it('uses a custom expires if passed', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { expires: new Date(Date.now() + 600000) }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664268252], + ]); + }); + it('overrides the keyid', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { keyid: '321' }, + }).entries())).to.deep.equal([ + ['keyid', '321'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('overrides the alg', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { alg: 'rsa321' }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa321'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('handles missing alg', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('handles missing keyid', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + sign: () => Promise.resolve(Buffer.from('')), + }, + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('returns nothing if no data', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + sign: () => Promise.resolve(Buffer.from('')), + }, + paramValues: { created: null }, + }).entries())).to.deep.equal([]); + }); + }); + describe('specified params', () => { + it('returns specified params', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg'], + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ]); + }); + it('returns arbitrary params', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg', 'custom'], + paramValues: { custom: 'value' }, + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ['custom', 'value'], + ]); + }); + }); + }); + describe('.augmentHeaders', () => { + it('adds a new signature and input header', () => { + expect(httpbis.augmentHeaders({}, Buffer.from('a fake signature'), '("@method";req);created=12345')).to.deep.equal({ + 'Signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'Signature-Input': 'sig=("@method";req);created=12345', + }); + }); + it('adds to an existing headers', () => { + expect(httpbis.augmentHeaders({ + 'signature': 'sig1=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig1=("@method";req);created=12345', + }, Buffer.from('another fake signature'), '("@request-target";req);created=12345')).to.deep.equal({ + 'signature': 'sig1=:YSBmYWtlIHNpZ25hdHVyZQ==:, sig=:YW5vdGhlciBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig1=("@method";req);created=12345, sig=("@request-target";req);created=12345', + }); + }); + it('avoids naming clashes with existing signatures', () => { + expect(httpbis.augmentHeaders({ + 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig=("@method";req);created=12345', + }, Buffer.from('another fake signature'), '("@request-target";req);created=12345')).to.deep.equal({ + 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:, sig0=:YW5vdGhlciBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig=("@method";req);created=12345, sig0=("@request-target";req);created=12345', + }); + }); + it('uses a provided signature name', () => { + expect(httpbis.augmentHeaders({ + 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig=("@method";req);created=12345', + }, Buffer.from('another fake signature'), '("@request-target";req);created=12345', 'reqres')).to.deep.equal({ + 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:, reqres=:YW5vdGhlciBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig=("@method";req);created=12345, reqres=("@request-target";req);created=12345', + }); + }); + }); + describe('.signMessage', () => { + describe('requests', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + }, + }; + let signer: httpbis.SigningKey; + beforeEach('stub signer', () => { + signer = { + sign: stub().resolves(Buffer.from('a fake signature')), + }; + }); + it('signs a request', async () => { + const signed = await httpbis.signMessage({ + key: signer, + params: [ + 'created', + 'keyid', + ], + fields: [ + '@method', + '@authority', + '@path', + 'content-digest', + 'content-length', + 'content-type', + ], + paramValues: { + keyid: 'test-key-rsa-pss', + created: new Date(1618884473 * 1000), + }, + }, request); + expect(signed.headers).to.deep.equal({ + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'Signature-Input': 'sig=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + '"@method": POST\n' + + '"@authority": example.com\n' + + '"@path": /foo\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"content-length": 18\n' + + '"content-type": application/json\n' + + '"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"' + )); + }); + }); + describe('responses', () => { + const response: httpbis.Response = { + status: 503, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + }, + }; + let signer: httpbis.SigningKey; + beforeEach('stub signer', () => { + signer = { + sign: stub().resolves(Buffer.from('a fake signature')), + }; + }); + it('signs a response', async () => { + const signed = await httpbis.signMessage({ + key: signer, + fields: ['@status', 'content-length', 'content-type'], + params: ['created', 'keyid'], + paramValues: { + created: new Date(1618884479 * 1000), + keyid: 'test-key-ecc-p256', + }, + }, response); + expect(signed.headers).to.deep.equal({ + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + 'Signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'Signature-Input': 'sig=("@status" "content-length" "content-type");created=1618884479;keyid="test-key-ecc-p256"', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + '"@status": 503\n' + + '"content-length": 62\n' + + '"content-type": application/json\n' + + '"@signature-params": ("@status" "content-length" "content-type");created=1618884479;keyid="test-key-ecc-p256"' + )); + }); + }); + describe('request bound responses', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884475;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', + }, + }; + const response: httpbis.Response = { + status: 503, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + }, + }; + let signer: httpbis.SigningKey; + beforeEach('stub signer', () => { + signer = { + sign: stub().resolves(Buffer.from('a fake signature')), + }; + }); + it('binds request-response fields', async () => { + const signed = await httpbis.signMessage({ + key: signer, + name: 'reqres', + fields: ['@status', 'content-length', 'content-type', 'signature;req;key="sig1"', '@authority;req', '@method;req'], + params: ['created', 'keyid'], + paramValues: { + created: new Date(1618884479 * 1000), + keyid: 'test-key-ecc-p256', + }, + }, response, request); + expect(signed.headers).to.deep.equal({ + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + 'Signature': 'reqres=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'Signature-Input': 'reqres=("@status" "content-length" "content-type" "signature";req;key="sig1" "@authority";req "@method";req);created=1618884479;keyid="test-key-ecc-p256"', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + '"@status": 503\n' + + '"content-length": 62\n' + + '"content-type": application/json\n' + + '"signature";req;key="sig1": :LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:\n' + + '"@authority";req: example.com\n' + + '"@method";req: POST\n' + + '"@signature-params": ("@status" "content-length" "content-type" "signature";req;key="sig1" "@authority";req "@method";req);created=1618884479;keyid="test-key-ecc-p256"' + )); + }); + }); + }); +}); From 429e7ac695515bbb5a692ec36867e23c3d3224b7 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 28 Sep 2022 18:42:42 +0100 Subject: [PATCH 03/17] Add message verification logic --- src/httpbis/new.ts | 148 +++++++++++++++++++--- test/httpbis/new.spec.ts | 265 +++++++++++++++++++++++++-------------- 2 files changed, 303 insertions(+), 110 deletions(-) diff --git a/src/httpbis/new.ts b/src/httpbis/new.ts index 156082b..b21f394 100644 --- a/src/httpbis/new.ts +++ b/src/httpbis/new.ts @@ -9,6 +9,8 @@ import { serializeDictionary, parseList, Parameters, + isInnerList, + isByteSequence, } from 'structured-headers'; import { Dictionary, parseHeader } from '../structured-header'; @@ -24,6 +26,7 @@ export interface Response { } export type Signer = (data: Buffer) => Promise; +export type Verifier = (data: Buffer, signature: Buffer, parameters: SignatureParameters) => Promise; export interface SigningKey { id?: string; @@ -31,14 +34,14 @@ export interface SigningKey { sign: Signer; } -export interface SigningParameters { +export interface SignatureParameters { created?: Date | null; expires?: Date; nonce?: string; alg?: string; keyid?: string; context?: string; - [param: string]: string | Date | null | undefined; + [param: string]: Date | number | string | null | undefined; } const defaultParams = [ @@ -48,12 +51,22 @@ const defaultParams = [ 'expires', ]; -export interface SigningConfig { +export interface SignConfig { key: SigningKey; name?: string; params?: string[]; fields?: string[]; - paramValues?: SigningParameters, + paramValues?: SignatureParameters, +} + +export interface VerifyConfig { + verifier: Verifier; + notAfter?: Date | number; + maxAge?: number; + tolerance?: number; + requiredParams?: string[]; + requiredFields?: string[]; + all?: boolean; } function isRequest(obj: Request | Response): obj is Request { @@ -226,15 +239,15 @@ export function extractHeader(header: string, { headers }: Request | Response, r return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; } -export function createSignatureBase(config: SigningConfig, res: Response, req?: Request): [string, string[]][]; -export function createSignatureBase(config: SigningConfig, req: Request): [string, string[]][]; +export function createSignatureBase(fields: string[], res: Response, req?: Request): [string, string[]][]; +export function createSignatureBase(fields: string[], req: Request): [string, string[]][]; -export function createSignatureBase(config: SigningConfig, res: Request | Response, req?: Request): [string, string[]][] { - return (config.fields ?? []).reduce<[string, string[]][]>((base, fieldName) => { +export function createSignatureBase(fields: string[], res: Request | Response, req?: Request): [string, string[]][] { + return (fields).reduce<[string, string[]][]>((base, fieldName) => { const [field, params] = parseItem(quoteString(fieldName)); const lcFieldName = field.toString().toLowerCase(); if (lcFieldName !== '@signature-params') { - const value = fieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); + const value = lcFieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); base.push([serializeItem([lcFieldName, params]), value]); } return base; @@ -248,7 +261,7 @@ export function formatSignatureBase(base: [string, string[]][]): string { }).join('\n'); } -export function createSigningParameters(config: SigningConfig): Parameters { +export function createSigningParameters(config: SignConfig): Parameters { const now = new Date(); return (config.params ?? defaultParams).reduce((params, paramName) => { let value: string | number = ''; @@ -340,12 +353,12 @@ export function augmentHeaders(headers: Record, signa }; } -export async function signMessage(config: SigningConfig, res: T, req?: U): Promise; -export async function signMessage(config: SigningConfig, req: T): Promise; +export async function signMessage(config: SignConfig, res: T, req?: U): Promise; +export async function signMessage(config: SignConfig, req: T): Promise; -export async function signMessage(config: SigningConfig, message: T, req?: U): Promise { +export async function signMessage(config: SignConfig, message: T, req?: U): Promise { const signingParameters = createSigningParameters(config); - const signatureBase = createSignatureBase(config, message as Response, req); + const signatureBase = createSignatureBase(config?.fields ?? [], message as Response, req); const signatureInput = serializeList([ [ signatureBase.map(([item]) => parseItem(item)), @@ -361,3 +374,110 @@ export async function signMessage; +export async function verifyMessage(config: VerifyConfig, request: Request): Promise; + +export async function verifyMessage(config: VerifyConfig, message: Request | Response, req?: Request): Promise { + const { signatures, signatureInputs } = Object.entries(message.headers).reduce<{ signatures?: DictionaryType; signatureInputs?: DictionaryType }>((accum, [name, value]) => { + switch (name.toLowerCase()) { + case 'signature': + return Object.assign(accum, { + signatures: parseDictionary(Array.isArray(value) ? value.join(', ') : value), + }); + case 'signature-input': + return Object.assign(accum, { + signatureInputs: parseDictionary(Array.isArray(value) ? value.join(', ') : value), + }); + default: + return accum; + } + }, {}); + // no signatures means an indeterminate result + if (!signatures?.size && !signatureInputs?.size) { + return null; + } + // a missing header means we can't verify the signatures + if (!signatures?.size || !signatureInputs?.size) { + throw new Error('Incomplete signature headers'); + } + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + return Array.from(signatureInputs.entries()).reduce>(async (prev, [name, input]) => { + const result: Error | boolean | null = await prev.catch((e) => e); + if (!config.all && result === true) { + return result; + } + if (config.all && result !== true && result !== null) { + if (result instanceof Error) { + throw result; + } + return result; + } + if (!isInnerList(input)) { + throw new Error('Malformed signature input'); + } + const hasRequiredParams = requiredParams.every((param) => input[1].has(param)); + if (!hasRequiredParams) { + return false; + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field)); + if (!hasRequiredFields) { + return false; + } + if (input[1].has('created')) { + const created = input[1].get('created') as number - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if (maxAge && created - now > maxAge) { + return false; + } + // created after the allowed time (ie: created in the future) + if (created > notAfter) { + return false; + } + } + if (input[1].has('expires')) { + const expires = input[1].get('expires') as number + tolerance; + // expired signature + if (expires > now) { + return false; + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + const signingBase = createSignatureBase(input[0].map((item) => serializeItem(item)), message as Response, req); + signingBase.push(['"@signature-params"', [serializeList([input])]]); + const base = formatSignatureBase(signingBase); + const signature = signatures.get(name); + if (!signature) { + throw new Error('No signature found for inputs'); + } + if (!isByteSequence(signature[0] as BareItem)) { + throw new Error('Malformed signature'); + } + return config.verifier(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { + let val: Date | number | string; + switch (key.toLowerCase()) { + case 'created': + case 'expires': + val = new Date((value as number) * 1000); + break; + default: { + if (typeof value === 'string' || typeof value=== 'number') { + val = value; + } else { + val = value.toString(); + } + } + } + return Object.assign(params, { + [key]: val, + }); + }, {})); + }, Promise.resolve(null)); +} diff --git a/test/httpbis/new.spec.ts b/test/httpbis/new.spec.ts index 3e324c9..58e8aba 100644 --- a/test/httpbis/new.spec.ts +++ b/test/httpbis/new.spec.ts @@ -448,17 +448,14 @@ describe('httpbis', () => { }, }; it('creates a signature base from raw headers', () => { - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'host', - 'date', - 'x-ows-header', - 'x-obs-fold-header', - 'cache-control', - 'example-dict', - ], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase([ + 'host', + 'date', + 'x-ows-header', + 'x-obs-fold-header', + 'cache-control', + 'example-dict', + ], request)).to.deep.equal([ ['"host"', ['www.example.com']], ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], ['"x-ows-header"', ['Leading and trailing whitespace.']], @@ -468,35 +465,26 @@ describe('httpbis', () => { ]); }); it('extracts an empty header', () => { - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'X-Empty-Header', - ], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase([ + 'X-Empty-Header', + ], request)).to.deep.equal([ ['"x-empty-header"', ['']], ]); }); it('extracts strict formatted headers', () => { - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'example-dict;sf', - ], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase([ + 'example-dict;sf', + ], request)).to.deep.equal([ ['"example-dict";sf', ['a=1, b=2;x=1;y=2, c=(a b c)']], ]); }); it('extracts keys from dictionary headers', () => { - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'example-dict;key="a"', - 'example-dict;key="d"', - 'example-dict;key="b"', - 'example-dict;key="c"', - ], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase([ + 'example-dict;key="a"', + 'example-dict;key="d"', + 'example-dict;key="b"', + 'example-dict;key="c"', + ], { ...request, headers: { ...request.headers, @@ -510,12 +498,9 @@ describe('httpbis', () => { ]); }); it('extracts binary formatted headers', () => { - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'example-header;bs', - ], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase([ + 'example-header;bs', + ], { ...request, headers: { 'Example-Header': ['value, with, lots', 'of, commas'], @@ -523,12 +508,9 @@ describe('httpbis', () => { } as httpbis.Request)).to.deep.equal([ ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']], ]); - expect(httpbis.createSignatureBase({ - name: 'sig', - fields: [ - 'example-header;bs', - ], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase([ + 'example-header;bs', + ], { ...request, headers: { 'Example-Header': ['value, with, lots, of, commas'], @@ -547,67 +529,49 @@ describe('httpbis', () => { }, }; it('derives @method', () => { - expect(httpbis.createSignatureBase({ - fields: ['@method'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@method'], request)).to.deep.equal([ ['"@method"', ['POST']], ]); }); it('derives @target-uri', () => { - expect(httpbis.createSignatureBase({ - fields: ['@target-uri'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@target-uri'], request)).to.deep.equal([ ['"@target-uri"', ['https://www.example.com/path?param=value']], ]); }); it('derives @authority', () => { - expect(httpbis.createSignatureBase({ - fields: ['@authority'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@authority'], request)).to.deep.equal([ ['"@authority"', ['www.example.com']], ]); }); it('derives @scheme', () => { - expect(httpbis.createSignatureBase({ - fields: ['@scheme'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@scheme'], request)).to.deep.equal([ ['"@scheme"', ['https']], ]); }); it('derives @request-target', () => { - expect(httpbis.createSignatureBase({ - fields: ['@request-target'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@request-target'], request)).to.deep.equal([ ['"@request-target"', ['/path?param=value']], ]); }); it('derives @path', () => { - expect(httpbis.createSignatureBase({ - fields: ['@path'], - } as httpbis.SigningConfig, request)).to.deep.equal([ + expect(httpbis.createSignatureBase(['@path'], request)).to.deep.equal([ ['"@path"', ['/path']], ]); }); it('derives @query', () => { - expect(httpbis.createSignatureBase({ - fields: ['@query'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query'], { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', })).to.deep.equal([ ['"@query"', ['?param=value&foo=bar&baz=batman']], ]); - expect(httpbis.createSignatureBase({ - fields: ['@query'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query'], { ...request, url: 'https://www.example.com/path?queryString', })).to.deep.equal([ ['"@query"', ['?queryString']], ]); - expect(httpbis.createSignatureBase({ - fields: ['@query'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query'], { ...request, url: 'https://www.example.com/path', })).to.deep.equal([ @@ -615,25 +579,19 @@ describe('httpbis', () => { ]); }); it('derives @query-param', () => { - expect(httpbis.createSignatureBase({ - fields: ['@query-param;name="baz"'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query-param;name="baz"'], { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ ['"@query-param";name="baz"', ['batman']], ]); - expect(httpbis.createSignatureBase({ - fields: ['@query-param;name="qux"'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query-param;name="qux"'], { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ ['"@query-param";name="qux"', ['']], ]); - expect(httpbis.createSignatureBase({ - fields: ['@query-param;name="param"'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@query-param;name="param"'], { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ @@ -641,9 +599,7 @@ describe('httpbis', () => { ]); }); it('derives @status', () => { - expect(httpbis.createSignatureBase({ - fields: ['@status'], - } as httpbis.SigningConfig, { + expect(httpbis.createSignatureBase(['@status'], { status: 200, headers: {}, }, request)).to.deep.equal([ @@ -664,19 +620,14 @@ describe('httpbis', () => { }, }; it('produces a signature base for a request', () => { - expect(httpbis.createSignatureBase({ - key: { - sign: () => Promise.resolve(Buffer.from('')), - }, - fields: [ - '@method', - '@authority', - '@path', - 'content-digest', - 'content-length', - 'content-type', - ], - }, request)).to.deep.equal([ + expect(httpbis.createSignatureBase([ + '@method', + '@authority', + '@path', + 'content-digest', + 'content-length', + 'content-type', + ], request)).to.deep.equal([ ['"@method"', ['POST']], ['"@authority"', ['example.com']], ['"@path"', ['/foo']], @@ -1105,4 +1056,126 @@ describe('httpbis', () => { }); }); }); + describe('.verifyMessage', () => { + describe('requests', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:HIbjHC5rS0BYaa9v4QfD4193TORw7u9edguPh0AW3dMq9WImrlFrCGUDih47vAxi4L2YRZ3XMJc1uOKk/J0ZmZ+wcta4nKIgBkKq0rM9hs3CQyxXGxHLMCy8uqK488o+9jrptQ+xFPHK7a9sRL1IXNaagCNN3ZxJsYapFj+JXbmaI5rtAdSfSvzPuBCh+ARHBmWuNo1UzVVdHXrl8ePL4cccqlazIJdC4QEjrF+Sn4IxBQzTZsL9y9TP5FsZYzHvDqbInkTNigBcE9cKOYNFCn4D/WM7F6TNuZO9EgtzepLWcjTymlHzK7aXq6Am6sfOrpIC49yXjj3ae6HRalVc/g==:', + }, + }; + it('verifies a request', async () => { + const verifierStub = stub().resolves(true); + const valid = await httpbis.verifyMessage({ + verifier: verifierStub, + }, request); + expect(valid).to.equal(true); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( + Buffer.from('"@method": POST\n' + + '"@authority": example.com\n' + + '"@path": /foo\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"content-length": 18\n' + + '"content-type": application/json\n' + + '"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"', + ), + Buffer.from('HIbjHC5rS0BYaa9v4QfD4193TORw7u9edguPh0AW3dMq9WImrlFrCGUDih47vAxi4L2YRZ3XMJc1uOKk/J0ZmZ+wcta4nKIgBkKq0rM9hs3CQyxXGxHLMCy8uqK488o+9jrptQ+xFPHK7a9sRL1IXNaagCNN3ZxJsYapFj+JXbmaI5rtAdSfSvzPuBCh+ARHBmWuNo1UzVVdHXrl8ePL4cccqlazIJdC4QEjrF+Sn4IxBQzTZsL9y9TP5FsZYzHvDqbInkTNigBcE9cKOYNFCn4D/WM7F6TNuZO9EgtzepLWcjTymlHzK7aXq6Am6sfOrpIC49yXjj3ae6HRalVc/g==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + }, + ); + }); + }); + describe('responses', () => { + const response: httpbis.Response = { + status: 200, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:JlEy2bfUz7WrWIjc1qV6KVLpdr/7L5/L4h7Sxvh6sNHpDQWDCL+GauFQWcZBvVDhiyOnAQsxzZFYwi0wDH+1pw==:', + 'Content-Length': '23', + 'Signature-Input': 'sig-b24=("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"', + 'Signature': 'sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:', + }, + }; + it('verifies a response', async () => { + const verifierStub = stub().resolves(true); + const result = await httpbis.verifyMessage({ + verifier: verifierStub, + }, response); + expect(result).to.equal(true); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( + Buffer.from('"@status": 200\n' + + '"content-type": application/json\n' + + '"content-digest": sha-512=:JlEy2bfUz7WrWIjc1qV6KVLpdr/7L5/L4h7Sxvh6sNHpDQWDCL+GauFQWcZBvVDhiyOnAQsxzZFYwi0wDH+1pw==:\n' + + '"content-length": 23\n' + + '"@signature-params": ("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"', + ), + Buffer.from('wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-ecc-p256', + }, + ); + }); + }); + describe('request bound responses', () => { + const request: httpbis.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884475;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', + }, + }; + const response: httpbis.Response = { + status: 503, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + 'Signature-Input': 'reqres=("@status" "content-length" "content-type" "signature";req;key="sig1" "@authority";req "@method";req);created=1618884479;keyid="test-key-ecc-p256"', + 'Signature': 'reqres=:mh17P4TbYYBmBwsXPT4nsyVzW4Rp9Fb8WcvnfqKCQLoMvzOBLD/n32tL/GPW6XE5GAS5bdsg1khK6lBzV1Cx/Q==:', + }, + }; + it('verifies a response bound to a request', async () => { + const stubVerifier = stub().resolves(true); + const result = await httpbis.verifyMessage({ + verifier: stubVerifier, + }, response, request); + expect(result).to.equal(true); + expect(stubVerifier).to.have.callCount(1); + expect(stubVerifier).to.have.been.calledOnceWithExactly( + Buffer.from('"@status": 503\n' + + '"content-length": 62\n' + + '"content-type": application/json\n' + + '"signature";req;key="sig1": :LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:\n' + + '"@authority";req: example.com\n' + + '"@method";req: POST\n' + + '"@signature-params": ("@status" "content-length" "content-type" "signature";req;key="sig1" "@authority";req "@method";req);created=1618884479;keyid="test-key-ecc-p256"', + ), + Buffer.from('mh17P4TbYYBmBwsXPT4nsyVzW4Rp9Fb8WcvnfqKCQLoMvzOBLD/n32tL/GPW6XE5GAS5bdsg1khK6lBzV1Cx/Q==', 'base64'), + { + keyid: 'test-key-ecc-p256', + created: new Date(1618884479 * 1000), + } + ); + }); + }); + }); }); From 93f78053d371b6e8e1e5c1c39bbec9bf37fe4259 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 28 Sep 2022 23:22:09 +0100 Subject: [PATCH 04/17] Rewrite cavage/RichAnna spec --- README.md | 13 +- src/algorithm/index.ts | 9 +- src/cavage/new.ts | 452 ++++++++++++++++++++++++++++++++++++++ test/cavage/new.spec.ts | 475 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 944 insertions(+), 5 deletions(-) create mode 100644 src/cavage/new.ts create mode 100644 test/cavage/new.spec.ts diff --git a/README.md b/README.md index 1de3e66..dedf4a8 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,19 @@ of HTTP messages before being sent. Two specifications are supported by this library: 1. [HTTPbis](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures) -2. [Cavage](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) +2. [Cavage](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) and subsequent [RichAnna](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures) ## Approach -As the cavage specification is now expired and superseded by the HTTPbis one, this library takes a +As the Cavage/RichAnna specification is now expired and superseded by the HTTPbis one, this library takes a "HTTPbis-first" approach. This means that most support and maintenance will go into the HTTPbis -implementation and syntax. The syntax is then back-ported to the Cavage implementation as much as -possible. +implementation and syntax. The syntax is then back-ported to the as much as possible. + +## Caveats + +The Cavage/RichAnna specifications have changed over time, introducing new features. The aim is to support +the [latest version of the specification](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures) +and not to try to support each version in isolation. ## Examples diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index b1cf6cb..ce71611 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -12,7 +12,7 @@ import { } from 'crypto'; import { RSA_PKCS1_PADDING, RSA_PKCS1_PSS_PADDING } from 'constants'; -export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512'; +export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512' | string; export interface Signer { (data: BinaryLike): Promise, @@ -42,6 +42,13 @@ export function createSigner(alg: Algorithm, key: BinaryLike | KeyLike | SignKey padding: RSA_PKCS1_PADDING, } as SignPrivateKeyInput); break; + case 'rsa-v1_5-sha1': + // this is legacy for cavage + signer = async (data: BinaryLike) => createSign('sha1').update(data).sign({ + key, + padding: RSA_PKCS1_PADDING, + } as SignPrivateKeyInput); + break; case 'ecdsa-p256-sha256': signer = async (data: BinaryLike) => createSign('sha256').update(data).sign(key as KeyLike); break; diff --git a/src/cavage/new.ts b/src/cavage/new.ts new file mode 100644 index 0000000..450a5ee --- /dev/null +++ b/src/cavage/new.ts @@ -0,0 +1,452 @@ +import { parseItem } from 'structured-headers'; +import { Algorithm } from '../algorithm'; + +export interface Request { + method: string; + url: string | URL; + headers: Record; +} + +export interface Response { + status: number; + headers: Record; +} + +export type Signer = (data: Buffer) => Promise; +export type Verifier = (data: Buffer, signature: Buffer, parameters: SignatureParameters) => Promise; + +export interface SigningKey { + id?: string; + alg?: string; + sign: Signer; +} + +/** + * The signature parameters to include in signing + */ +export interface SignatureParameters { + /** + * The created time for the signature. `null` indicates not to populate the `created` time + * default: Date.now() + */ + created?: Date | null; + /** + * The time the signature should be deemed to have expired + * default: Date.now() + 5 mins + */ + expires?: Date; + /** + * A nonce for the request + */ + nonce?: string; + /** + * The algorithm the signature is signed with (overrides the alg provided by the signing key) + */ + alg?: string; + /** + * The key id the signature is signed with (overrides the keyid provided by the signing key) + */ + keyid?: string; + /** + * A context parameter for the signature + */ + context?: string; + [param: string]: Date | number | string | null | undefined; +} + +/** + * Default parameters to use when signing a request if none are supplied by the consumer + */ +const defaultParams = [ + 'keyid', + 'alg', + 'created', + 'expires', +]; + +export interface SignConfig { + key: SigningKey; + /** + * The name to try to use for the signature + * Default: 'sig' + */ + name?: string; + /** + * The parameters to add to the signature + * Default: see defaultParams + */ + params?: string[]; + /** + * The HTTP fields / derived component names to sign + * Default: none + */ + fields?: string[]; + /** + * Specified parameter values to use (eg: created time, expires time, etc) + * This can be used by consumers to override the default expiration time or explicitly opt-out + * of adding creation time (by setting `created: null`) + */ + paramValues?: SignatureParameters, +} + +/** + * Options when verifying signatures + */ +export interface VerifyConfig { + verifier: Verifier; + /** + * A maximum age for the signature + * Default: Date.now() + tolerance + */ + notAfter?: Date | number; + /** + * The maximum age of the signature - this overrides the `expires` value for the signature + * if provided + */ + maxAge?: number; + /** + * A clock tolerance when verifying created/expires times + * Default: 0 + */ + tolerance?: number; + /** + * Any parameters that *must* be in the signature (eg: require a created time) + * Default: [] + */ + requiredParams?: string[]; + /** + * Any fields that *must* be in the signature (eg: Authorization, Digest, etc) + * Default: [] + */ + requiredFields?: string[]; + /** + * Verify every signature in the request. By default, only 1 signature will need to be valid + * for the verification to pass. + * Default: false + */ + all?: boolean; +} + +function mapCavageAlgorithm(alg: string): Algorithm { + switch (alg.toLowerCase()) { + case 'hs2019': + return 'rsa-pss-sha512'; + case 'rsa-sha1': + return 'rsa-v1_5-sha1'; + case 'rsa-sha256': + return 'rsa-v1_5-sha256'; + case 'ecdsa-sha256': + return 'ecdsa-p256-sha256'; + default: + return alg; + } +} + +function mapHttpbisAlgorithm(alg: Algorithm): string { + switch (alg.toLowerCase()) { + case 'rsa-pss-sha512': + return 'hs2019'; + case 'rsa-v1_5-sha1': + return 'rsa-sha1'; + case 'rsa-v1_5-sha256': + return 'rsa-sha256'; + case 'ecdsa-p256-sha256': + return 'ecdsa-sha256'; + default: + return alg; + } +} + +function isRequest(obj: Request | Response): obj is Request { + return !!(obj as Request).method; +} + +/** + * This allows consumers of the library to supply field specifications that aren't + * strictly "structured fields". Really a string must start with a `"` but that won't + * tend to happen in our configs. + * + * @param {string} input + * @returns {string} + */ +function quoteString(input: string): string { + // if it's not quoted, attempt to quote + if (!input.startsWith('"')) { + // try to split the structured field + const [name, ...rest] = input.split(';'); + // no params, just quote the whole thing + if (!rest.length) { + return `"${name}"`; + } + // quote the first part and put the rest back as it was + return `"${name}";${rest.join(';')}`; + } + return input; +} + +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +export function deriveComponent(component: string, message: Request | Response): string[] { + const [componentName, params] = parseItem(quoteString(component)); + if (params.size) { + throw new Error('Component parameters are not supported in cavage'); + } + switch (componentName.toString().toLowerCase()) { + case '@request-target': { + if (!isRequest(message)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${message.method.toLowerCase()} ${pathname}${search}`]; + } + default: + throw new Error(`Unsupported component "${component}"`); + } +} + +export function extractHeader(header: string, { headers }: Request | Response): string[] { + const [headerName, params] = parseItem(quoteString(header)); + if (params.size) { + throw new Error('Field parameters are not supported in cavage'); + } + const lcHeaderName = headerName.toString().toLowerCase(); + const headerTuple = Object.entries(headers).find(([name]) => name.toLowerCase() === lcHeaderName); + if (!headerTuple) { + throw new Error(`No header ${headerName} found in headers`); + } + return [(Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]).map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} + +export function formatSignatureBase(base: [string, string[]][]): string { + return base.reduce((accum, [key, value]) => { + const [keyName] = parseItem(quoteString(key)); + const lcKey = (keyName as string).toLowerCase(); + if (lcKey.startsWith('@')) { + accum.push(`(${lcKey.slice(1)}): ${value.join(', ')}`); + } else { + accum.push(`${key.toLowerCase()}: ${value.join(', ')}`); + } + return accum; + }, []).join('\n'); +} + +export function createSigningParameters(config: SignConfig): Map { + const now = new Date(); + return (config.params ?? defaultParams).reduce>((params, paramName) => { + let value: string | number = ''; + switch (paramName.toLowerCase()) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created: Date = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } + default: + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); + } else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName] as string; + } + } + if (value) { + params.set(paramName, value); + } + return params; + }, new Map()); +} + +export function createSignatureBase(fields: string[], message: Request | Response, signingParameters: Map): [string, string[]][] { + return fields.reduce<[string, string[]][]>((base, fieldName) => { + const [field, params] = parseItem(quoteString(fieldName)); + if (params.size) { + throw new Error('Field parameters are not supported'); + } + const lcFieldName = field.toString().toLowerCase(); + switch (lcFieldName) { + case '@created': + if (signingParameters.has('created')) { + base.push(['(created)', [signingParameters.get('created') as string]]); + } + break; + case '@expires': + if (signingParameters.has('expires')) { + base.push(['(expires)', [signingParameters.get('expires') as string]]); + } + break; + case '@request-target': { + if (!isRequest(message)) { + throw new Error('Cannot read target of response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + base.push(['(request-target)', [`${message.method} ${pathname}${search}`]]); + break; + } + default: + base.push([lcFieldName, extractHeader(lcFieldName, message)]); + } + return base; + }, []); +} + +export async function signMessage(config: SignConfig, message: T): Promise { + const signingParameters = createSigningParameters(config); + const signatureBase = createSignatureBase(config.fields ?? [], message, signingParameters); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + const headerNames = signatureBase.map(([key]) => key); + const header = [ + ...Array.from(signingParameters.entries()).map(([name, value]) => { + if (name === 'alg') { + return `algorithm="${mapHttpbisAlgorithm(value as string)}"`; + } + if (name === 'keyid') { + return `keyId="${value}"`; + } + if (typeof value === 'number') { + return `${name}=${value}`; + } + return `${name}="${value.toString()}"` + }), + `headers="${headerNames.join(' ')}"`, + `signature="${signature.toString('base64')}"`, + ].join(', '); + return { + ...message, + headers: { + ...message.headers, + Signature: header, + }, + }; +} + +export async function verifyMessage(config: VerifyConfig, message: Request | Response): Promise { + const header = Object.entries(message.headers).find(([name]) => name.toLowerCase() === 'signature'); + if (!header) { + return null; + } + const parsedHeader = (Array.isArray(header[1]) ? header[1].join(', ') : header[1]).split(',').reduce((parts, value) => { + const [key, ...values] = value.trim().split('='); + if (parts.has(key)) { + throw new Error('Same parameter defined repeatedly'); + } + const val = values.join('=').replace(/^"(.*)"$/, '$1'); + switch (key.toLowerCase()) { + case 'created': + case 'expires': + parts.set(key, parseInt(val, 10)); + break; + default: + parts.set(key, val); + } + return parts; + }, new Map()); + if (!parsedHeader.has('signature')) { + throw new Error('Missing signature from header'); + } + const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '').split(' ').map((component: string) => { + return component.toLowerCase().replace(/^\((.*)\)$/, '@$1'); + }), message, parsedHeader)); + const base = formatSignatureBase(Array.from(baseParts.entries())); + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + const hasRequiredParams = requiredParams.every((param) => baseParts.has(param)); + if (!hasRequiredParams) { + return false; + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => { + return parsedHeader.has(field.toLowerCase().replace(/^@(.*)/, '($1)')); + }); + if (!hasRequiredFields) { + return false; + } + if (parsedHeader.has('created')) { + const created = parsedHeader.get('created') as number - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if (maxAge && created - now > maxAge) { + return false; + } + // created after the allowed time (ie: created in the future) + if (created > notAfter) { + return false; + } + } + if (parsedHeader.has('expires')) { + const expires = parsedHeader.get('expires') as number + tolerance; + // expired signature + if (expires > now) { + return false; + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + return config.verifier(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { + let keyName = key; + let val: Date | number | string; + switch (key.toLowerCase()) { + case 'created': + case 'expires': + val = new Date((value as number) * 1000); + break; + case 'signature': + case 'headers': + return params; + case 'algorithm': + keyName = 'alg'; + val = mapCavageAlgorithm(value); + break; + case 'keyid': + keyName = 'keyid'; + val = value; + break; + // no break + default: { + if (typeof value === 'string' || typeof value=== 'number') { + val = value; + } else { + val = value.toString(); + } + } + } + return Object.assign(params, { + [keyName]: val, + }); + }, {})); +} diff --git a/test/cavage/new.spec.ts b/test/cavage/new.spec.ts new file mode 100644 index 0000000..59f18e6 --- /dev/null +++ b/test/cavage/new.spec.ts @@ -0,0 +1,475 @@ +import * as cavage from '../../src/cavage/new'; +import { expect } from 'chai'; +import { describe } from 'mocha'; +import * as MockDate from 'mockdate'; +import { stub } from 'sinon'; + +describe('cavage', () => { + // test the spec as per https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2 + describe('.deriveComponent', () => { + describe('unbound components', () => { + it('derives @request-target', () => { + const req: cavage.Request = { + method: 'POST', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + expect(cavage.deriveComponent('@request-target', req)).to.deep.equal([ + 'post /path?param=value', + ]); + }); + }); + }); + describe('.extractHeader', () => { + describe('raw headers', () => { + const request: cavage.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'X-OWS-Header': ' Leading and trailing whitespace. ', + 'X-Obs-Fold-Header': 'Obsolete\n line folding.', + 'Cache-Control': ['max-age=60', ' must-revalidate'], + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c)', + 'X-Empty-Header': '', + }, + }; + it('parses raw fields', () => { + expect(cavage.extractHeader('host', request)).to.deep.equal(['www.example.com']); + expect(cavage.extractHeader('date', request)).to.deep.equal(['Tue, 20 Apr 2021 02:07:56 GMT']); + expect(cavage.extractHeader('X-OWS-Header', request)).to.deep.equal(['Leading and trailing whitespace.']); + expect(cavage.extractHeader('x-obs-fold-header', request)).to.deep.equal(['Obsolete line folding.']); + expect(cavage.extractHeader('cache-control', request)).to.deep.equal(['max-age=60, must-revalidate']); + expect(cavage.extractHeader('example-dict', request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); + expect(cavage.extractHeader('x-empty-header', request)).to.deep.equal(['']); + }); + }); + }); + describe('.createSignatureBase', () => { + describe('header fields', () => { + const request: cavage.Request = { + method: 'POST', + url: 'https://www.example.com/', + headers: { + 'Host': 'www.example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'X-OWS-Header': ' Leading and trailing whitespace. ', + 'X-Obs-Fold-Header': 'Obsolete\n line folding.', + 'Cache-Control': ['max-age=60', ' must-revalidate'], + 'Example-Dict': ' a=1, b=2;x=1;y=2, c=(a b c)', + 'X-Empty-Header': '', + }, + }; + it('creates a signature base from raw headers', () => { + expect(cavage.createSignatureBase([ + 'host', + 'date', + 'x-ows-header', + 'x-obs-fold-header', + 'cache-control', + 'example-dict', + ], request, new Map())).to.deep.equal([ + ['host', ['www.example.com']], + ['date', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['x-ows-header', ['Leading and trailing whitespace.']], + ['x-obs-fold-header', ['Obsolete line folding.']], + ['cache-control', ['max-age=60, must-revalidate']], + ['example-dict', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ]); + }); + it('extracts an empty header', () => { + expect(cavage.createSignatureBase([ + 'X-Empty-Header', + ], request, new Map())).to.deep.equal([ + ['x-empty-header', ['']], + ]); + }); + }); + describe('derived components', () => { + const request: cavage.Request = { + method: 'post', + url: 'https://www.example.com/path?param=value', + headers: { + Host: 'www.example.com', + }, + }; + it('derives @request-target', () => { + expect(cavage.createSignatureBase(['@request-target'], request, new Map())).to.deep.equal([ + ['(request-target)', ['post /path?param=value']], + ]); + }); + }); + describe('full example', () => { + const request: cavage.Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + }, + }; + it('produces a signature base for a request', () => { + expect(cavage.createSignatureBase([ + '@request-target', + 'content-digest', + 'content-length', + 'content-type', + ], request, new Map())).to.deep.equal([ + ['(request-target)', ['post /foo?param=Value&Pet=dog']], + ['content-digest', ['sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:']], + ['content-length', ['18']], + ['content-type', ['application/json']], + ]); + }); + }); + }); + describe('.formatSignatureBase', () => { + it('derives @request-target', () => { + expect(cavage.formatSignatureBase([ + ['@request-target', ['post /path?param=value']], + ])).to.equal('(request-target): post /path?param=value'); + }); + it('formats many headers', () => { + expect(cavage.formatSignatureBase([ + ['host', ['www.example.com']], + ['date', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['x-ows-header', ['Leading and trailing whitespace.']], + ['x-obs-fold-header', ['Obsolete line folding.']], + ['cache-control', ['max-age=60, must-revalidate']], + ['example-dict', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ['x-empty-header', ['']], + ])).to.equal('host: www.example.com\n' + + 'date: Tue, 20 Apr 2021 02:07:56 GMT\n' + + 'x-ows-header: Leading and trailing whitespace.\n' + + 'x-obs-fold-header: Obsolete line folding.\n' + + 'cache-control: max-age=60, must-revalidate\n' + + 'example-dict: a=1, b=2;x=1;y=2, c=(a b c)\n' + + 'x-empty-header: '); + }); + }); + describe('.createSigningParameters', () => { + before('mock date', () => { + MockDate.set(new Date('2022-09-27 08:34:12 GMT')); + }); + after('reset date', () => { + MockDate.reset(); + }); + describe('default params', () => { + it('creates a set of default parameters', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('omits created if null passed', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { created: null }, + }, ).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ]); + }); + it('uses a custom expires if passed', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { expires: new Date(Date.now() + 600000) }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664268252], + ]); + }); + it('overrides the keyid', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { keyid: '321' }, + }).entries())).to.deep.equal([ + ['keyid', '321'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('overrides the alg', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { alg: 'rsa321' }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa321'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('handles missing alg', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + }, + }).entries())).to.deep.equal([ + ['keyid', '123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('handles missing keyid', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + sign: () => Promise.resolve(Buffer.from('')), + }, + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); + it('returns nothing if no data', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + sign: () => Promise.resolve(Buffer.from('')), + }, + paramValues: { created: null }, + }).entries())).to.deep.equal([]); + }); + }); + describe('specified params', () => { + it('returns specified params', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg'], + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ]); + }); + it('returns arbitrary params', () => { + expect(Array.from(cavage.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg', 'custom'], + paramValues: { custom: 'value' }, + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ['custom', 'value'], + ]); + }); + }); + }); + describe('.signMessage', () => { + describe('requests', () => { + const request: cavage.Request = { + method: 'post', + url: 'https://example.org/foo', + headers: { + 'Host': 'example.org', + 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', + 'Content-Type': 'application/json', + 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'Content-Length': '18', + }, + }; + let signer: cavage.SigningKey; + beforeEach('stub signer', () => { + signer = { + sign: stub().resolves(Buffer.from('a fake signature')), + }; + }); + it('signs a request', async () => { + const signed = await cavage.signMessage({ + key: signer, + params: [ + 'keyid', + 'alg', + 'created', + 'expires', + ], + fields: [ + '@request-target', + '@created', + '@expires', + 'host', + 'digest', + 'content-length', + ], + paramValues: { + keyid: 'rsa-key-1', + alg: 'hs2019', + created: new Date(1402170695 * 1000), + expires: new Date(1402170995 * 1000), + }, + }, request); + expect(signed.headers).to.deep.equal({ + 'Host': 'example.org', + 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', + 'Content-Type': 'application/json', + 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'Content-Length': '18', + 'Signature': 'keyId="rsa-key-1", algorithm="hs2019", created=1402170695, expires=1402170995, headers="(request-target) (created) (expires) host digest content-length", signature="YSBmYWtlIHNpZ25hdHVyZQ=="', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + '(request-target): post /foo\n' + + '(created): 1402170695\n' + + '(expires): 1402170995\n' + + 'host: example.org\n' + + 'digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + + 'content-length: 18' + )); + }); + }); + describe('responses', () => { + const response: cavage.Response = { + status: 503, + headers: { + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + }, + }; + let signer: cavage.SigningKey; + beforeEach('stub signer', () => { + signer = { + sign: stub().resolves(Buffer.from('a fake signature')), + }; + }); + it('signs a response', async () => { + const signed = await cavage.signMessage({ + key: signer, + fields: ['content-length', 'content-type'], + params: ['created', 'keyid'], + paramValues: { + created: new Date(1618884479 * 1000), + keyid: 'test-key-ecc-p256', + }, + }, response); + expect(signed.headers).to.deep.equal({ + 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'Content-Type': 'application/json', + 'Content-Length': '62', + 'Signature': 'created=1618884479, keyId="test-key-ecc-p256", headers="content-length content-type", signature="YSBmYWtlIHNpZ25hdHVyZQ=="', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + 'content-length: 62\n' + + 'content-type: application/json' + )); + }); + }); + }); + describe('.verifyMessage', () => { + describe('requests', () => { + const request: cavage.Request = { + method: 'post', + url: 'https://example.com/foo?param=value&pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', + 'Content-Type': 'application/json', + 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'Content-Length': '18', + 'Signature': 'keyId="test-key-a", algorithm="hs2019", created=1402170695, headers="(request-target) (created) host date content-type digest content-length", signature="KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=="', + }, + }; + it('verifies a request', async () => { + const verifierStub = stub().resolves(true); + const valid = await cavage.verifyMessage({ + verifier: verifierStub, + }, request); + expect(valid).to.equal(true); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( + Buffer.from( + '(request-target): post /foo?param=value&pet=dog\n' + + '(created): 1402170695\n' + + 'host: example.com\n' + + 'date: Tue, 07 Jun 2014 20:51:35 GMT\n' + + 'content-type: application/json\n' + + 'digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + + 'content-length: 18', + ), + Buffer.from('KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg==', 'base64'), + { + created: new Date(1402170695 * 1000), + keyid: 'test-key-a', + alg: 'rsa-pss-sha512', + }, + ); + }); + }); + describe('responses', () => { + const response: cavage.Response = { + status: 200, + headers: { + 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', + 'Content-Type': 'application/json', + 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', + 'Content-Length': '18', + 'Signature': 'keyId="test-key-a", algorithm="hs2019", created=1402170695, headers="(created) date content-type digest content-length", signature="KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg=="', + }, + }; + it('verifies a response', async () => { + const verifierStub = stub().resolves(true); + const result = await cavage.verifyMessage({ + verifier: verifierStub, + }, response); + expect(result).to.equal(true); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( + Buffer.from( + '(created): 1402170695\n' + + 'date: Tue, 07 Jun 2014 20:51:35 GMT\n' + + 'content-type: application/json\n' + + 'digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + + 'content-length: 18', + ), + Buffer.from('KXUj1H3ZOhv3Nk4xlRLTn4bOMlMOmFiud3VXrMa9MaLCxnVmrqOX5BulRvB65YW/wQp0oT/nNQpXgOYeY8ovmHlpkRyz5buNDqoOpRsCpLGxsIJ9cX8XVsM9jy+Q1+RIlD9wfWoPHhqhoXt35ZkasuIDPF/AETuObs9QydlsqONwbK+TdQguDK/8Va1Pocl6wK1uLwqcXlxhPEb55EmdYB9pddDyHTADING7K4qMwof2mC3t8Pb0yoLZoZX5a4Or4FrCCKK/9BHAhq/RsVk0dTENMbTB4i7cHvKQu+o9xuYWuxyvBa0Z6NdOb0di70cdrSDEsL5Gz7LBY5J2N9KdGg==', 'base64'), + { + created: new Date(1402170695 * 1000), + keyid: 'test-key-a', + alg: 'rsa-pss-sha512', + }, + ); + }); + }); + }); +}); From 16e92ee305e8a8aac3dd91f2e710a5a5f5cc908b Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 29 Sep 2022 12:48:40 +0100 Subject: [PATCH 05/17] Remove legacy code, update algorithms and signers/verifiers --- src/algorithm/index.ts | 60 ++-- src/cavage/index.ts | 388 +++++++++++++++------- src/cavage/new.ts | 452 -------------------------- src/httpbis/index.ts | 474 +++++++++++++++++++++------ src/httpbis/new.ts | 483 ---------------------------- src/structured-header.ts | 23 ++ src/types/index.ts | 149 +++++++-- test/algorithm/ecdsa-p256-sha256.ts | 25 +- test/algorithm/hmac-sha256.ts | 20 +- test/algorithm/rsa-pkcs1-sha1.ts | 40 +++ test/algorithm/rsa-pkcs1-sha256.ts | 17 +- test/algorithm/rsa-pss-sha512.ts | 37 ++- test/cavage/cavage.ts | 28 +- test/cavage/new.spec.ts | 25 +- test/httpbis/httpbis.ts | 168 ++-------- test/httpbis/new.spec.ts | 85 ++--- 16 files changed, 1012 insertions(+), 1462 deletions(-) delete mode 100644 src/cavage/new.ts delete mode 100644 src/httpbis/new.ts create mode 100644 test/algorithm/rsa-pkcs1-sha1.ts diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index ce71611..7204d46 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -11,54 +11,66 @@ import { VerifyPublicKeyInput, } from 'crypto'; import { RSA_PKCS1_PADDING, RSA_PKCS1_PSS_PADDING } from 'constants'; +import { SigningKey, Algorithm, Verifier } from '../types'; -export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512' | string; - -export interface Signer { - (data: BinaryLike): Promise, - alg: Algorithm, -} - -export interface Verifier { - (data: BinaryLike, signature: BinaryLike): Promise, - alg: Algorithm, -} - -export function createSigner(alg: Algorithm, key: BinaryLike | KeyLike | SignKeyObjectInput | SignPrivateKeyInput): Signer { - let signer; +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a signer "out of the box" using a PEM + * file they have access to. + * + * @todo - read the key and determine its type automatically to make usage even easier + */ +export function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | SignPrivateKeyInput, alg: Algorithm, id?: string): SigningKey { + const signer = { alg } as SigningKey; switch (alg) { case 'hmac-sha256': - signer = async (data: BinaryLike) => createHmac('sha256', key as BinaryLike).update(data).digest(); + signer.sign = async (data: BinaryLike) => createHmac('sha256', key as BinaryLike).update(data).digest(); break; case 'rsa-pss-sha512': - signer = async (data: BinaryLike) => createSign('sha512').update(data).sign({ + signer.sign = async (data: BinaryLike) => createSign('sha512').update(data).sign({ key, padding: RSA_PKCS1_PSS_PADDING, } as SignPrivateKeyInput); break; case 'rsa-v1_5-sha256': - signer = async (data: BinaryLike) => createSign('sha256').update(data).sign({ + signer.sign = async (data: BinaryLike) => createSign('sha256').update(data).sign({ key, padding: RSA_PKCS1_PADDING, } as SignPrivateKeyInput); break; case 'rsa-v1_5-sha1': // this is legacy for cavage - signer = async (data: BinaryLike) => createSign('sha1').update(data).sign({ + signer.sign = async (data: BinaryLike) => createSign('sha1').update(data).sign({ key, padding: RSA_PKCS1_PADDING, } as SignPrivateKeyInput); break; case 'ecdsa-p256-sha256': - signer = async (data: BinaryLike) => createSign('sha256').update(data).sign(key as KeyLike); + signer.sign = async (data: BinaryLike) => createSign('sha256').update(data).sign(key as KeyLike); break; default: throw new Error(`Unsupported signing algorithm ${alg}`); } - return Object.assign(signer, { alg }); + if (id) { + signer.id = id; + } + return signer; } -export function createVerifier(alg: Algorithm, key: BinaryLike | KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput): Verifier { +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a verifier "out of the box" using a PEM + * file they have access to. + * + * Verifiers are a little trickier as they will need to be produced "on demand" and the consumer will + * need to implement some logic for looking up keys by id (or other aspects of the request if no keyid + * is supplied) and then returning a validator + * + * @todo - attempt to look up algorithm automatically + */ +export function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, alg: Algorithm): Verifier { let verifier; switch (alg) { case 'hmac-sha256': @@ -74,6 +86,12 @@ export function createVerifier(alg: Algorithm, key: BinaryLike | KeyLike | Verif padding: RSA_PKCS1_PSS_PADDING, } as VerifyPublicKeyInput, Buffer.from(signature)); break; + case 'rsa-v1_5-sha1': + verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha1').update(data).verify({ + key, + padding: RSA_PKCS1_PADDING, + } as VerifyPublicKeyInput, Buffer.from(signature)); + break; case 'rsa-v1_5-sha256': verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha256').update(data).verify({ key, diff --git a/src/cavage/index.ts b/src/cavage/index.ts index 3625bd6..b5ef660 100644 --- a/src/cavage/index.ts +++ b/src/cavage/index.ts @@ -1,140 +1,300 @@ -import { - Component, - HeaderExtractionOptions, - Parameters, - RequestLike, - ResponseLike, SignOptions, -} from '../types'; -import { URL } from 'url'; +import { parseItem } from 'structured-headers'; +import { Algorithm, Request, Response, SignConfig, VerifyConfig, defaultParams, isRequest } from '../types'; +import { quoteString } from '../structured-header'; -export const defaultSigningComponents: Component[] = [ - '@request-target', - 'content-type', - 'digest', - 'content-digest', -]; - -export function extractHeader({ headers }: RequestLike | ResponseLike, header: string, opts?: HeaderExtractionOptions): string { - const lcHeader = header.toLowerCase(); - const key = Object.keys(headers).find((name) => name.toLowerCase() === lcHeader); - const allowMissing = opts?.allowMissing ?? true; - if (!allowMissing && !key) { - throw new Error(`Unable to extract header "${header}" from message`); +function mapCavageAlgorithm(alg: string): Algorithm { + switch (alg.toLowerCase()) { + case 'hs2019': + return 'rsa-pss-sha512'; + case 'rsa-sha1': + return 'rsa-v1_5-sha1'; + case 'rsa-sha256': + return 'rsa-v1_5-sha256'; + case 'ecdsa-sha256': + return 'ecdsa-p256-sha256'; + default: + return alg; } - let val = key ? headers[key] ?? '' : ''; - if (Array.isArray(val)) { - val = val.join(', '); +} + +function mapHttpbisAlgorithm(alg: Algorithm): string { + switch (alg.toLowerCase()) { + case 'rsa-pss-sha512': + return 'hs2019'; + case 'rsa-v1_5-sha1': + return 'rsa-sha1'; + case 'rsa-v1_5-sha256': + return 'rsa-sha256'; + case 'ecdsa-p256-sha256': + return 'ecdsa-sha256'; + default: + return alg; } - return val.toString().replace(/\s+/g, ' '); } -// see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-06#section-2.3 -export function extractComponent(message: RequestLike | ResponseLike, component: string): string { - switch (component) { +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +export function deriveComponent(component: string, message: Request | Response): string[] { + const [componentName, params] = parseItem(quoteString(component)); + if (params.size) { + throw new Error('Component parameters are not supported in cavage'); + } + switch (componentName.toString().toLowerCase()) { case '@request-target': { - const { pathname, search } = new URL(message.url); - return `${message.method.toLowerCase()} ${pathname}${search}`; + if (!isRequest(message)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${message.method.toLowerCase()} ${pathname}${search}`]; } default: - throw new Error(`Unknown specialty component ${component}`); + throw new Error(`Unsupported component "${component}"`); } } -const ALG_MAP: { [name: string]: string } = { - 'rsa-v1_5-sha256': 'rsa-sha256', -}; - -export function buildSignedData(request: RequestLike, components: Component[], params: Parameters): string { - const payloadParts: Parameters = {}; - const paramNames = Object.keys(params); - if (components.includes('@request-target')) { - Object.assign(payloadParts, { - '(request-target)': extractComponent(request, '@request-target'), - }); +export function extractHeader(header: string, { headers }: Request | Response): string[] { + const [headerName, params] = parseItem(quoteString(header)); + if (params.size) { + throw new Error('Field parameters are not supported in cavage'); } - if (paramNames.includes('created')) { - Object.assign(payloadParts, { - '(created)': params.created, - }); - } - if (paramNames.includes('expires')) { - Object.assign(payloadParts, { - '(expires)': params.expires, - }); + const lcHeaderName = headerName.toString().toLowerCase(); + const headerTuple = Object.entries(headers).find(([name]) => name.toLowerCase() === lcHeaderName); + if (!headerTuple) { + throw new Error(`No header ${headerName} found in headers`); } - components.forEach((name) => { - if (!name.startsWith('@')) { - Object.assign(payloadParts, { - [name.toLowerCase()]: extractHeader(request, name), - }); - } - }); - return Object.entries(payloadParts).map(([name, value]) => { - if (value instanceof Date) { - return `${name}: ${Math.floor(value.getTime() / 1000)}`; + return [(Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]).map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} + +export function formatSignatureBase(base: [string, string[]][]): string { + return base.reduce((accum, [key, value]) => { + const [keyName] = parseItem(quoteString(key)); + const lcKey = (keyName as string).toLowerCase(); + if (lcKey.startsWith('@')) { + accum.push(`(${lcKey.slice(1)}): ${value.join(', ')}`); } else { - return `${name}: ${value.toString()}`; + accum.push(`${key.toLowerCase()}: ${value.join(', ')}`); } - }).join('\n'); + return accum; + }, []).join('\n'); } -export function buildSignatureInputString(componentNames: Component[], parameters: Parameters): string { - const params: Parameters = Object.entries(parameters).reduce((normalised, [name, value]) => { - switch (name.toLowerCase()) { - case 'keyid': - return Object.assign(normalised, { - keyId: value, - }); - case 'alg': - return Object.assign(normalised, { - algorithm: ALG_MAP[value as string] ?? value, - }); +export function createSigningParameters(config: SignConfig): Map { + const now = new Date(); + return (config.params ?? defaultParams).reduce>((params, paramName) => { + let value: string | number = ''; + switch (paramName.toLowerCase()) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created: Date = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } default: - return Object.assign(normalised, { - [name]: value, - }); + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); + } else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName] as string; + } } - }, {}); - const headers = []; - const paramNames = Object.keys(params); - if (componentNames.includes('@request-target')) { - headers.push('(request-target)'); - } - if (paramNames.includes('created')) { - headers.push('(created)'); - } - if (paramNames.includes('expires')) { - headers.push('(expires)'); - } - componentNames.forEach((name) => { - if (!name.startsWith('@')) { - headers.push(name.toLowerCase()); + if (value) { + params.set(paramName, value); } - }); - return `${Object.entries(params).map(([name, value]) => { - if (typeof value === 'number') { - return `${name}=${value}`; - } else if (value instanceof Date) { - return `${name}=${Math.floor(value.getTime() / 1000)}`; - } else { - return `${name}="${value.toString()}"`; + return params; + }, new Map()); +} + +export function createSignatureBase(fields: string[], message: Request | Response, signingParameters: Map): [string, string[]][] { + return fields.reduce<[string, string[]][]>((base, fieldName) => { + const [field, params] = parseItem(quoteString(fieldName)); + if (params.size) { + throw new Error('Field parameters are not supported'); + } + const lcFieldName = field.toString().toLowerCase(); + switch (lcFieldName) { + case '@created': + if (signingParameters.has('created')) { + base.push(['(created)', [signingParameters.get('created') as string]]); + } + break; + case '@expires': + if (signingParameters.has('expires')) { + base.push(['(expires)', [signingParameters.get('expires') as string]]); + } + break; + case '@request-target': { + if (!isRequest(message)) { + throw new Error('Cannot read target of response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + base.push(['(request-target)', [`${message.method.toLowerCase()} ${pathname}${search}`]]); + break; + } + default: + base.push([lcFieldName, extractHeader(lcFieldName, message)]); } - }).join(',')},headers="${headers.join(' ')}"` + return base; + }, []); } -// @todo - should be possible to sign responses too -export async function sign(request: RequestLike, opts: SignOptions): Promise { - const signingComponents: Component[] = opts.components ?? defaultSigningComponents; - const signingParams: Parameters = { - ...opts.parameters, - keyid: opts.keyId, - alg: opts.signer.alg, +export async function signMessage(config: SignConfig, message: T): Promise { + const signingParameters = createSigningParameters(config); + const signatureBase = createSignatureBase(config.fields ?? [], message, signingParameters); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + const headerNames = signatureBase.map(([key]) => key); + const header = [ + ...Array.from(signingParameters.entries()).map(([name, value]) => { + if (name === 'alg') { + return `algorithm="${mapHttpbisAlgorithm(value as string)}"`; + } + if (name === 'keyid') { + return `keyId="${value}"`; + } + if (typeof value === 'number') { + return `${name}=${value}`; + } + return `${name}="${value.toString()}"` + }), + `headers="${headerNames.join(' ')}"`, + `signature="${signature.toString('base64')}"`, + ].join(', '); + return { + ...message, + headers: { + ...message.headers, + Signature: header, + }, }; - const signatureInputString = buildSignatureInputString(signingComponents, signingParams); - const dataToSign = buildSignedData(request, signingComponents, signingParams); - const signature = await opts.signer(Buffer.from(dataToSign)); - Object.assign(request.headers, { - Signature: `${signatureInputString},signature="${signature.toString('base64')}"`, +} + +export async function verifyMessage(config: VerifyConfig, message: Request | Response): Promise { + const header = Object.entries(message.headers).find(([name]) => name.toLowerCase() === 'signature'); + if (!header) { + return null; + } + const parsedHeader = (Array.isArray(header[1]) ? header[1].join(', ') : header[1]).split(',').reduce((parts, value) => { + const [key, ...values] = value.trim().split('='); + if (parts.has(key)) { + throw new Error('Same parameter defined repeatedly'); + } + const val = values.join('=').replace(/^"(.*)"$/, '$1'); + switch (key.toLowerCase()) { + case 'created': + case 'expires': + parts.set(key, parseInt(val, 10)); + break; + default: + parts.set(key, val); + } + return parts; + }, new Map()); + if (!parsedHeader.has('signature')) { + throw new Error('Missing signature from header'); + } + const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '').split(' ').map((component: string) => { + return component.toLowerCase().replace(/^\((.*)\)$/, '@$1'); + }), message, parsedHeader)); + const base = formatSignatureBase(Array.from(baseParts.entries())); + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + const hasRequiredParams = requiredParams.every((param) => baseParts.has(param)); + if (!hasRequiredParams) { + return false; + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => { + return parsedHeader.has(field.toLowerCase().replace(/^@(.*)/, '($1)')); }); - return request; + if (!hasRequiredFields) { + return false; + } + if (parsedHeader.has('created')) { + const created = parsedHeader.get('created') as number - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if (maxAge && created - now > maxAge) { + return false; + } + // created after the allowed time (ie: created in the future) + if (created > notAfter) { + return false; + } + } + if (parsedHeader.has('expires')) { + const expires = parsedHeader.get('expires') as number + tolerance; + // expired signature + if (expires > now) { + return false; + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + return config.verifier(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { + let keyName = key; + let val: Date | number | string; + switch (key.toLowerCase()) { + case 'created': + case 'expires': + val = new Date((value as number) * 1000); + break; + case 'signature': + case 'headers': + return params; + case 'algorithm': + keyName = 'alg'; + val = mapCavageAlgorithm(value); + break; + case 'keyid': + keyName = 'keyid'; + val = value; + break; + // no break + default: { + if (typeof value === 'string' || typeof value=== 'number') { + val = value; + } else { + val = value.toString(); + } + } + } + return Object.assign(params, { + [keyName]: val, + }); + }, {})); } diff --git a/src/cavage/new.ts b/src/cavage/new.ts deleted file mode 100644 index 450a5ee..0000000 --- a/src/cavage/new.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { parseItem } from 'structured-headers'; -import { Algorithm } from '../algorithm'; - -export interface Request { - method: string; - url: string | URL; - headers: Record; -} - -export interface Response { - status: number; - headers: Record; -} - -export type Signer = (data: Buffer) => Promise; -export type Verifier = (data: Buffer, signature: Buffer, parameters: SignatureParameters) => Promise; - -export interface SigningKey { - id?: string; - alg?: string; - sign: Signer; -} - -/** - * The signature parameters to include in signing - */ -export interface SignatureParameters { - /** - * The created time for the signature. `null` indicates not to populate the `created` time - * default: Date.now() - */ - created?: Date | null; - /** - * The time the signature should be deemed to have expired - * default: Date.now() + 5 mins - */ - expires?: Date; - /** - * A nonce for the request - */ - nonce?: string; - /** - * The algorithm the signature is signed with (overrides the alg provided by the signing key) - */ - alg?: string; - /** - * The key id the signature is signed with (overrides the keyid provided by the signing key) - */ - keyid?: string; - /** - * A context parameter for the signature - */ - context?: string; - [param: string]: Date | number | string | null | undefined; -} - -/** - * Default parameters to use when signing a request if none are supplied by the consumer - */ -const defaultParams = [ - 'keyid', - 'alg', - 'created', - 'expires', -]; - -export interface SignConfig { - key: SigningKey; - /** - * The name to try to use for the signature - * Default: 'sig' - */ - name?: string; - /** - * The parameters to add to the signature - * Default: see defaultParams - */ - params?: string[]; - /** - * The HTTP fields / derived component names to sign - * Default: none - */ - fields?: string[]; - /** - * Specified parameter values to use (eg: created time, expires time, etc) - * This can be used by consumers to override the default expiration time or explicitly opt-out - * of adding creation time (by setting `created: null`) - */ - paramValues?: SignatureParameters, -} - -/** - * Options when verifying signatures - */ -export interface VerifyConfig { - verifier: Verifier; - /** - * A maximum age for the signature - * Default: Date.now() + tolerance - */ - notAfter?: Date | number; - /** - * The maximum age of the signature - this overrides the `expires` value for the signature - * if provided - */ - maxAge?: number; - /** - * A clock tolerance when verifying created/expires times - * Default: 0 - */ - tolerance?: number; - /** - * Any parameters that *must* be in the signature (eg: require a created time) - * Default: [] - */ - requiredParams?: string[]; - /** - * Any fields that *must* be in the signature (eg: Authorization, Digest, etc) - * Default: [] - */ - requiredFields?: string[]; - /** - * Verify every signature in the request. By default, only 1 signature will need to be valid - * for the verification to pass. - * Default: false - */ - all?: boolean; -} - -function mapCavageAlgorithm(alg: string): Algorithm { - switch (alg.toLowerCase()) { - case 'hs2019': - return 'rsa-pss-sha512'; - case 'rsa-sha1': - return 'rsa-v1_5-sha1'; - case 'rsa-sha256': - return 'rsa-v1_5-sha256'; - case 'ecdsa-sha256': - return 'ecdsa-p256-sha256'; - default: - return alg; - } -} - -function mapHttpbisAlgorithm(alg: Algorithm): string { - switch (alg.toLowerCase()) { - case 'rsa-pss-sha512': - return 'hs2019'; - case 'rsa-v1_5-sha1': - return 'rsa-sha1'; - case 'rsa-v1_5-sha256': - return 'rsa-sha256'; - case 'ecdsa-p256-sha256': - return 'ecdsa-sha256'; - default: - return alg; - } -} - -function isRequest(obj: Request | Response): obj is Request { - return !!(obj as Request).method; -} - -/** - * This allows consumers of the library to supply field specifications that aren't - * strictly "structured fields". Really a string must start with a `"` but that won't - * tend to happen in our configs. - * - * @param {string} input - * @returns {string} - */ -function quoteString(input: string): string { - // if it's not quoted, attempt to quote - if (!input.startsWith('"')) { - // try to split the structured field - const [name, ...rest] = input.split(';'); - // no params, just quote the whole thing - if (!rest.length) { - return `"${name}"`; - } - // quote the first part and put the rest back as it was - return `"${name}";${rest.join(';')}`; - } - return input; -} - -/** - * Components can be derived from requests or responses (which can also be bound to their request). - * The signature is essentially (component, signingSubject, supplementaryData) - * - * @todo - Allow consumers to register their own component parser somehow - */ -export function deriveComponent(component: string, message: Request | Response): string[] { - const [componentName, params] = parseItem(quoteString(component)); - if (params.size) { - throw new Error('Component parameters are not supported in cavage'); - } - switch (componentName.toString().toLowerCase()) { - case '@request-target': { - if (!isRequest(message)) { - throw new Error('Cannot derive @request-target on response'); - } - const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; - // this is really sketchy because the request-target is actually what is in the raw HTTP header - // so one should avoid signing this value as the application layer just can't know how this - // is formatted - return [`${message.method.toLowerCase()} ${pathname}${search}`]; - } - default: - throw new Error(`Unsupported component "${component}"`); - } -} - -export function extractHeader(header: string, { headers }: Request | Response): string[] { - const [headerName, params] = parseItem(quoteString(header)); - if (params.size) { - throw new Error('Field parameters are not supported in cavage'); - } - const lcHeaderName = headerName.toString().toLowerCase(); - const headerTuple = Object.entries(headers).find(([name]) => name.toLowerCase() === lcHeaderName); - if (!headerTuple) { - throw new Error(`No header ${headerName} found in headers`); - } - return [(Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]).map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; -} - -export function formatSignatureBase(base: [string, string[]][]): string { - return base.reduce((accum, [key, value]) => { - const [keyName] = parseItem(quoteString(key)); - const lcKey = (keyName as string).toLowerCase(); - if (lcKey.startsWith('@')) { - accum.push(`(${lcKey.slice(1)}): ${value.join(', ')}`); - } else { - accum.push(`${key.toLowerCase()}: ${value.join(', ')}`); - } - return accum; - }, []).join('\n'); -} - -export function createSigningParameters(config: SignConfig): Map { - const now = new Date(); - return (config.params ?? defaultParams).reduce>((params, paramName) => { - let value: string | number = ''; - switch (paramName.toLowerCase()) { - case 'created': - // created is optional but recommended. If created is supplied but is null, that's an explicit - // instruction to *not* include the created parameter - if (config.paramValues?.created !== null) { - const created: Date = config.paramValues?.created ?? now; - value = Math.floor(created.getTime() / 1000); - } - break; - case 'expires': - // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after - // creation. Don't add an expires time if there is no created time - if (config.paramValues?.expires || config.paramValues?.created !== null) { - const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); - value = Math.floor(expires.getTime() / 1000); - } - break; - case 'keyid': { - // attempt to obtain the keyid omit if missing - const kid = config.paramValues?.keyid ?? config.key.id ?? null; - if (kid) { - value = kid.toString(); - } - break; - } - case 'alg': { - const alg = config.paramValues?.alg ?? config.key.alg ?? null; - if (alg) { - value = alg.toString(); - } - break; - } - default: - if (config.paramValues?.[paramName] instanceof Date) { - value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); - } else if (config.paramValues?.[paramName]) { - value = config.paramValues[paramName] as string; - } - } - if (value) { - params.set(paramName, value); - } - return params; - }, new Map()); -} - -export function createSignatureBase(fields: string[], message: Request | Response, signingParameters: Map): [string, string[]][] { - return fields.reduce<[string, string[]][]>((base, fieldName) => { - const [field, params] = parseItem(quoteString(fieldName)); - if (params.size) { - throw new Error('Field parameters are not supported'); - } - const lcFieldName = field.toString().toLowerCase(); - switch (lcFieldName) { - case '@created': - if (signingParameters.has('created')) { - base.push(['(created)', [signingParameters.get('created') as string]]); - } - break; - case '@expires': - if (signingParameters.has('expires')) { - base.push(['(expires)', [signingParameters.get('expires') as string]]); - } - break; - case '@request-target': { - if (!isRequest(message)) { - throw new Error('Cannot read target of response'); - } - const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; - base.push(['(request-target)', [`${message.method} ${pathname}${search}`]]); - break; - } - default: - base.push([lcFieldName, extractHeader(lcFieldName, message)]); - } - return base; - }, []); -} - -export async function signMessage(config: SignConfig, message: T): Promise { - const signingParameters = createSigningParameters(config); - const signatureBase = createSignatureBase(config.fields ?? [], message, signingParameters); - const base = formatSignatureBase(signatureBase); - // call sign - const signature = await config.key.sign(Buffer.from(base)); - const headerNames = signatureBase.map(([key]) => key); - const header = [ - ...Array.from(signingParameters.entries()).map(([name, value]) => { - if (name === 'alg') { - return `algorithm="${mapHttpbisAlgorithm(value as string)}"`; - } - if (name === 'keyid') { - return `keyId="${value}"`; - } - if (typeof value === 'number') { - return `${name}=${value}`; - } - return `${name}="${value.toString()}"` - }), - `headers="${headerNames.join(' ')}"`, - `signature="${signature.toString('base64')}"`, - ].join(', '); - return { - ...message, - headers: { - ...message.headers, - Signature: header, - }, - }; -} - -export async function verifyMessage(config: VerifyConfig, message: Request | Response): Promise { - const header = Object.entries(message.headers).find(([name]) => name.toLowerCase() === 'signature'); - if (!header) { - return null; - } - const parsedHeader = (Array.isArray(header[1]) ? header[1].join(', ') : header[1]).split(',').reduce((parts, value) => { - const [key, ...values] = value.trim().split('='); - if (parts.has(key)) { - throw new Error('Same parameter defined repeatedly'); - } - const val = values.join('=').replace(/^"(.*)"$/, '$1'); - switch (key.toLowerCase()) { - case 'created': - case 'expires': - parts.set(key, parseInt(val, 10)); - break; - default: - parts.set(key, val); - } - return parts; - }, new Map()); - if (!parsedHeader.has('signature')) { - throw new Error('Missing signature from header'); - } - const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '').split(' ').map((component: string) => { - return component.toLowerCase().replace(/^\((.*)\)$/, '@$1'); - }), message, parsedHeader)); - const base = formatSignatureBase(Array.from(baseParts.entries())); - const now = Math.floor(Date.now() / 1000); - const tolerance = config.tolerance ?? 0; - const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; - const maxAge = config.maxAge ?? null; - const requiredParams = config.requiredParams ?? []; - const requiredFields = config.requiredFields ?? []; - const hasRequiredParams = requiredParams.every((param) => baseParts.has(param)); - if (!hasRequiredParams) { - return false; - } - // this could be tricky, what if we say "@method" but there is "@method;req" - const hasRequiredFields = requiredFields.every((field) => { - return parsedHeader.has(field.toLowerCase().replace(/^@(.*)/, '($1)')); - }); - if (!hasRequiredFields) { - return false; - } - if (parsedHeader.has('created')) { - const created = parsedHeader.get('created') as number - tolerance; - // maxAge overrides expires. - // signature is older than maxAge - if (maxAge && created - now > maxAge) { - return false; - } - // created after the allowed time (ie: created in the future) - if (created > notAfter) { - return false; - } - } - if (parsedHeader.has('expires')) { - const expires = parsedHeader.get('expires') as number + tolerance; - // expired signature - if (expires > now) { - return false; - } - } - // now look to verify the signature! Build the expected "signing base" and verify it! - return config.verifier(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { - let keyName = key; - let val: Date | number | string; - switch (key.toLowerCase()) { - case 'created': - case 'expires': - val = new Date((value as number) * 1000); - break; - case 'signature': - case 'headers': - return params; - case 'algorithm': - keyName = 'alg'; - val = mapCavageAlgorithm(value); - break; - case 'keyid': - keyName = 'keyid'; - val = value; - break; - // no break - default: { - if (typeof value === 'string' || typeof value=== 'number') { - val = value; - } else { - val = value.toString(); - } - } - } - return Object.assign(params, { - [keyName]: val, - }); - }, {})); -} diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index 1830ea5..da66d6d 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -1,126 +1,400 @@ import { - Component, - HeaderExtractionOptions, + BareItem, + parseDictionary, + parseItem, + serializeItem, + serializeList, + Dictionary as DictionaryType, + ByteSequence, + serializeDictionary, + parseList, Parameters, - RequestLike, - ResponseLike, - SignOptions, -} from '../types'; -import { URL } from 'url'; + isInnerList, + isByteSequence, +} from 'structured-headers'; +import { Dictionary, parseHeader, quoteString } from '../structured-header'; +import { Request, Response, SignConfig, VerifyConfig, defaultParams, isRequest } from '../types'; -export const defaultSigningComponents: Component[] = [ - '@method', - '@path', - '@query', - '@authority', - 'content-type', - 'digest', - 'content-digest', -]; +export function deriveComponent(component: string, res: Response, req?: Request): string[]; +export function deriveComponent(component: string, req: Request): string[]; -export function extractHeader({ headers }: RequestLike | ResponseLike, header: string, opts?: HeaderExtractionOptions): string { - const lcHeader = header.toLowerCase(); - const key = Object.keys(headers).find((name) => name.toLowerCase() === lcHeader); - const allowMissing = opts?.allowMissing ?? true; - if (!allowMissing && !key) { - throw new Error(`Unable to extract header "${header}" from message`); +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +export function deriveComponent(component: string, message: Request | Response, req?: Request): string[] { + const [componentName, params] = parseItem(quoteString(component)); + // switch the context of the signing data depending on if the `req` flag was passed + const context = params.has('req') ? req : message; + if (!context) { + throw new Error('Missing request in request-response bound component'); } - let val = key ? headers[key] ?? '' : ''; - if (Array.isArray(val)) { - val = val.join(', '); - } - return val.toString().replace(/\s+/g, ' '); -} - -function populateDefaultParameters(parameters: Parameters) { - return { - created: new Date(), - ...parameters, - }; -} - -// see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-06#section-2.3 -export function extractComponent(message: RequestLike | ResponseLike, component: string): string { - switch (component) { + switch (componentName.toString().toLowerCase()) { case '@method': - return message.method.toUpperCase(); - case '@target-uri': - return message.url; + if (!isRequest(context)) { + throw new Error('Cannot derive @method from response'); + } + return [context.method.toUpperCase()]; + case '@target-uri': { + if (!isRequest(context)) { + throw new Error('Cannot derive @target-url on response'); + } + return [context.url.toString()]; + } case '@authority': { - const url = new URL(message.url); - const port = url.port ? parseInt(url.port, 10) : null; - return `${url.host}${port && ![80, 443].includes(port) ? `:${port}` : ''}`; + if (!isRequest(context)) { + throw new Error('Cannot derive @authority on response'); + } + const { port, protocol, hostname } = typeof context.url === 'string' ? new URL(context.url) : context.url; + let authority = hostname.toLowerCase(); + if (port && (protocol === 'http:' && port !== '80' || protocol === 'https:' && port !== '443')) { + authority += `:${port}`; + } + return [authority]; } case '@scheme': { - const { protocol } = new URL(message.url); - return protocol.slice(0, -1); + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { protocol } = typeof context.url === 'string' ? new URL(context.url) : context.url; + return [protocol.slice(0, -1)]; } case '@request-target': { - const { pathname, search } = new URL(message.url); - return `${pathname}${search}`; + if (!isRequest(context)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${pathname}${search}`]; } case '@path': { - const { pathname } = new URL(message.url); - return pathname; + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { pathname } = typeof context.url === 'string' ? new URL(context.url) : context.url; + return [decodeURI(pathname)]; } case '@query': { - const { search } = new URL(message.url); - return search; + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.7 + // absent query params means use `?` + return [decodeURI(search) || '?']; + } + case '@status': { + if (isRequest(context)) { + throw new Error('Cannot obtain @status component for requests'); + } + return [context.status.toString()]; } - case '@status': - if (!(message as ResponseLike).status) { - throw new Error(`${component} is only valid for responses`); + case '@query-param': { + if (!isRequest(context)) { + throw new Error('Cannot derive @scheme on response'); } - return (message as ResponseLike).status.toString(); - case '@query-params': - case '@request-response': - throw new Error(`${component} is not implemented yet`); + const { searchParams } = typeof context.url === 'string' ? new URL(context.url) : context.url; + if (!params.has('name')) { + throw new Error('@query-param must have a named parameter'); + } + const name = (params.get('name') as BareItem).toString(); + if (!searchParams.has(name)) { + throw new Error(`Expected query parameter "${name}" not found`); + } + return searchParams.getAll(name); + } default: - throw new Error(`Unknown specialty component ${component}`); + throw new Error(`Unsupported component "${component}"`); + } +} + +export function extractHeader(header: string, res: Response, req?: Request): string[]; +export function extractHeader(header: string, req: Request): string[]; + +export function extractHeader(header: string, { headers }: Request | Response, req?: Request): string[] { + const [headerName, params] = parseItem(quoteString(header)); + const context = params.has('req') ? req?.headers : headers; + if (!context) { + throw new Error('Missing request in request-response bound component'); + } + const lcHeaderName = headerName.toString().toLowerCase(); + const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === lcHeaderName); + if (!headerTuple) { + throw new Error(`No header "${headerName}" found in headers`); + } + const values = (Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]); + if (params.has('bs') && params.has('sf')) { + throw new Error('Invalid combination of parameters'); } + if (params.has('sf') || params.has('key')) { + // strict encoding of field + const value = values.join(', '); + const parsed = parseHeader(value); + if (params.has('key') && !(parsed instanceof Dictionary)) { + throw new Error('Unable to parse header as dictionary'); + } + if (params.has('key')) { + const key = (params.get('key') as BareItem).toString(); + if (!(parsed as Dictionary).has(key)) { + throw new Error(`Unable to find key "${key}" in structured field`); + } + return [(parsed as Dictionary).get(key) as string]; + } + return [parsed.toString()]; + } + if (params.has('bs')) { + return [values.map((val) => { + const encoded = Buffer.from(val.trim().replace(/\n\s*/gm, ' ')); + return `:${encoded.toString('base64')}:` + }).join(', ')]; + } + // raw encoding + return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} + +export function createSignatureBase(fields: string[], res: Response, req?: Request): [string, string[]][]; +export function createSignatureBase(fields: string[], req: Request): [string, string[]][]; + +export function createSignatureBase(fields: string[], res: Request | Response, req?: Request): [string, string[]][] { + return (fields).reduce<[string, string[]][]>((base, fieldName) => { + const [field, params] = parseItem(quoteString(fieldName)); + const lcFieldName = field.toString().toLowerCase(); + if (lcFieldName !== '@signature-params') { + const value = lcFieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); + base.push([serializeItem([lcFieldName, params]), value]); + } + return base; + }, []); } -export function buildSignatureInputString(componentNames: Component[], parameters: Parameters): string { - const components = componentNames.map((name) => `"${name.toLowerCase()}"`).join(' '); - return `(${components})${Object.entries(parameters).map(([parameter, value]) => { - if (typeof value === 'number') { - return `;${parameter}=${value}`; - } else if (value instanceof Date) { - return `;${parameter}=${Math.floor(value.getTime() / 1000)}`; - } else { - return `;${parameter}="${value.toString()}"`; - } - }).join('')}` +export function formatSignatureBase(base: [string, string[]][]): string { + return base.map(([key, value]) => { + const quotedKey = serializeItem(parseItem(quoteString(key))); + return value.map((val) => `${quotedKey}: ${val}`).join('\n'); + }).join('\n'); } -export function buildSignedData(request: RequestLike, components: Component[], signatureInputString: string): string { - const parts = components.map((component) => { - let value; - if (component.startsWith('@')) { - value = extractComponent(request, component); - } else { - value = extractHeader(request, component); - } - return`"${component.toLowerCase()}": ${value}` - }); - parts.push(`"@signature-params": ${signatureInputString}`); - return parts.join('\n'); +export function createSigningParameters(config: SignConfig): Parameters { + const now = new Date(); + return (config.params ?? defaultParams).reduce((params, paramName) => { + let value: string | number = ''; + switch (paramName.toLowerCase()) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created: Date = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } + default: + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); + } else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName] as string; + } + } + if (value) { + params.set(paramName, value); + } + return params; + }, new Map()); +} + +export function augmentHeaders(headers: Record, signature: Buffer, signatureInput: string, name?: string): Record { + let signatureHeaderName = 'Signature'; + let signatureInputHeaderName = 'Signature-Input'; + let signatureHeader: DictionaryType = new Map(); + let inputHeader: DictionaryType = new Map(); + // check to see if there are already signature/signature-input headers + // if there are we want to store the current (case-sensitive) name of the header + // and we want to parse out the current values so we can append our new signature + for (const header in headers) { + switch (header.toLowerCase()) { + case 'signature': { + signatureHeaderName = header; + signatureHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); + break; + } + case 'signature-input': + signatureInputHeaderName = header; + inputHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); + break; + } + } + // find a unique signature name for the header. Check if any existing headers already use + // the name we intend to use, if there are, add incrementing numbers to the signature name + // until we have a unique name to use + let signatureName = name ?? 'sig'; + if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) { + let count = 0; + while (signatureHeader?.has(`${signatureName}${count}`) || inputHeader?.has(`${signatureName}${count}`)) { + count++; + } + signatureName += count.toString(); + } + // append our signature and signature-inputs to the headers and return + signatureHeader.set(signatureName, [new ByteSequence(signature.toString('base64')), new Map()]); + inputHeader.set(signatureName, parseList(signatureInput)[0]); + return { + ...headers, + [signatureHeaderName]: serializeDictionary(signatureHeader), + [signatureInputHeaderName]: serializeDictionary(inputHeader), + }; +} + +export async function signMessage(config: SignConfig, res: T, req?: U): Promise; +export async function signMessage(config: SignConfig, req: T): Promise; + +export async function signMessage(config: SignConfig, message: T, req?: U): Promise { + const signingParameters = createSigningParameters(config); + const signatureBase = createSignatureBase(config?.fields ?? [], message as Response, req); + const signatureInput = serializeList([ + [ + signatureBase.map(([item]) => parseItem(item)), + signingParameters, + ], + ]); + signatureBase.push(['"@signature-params"', [signatureInput]]); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + return { + ...message, + headers: augmentHeaders({...message.headers}, signature, signatureInput, config.name), + }; } -// @todo - should be possible to sign responses too -export async function sign(request: RequestLike, opts: SignOptions): Promise { - const signingComponents: Component[] = opts.components ?? defaultSigningComponents; - const signingParams: Parameters = populateDefaultParameters({ - ...opts.parameters, - keyid: opts.keyId, - alg: opts.signer.alg, - }); - const signatureInputString = buildSignatureInputString(signingComponents, signingParams); - const dataToSign = buildSignedData(request, signingComponents, signatureInputString); - const signature = await opts.signer(Buffer.from(dataToSign)); - Object.assign(request.headers, { - 'Signature': `sig1=:${signature.toString('base64')}:`, - 'Signature-Input': `sig1=${signatureInputString}`, - }); - return request; +export async function verifyMessage(config: VerifyConfig, response: Response, request?: Request): Promise; +export async function verifyMessage(config: VerifyConfig, request: Request): Promise; + +export async function verifyMessage(config: VerifyConfig, message: Request | Response, req?: Request): Promise { + const { signatures, signatureInputs } = Object.entries(message.headers).reduce<{ signatures?: DictionaryType; signatureInputs?: DictionaryType }>((accum, [name, value]) => { + switch (name.toLowerCase()) { + case 'signature': + return Object.assign(accum, { + signatures: parseDictionary(Array.isArray(value) ? value.join(', ') : value), + }); + case 'signature-input': + return Object.assign(accum, { + signatureInputs: parseDictionary(Array.isArray(value) ? value.join(', ') : value), + }); + default: + return accum; + } + }, {}); + // no signatures means an indeterminate result + if (!signatures?.size && !signatureInputs?.size) { + return null; + } + // a missing header means we can't verify the signatures + if (!signatures?.size || !signatureInputs?.size) { + throw new Error('Incomplete signature headers'); + } + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + return Array.from(signatureInputs.entries()).reduce>(async (prev, [name, input]) => { + const result: Error | boolean | null = await prev.catch((e) => e); + if (!config.all && result === true) { + return result; + } + if (config.all && result !== true && result !== null) { + if (result instanceof Error) { + throw result; + } + return result; + } + if (!isInnerList(input)) { + throw new Error('Malformed signature input'); + } + const hasRequiredParams = requiredParams.every((param) => input[1].has(param)); + if (!hasRequiredParams) { + return false; + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field)); + if (!hasRequiredFields) { + return false; + } + if (input[1].has('created')) { + const created = input[1].get('created') as number - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if (maxAge && created - now > maxAge) { + return false; + } + // created after the allowed time (ie: created in the future) + if (created > notAfter) { + return false; + } + } + if (input[1].has('expires')) { + const expires = input[1].get('expires') as number + tolerance; + // expired signature + if (expires > now) { + return false; + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + const signingBase = createSignatureBase(input[0].map((item) => serializeItem(item)), message as Response, req); + signingBase.push(['"@signature-params"', [serializeList([input])]]); + const base = formatSignatureBase(signingBase); + const signature = signatures.get(name); + if (!signature) { + throw new Error('No signature found for inputs'); + } + if (!isByteSequence(signature[0] as BareItem)) { + throw new Error('Malformed signature'); + } + return config.verifier(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { + let val: Date | number | string; + switch (key.toLowerCase()) { + case 'created': + case 'expires': + val = new Date((value as number) * 1000); + break; + default: { + if (typeof value === 'string' || typeof value=== 'number') { + val = value; + } else { + val = value.toString(); + } + } + } + return Object.assign(params, { + [key]: val, + }); + }, {})); + }, Promise.resolve(null)); } diff --git a/src/httpbis/new.ts b/src/httpbis/new.ts deleted file mode 100644 index b21f394..0000000 --- a/src/httpbis/new.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { - BareItem, - parseDictionary, - parseItem, - serializeItem, - serializeList, - Dictionary as DictionaryType, - ByteSequence, - serializeDictionary, - parseList, - Parameters, - isInnerList, - isByteSequence, -} from 'structured-headers'; -import { Dictionary, parseHeader } from '../structured-header'; - -export interface Request { - method: string; - url: string | URL; - headers: Record; -} - -export interface Response { - status: number; - headers: Record; -} - -export type Signer = (data: Buffer) => Promise; -export type Verifier = (data: Buffer, signature: Buffer, parameters: SignatureParameters) => Promise; - -export interface SigningKey { - id?: string; - alg?: string; - sign: Signer; -} - -export interface SignatureParameters { - created?: Date | null; - expires?: Date; - nonce?: string; - alg?: string; - keyid?: string; - context?: string; - [param: string]: Date | number | string | null | undefined; -} - -const defaultParams = [ - 'keyid', - 'alg', - 'created', - 'expires', -]; - -export interface SignConfig { - key: SigningKey; - name?: string; - params?: string[]; - fields?: string[]; - paramValues?: SignatureParameters, -} - -export interface VerifyConfig { - verifier: Verifier; - notAfter?: Date | number; - maxAge?: number; - tolerance?: number; - requiredParams?: string[]; - requiredFields?: string[]; - all?: boolean; -} - -function isRequest(obj: Request | Response): obj is Request { - return !!(obj as Request).method; -} - -/** - * This allows consumers of the library to supply field specifications that aren't - * strictly "structured fields". Really a string must start with a `"` but that won't - * tend to happen in our configs. - * - * @param {string} input - * @returns {string} - */ -function quoteString(input: string): string { - // if it's not quoted, attempt to quote - if (!input.startsWith('"')) { - // try to split the structured field - const [name, ...rest] = input.split(';'); - // no params, just quote the whole thing - if (!rest.length) { - return `"${name}"`; - } - // quote the first part and put the rest back as it was - return `"${name}";${rest.join(';')}`; - } - return input; -} - -export function deriveComponent(component: string, res: Response, req?: Request): string[]; -export function deriveComponent(component: string, req: Request): string[]; - -/** - * Components can be derived from requests or responses (which can also be bound to their request). - * The signature is essentially (component, signingSubject, supplementaryData) - * - * @todo - Allow consumers to register their own component parser somehow - */ -export function deriveComponent(component: string, message: Request | Response, req?: Request): string[] { - const [componentName, params] = parseItem(quoteString(component)); - // switch the context of the signing data depending on if the `req` flag was passed - const context = params.has('req') ? req : message; - if (!context) { - throw new Error('Missing request in request-response bound component'); - } - switch (componentName.toString().toLowerCase()) { - case '@method': - if (!isRequest(context)) { - throw new Error('Cannot derive @method from response'); - } - return [context.method.toUpperCase()]; - case '@target-uri': { - if (!isRequest(context)) { - throw new Error('Cannot derive @target-url on response'); - } - return [context.url.toString()]; - } - case '@authority': { - if (!isRequest(context)) { - throw new Error('Cannot derive @authority on response'); - } - const { port, protocol, hostname } = typeof context.url === 'string' ? new URL(context.url) : context.url; - let authority = hostname.toLowerCase(); - if (port && (protocol === 'http:' && port !== '80' || protocol === 'https:' && port !== '443')) { - authority += `:${port}`; - } - return [authority]; - } - case '@scheme': { - if (!isRequest(context)) { - throw new Error('Cannot derive @scheme on response'); - } - const { protocol } = typeof context.url === 'string' ? new URL(context.url) : context.url; - return [protocol.slice(0, -1)]; - } - case '@request-target': { - if (!isRequest(context)) { - throw new Error('Cannot derive @request-target on response'); - } - const { pathname, search } = typeof context.url === 'string' ? new URL(context.url) : context.url; - // this is really sketchy because the request-target is actually what is in the raw HTTP header - // so one should avoid signing this value as the application layer just can't know how this - // is formatted - return [`${pathname}${search}`]; - } - case '@path': { - if (!isRequest(context)) { - throw new Error('Cannot derive @scheme on response'); - } - const {pathname} = typeof context.url === 'string' ? new URL(context.url) : context.url; - return [decodeURI(pathname)]; - } - case '@query': { - if (!isRequest(context)) { - throw new Error('Cannot derive @scheme on response'); - } - const { search } = typeof context.url === 'string' ? new URL(context.url) : context.url; - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.7 - // absent query params means use `?` - return [decodeURI(search) || '?']; - } - case '@status': { - if (isRequest(context)) { - throw new Error('Cannot obtain @status component for requests'); - } - return [context.status.toString()]; - } - case '@query-param': { - if (!isRequest(context)) { - throw new Error('Cannot derive @scheme on response'); - } - const { searchParams } = typeof context.url === 'string' ? new URL(context.url) : context.url; - if (!params.has('name')) { - throw new Error('@query-param must have a named parameter'); - } - const name = (params.get('name') as BareItem).toString(); - if (!searchParams.has(name)) { - throw new Error(`Expected query parameter "${name}" not found`); - } - return searchParams.getAll(name); - } - default: - throw new Error(`Unsupported component "${component}"`); - } -} - -export function extractHeader(header: string, res: Response, req?: Request): string[]; -export function extractHeader(header: string, req: Request): string[]; - -export function extractHeader(header: string, { headers }: Request | Response, req?: Request): string[] { - const [headerName, params] = parseItem(quoteString(header)); - const context = params.has('req') ? req?.headers : headers; - if (!context) { - throw new Error('Missing request in request-response bound component'); - } - const lcHeaderName = headerName.toString().toLowerCase(); - const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === lcHeaderName); - if (!headerTuple) { - throw new Error(`No header ${headerName} found in headers`); - } - const values = (Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]); - if (params.has('bs') && params.has('sf')) { - throw new Error('Invalid combination of parameters'); - } - if (params.has('sf') || params.has('key')) { - // strict encoding of field - // I think this is wrong as the values need to be combined first and then parsed, - // not parsed one-by-one - const value = values.join(', '); - const parsed = parseHeader(value); - if (params.has('key') && !(parsed instanceof Dictionary)) { - throw new Error('Unable to parse header as dictionary'); - } - if (params.has('key')) { - const key = (params.get('key') as BareItem).toString(); - if (!(parsed as Dictionary).has(key)) { - throw new Error(`Unable to find key "${key}" in structured field`); - } - return [(parsed as Dictionary).get(key) as string]; - } - return [parsed.toString()]; - } - if (params.has('bs')) { - return [values.map((val) => { - const encoded = Buffer.from(val.trim().replace(/\n\s*/gm, ' ')); - return `:${encoded.toString('base64')}:` - }).join(', ')]; - } - // raw encoding - return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; -} - -export function createSignatureBase(fields: string[], res: Response, req?: Request): [string, string[]][]; -export function createSignatureBase(fields: string[], req: Request): [string, string[]][]; - -export function createSignatureBase(fields: string[], res: Request | Response, req?: Request): [string, string[]][] { - return (fields).reduce<[string, string[]][]>((base, fieldName) => { - const [field, params] = parseItem(quoteString(fieldName)); - const lcFieldName = field.toString().toLowerCase(); - if (lcFieldName !== '@signature-params') { - const value = lcFieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); - base.push([serializeItem([lcFieldName, params]), value]); - } - return base; - }, []); -} - -export function formatSignatureBase(base: [string, string[]][]): string { - return base.map(([key, value]) => { - const quotedKey = serializeItem(parseItem(quoteString(key))); - return value.map((val) => `${quotedKey}: ${val}`).join('\n'); - }).join('\n'); -} - -export function createSigningParameters(config: SignConfig): Parameters { - const now = new Date(); - return (config.params ?? defaultParams).reduce((params, paramName) => { - let value: string | number = ''; - switch (paramName) { - case 'created': - // created is optional but recommended. If created is supplied but is null, that's an explicit - // instruction to *not* include the created parameter - if (config.paramValues?.created !== null) { - const created: Date = config.paramValues?.created ?? now; - value = Math.floor(created.getTime() / 1000); - } - break; - case 'expires': - // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after - // creation. Don't add an expires time if there is no created time - if (config.paramValues?.expires || config.paramValues?.created !== null) { - const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); - value = Math.floor(expires.getTime() / 1000); - } - break; - case 'keyid': { - // attempt to obtain the keyid omit if missing - const kid = config.paramValues?.keyid ?? config.key.id ?? null; - if (kid) { - value = kid.toString(); - } - break; - } - case 'alg': { - const alg = config.paramValues?.alg ?? config.key.alg ?? null; - if (alg) { - value = alg.toString(); - } - break; - } - default: - if (config.paramValues?.[paramName] instanceof Date) { - value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString(); - } else if (config.paramValues?.[paramName]) { - value = config.paramValues[paramName] as string; - } - } - if (value) { - params.set(paramName, value); - } - return params; - }, new Map()); -} - -export function augmentHeaders(headers: Record, signature: Buffer, signatureInput: string, name?: string): Record { - let signatureHeaderName = 'Signature'; - let signatureInputHeaderName = 'Signature-Input'; - let signatureHeader: DictionaryType = new Map(); - let inputHeader: DictionaryType = new Map(); - // check to see if there are already signature/signature-input headers - // if there are we want to store the current (case-sensitive) name of the header - // and we want to parse out the current values so we can append our new signature - for (const header in headers) { - switch (header.toLowerCase()) { - case 'signature': { - signatureHeaderName = header; - signatureHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); - break; - } - case 'signature-input': - signatureInputHeaderName = header; - inputHeader = parseDictionary(Array.isArray(headers[header]) ? (headers[header] as string[]).join(', ') : headers[header] as string); - break; - } - } - // find a unique signature name for the header. Check if any existing headers already use - // the name we intend to use, if there are, add incrementing numbers to the signature name - // until we have a unique name to use - let signatureName = name ?? 'sig'; - if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) { - let count = 0; - while (signatureHeader?.has(`${signatureName}${count}`) || inputHeader?.has(`${signatureName}${count}`)) { - count++; - } - signatureName += count.toString(); - } - // append our signature and signature-inputs to the headers and return - signatureHeader.set(signatureName, [new ByteSequence(signature.toString('base64')), new Map()]); - inputHeader.set(signatureName, parseList(signatureInput)[0]); - return { - ...headers, - [signatureHeaderName]: serializeDictionary(signatureHeader), - [signatureInputHeaderName]: serializeDictionary(inputHeader), - }; -} - -export async function signMessage(config: SignConfig, res: T, req?: U): Promise; -export async function signMessage(config: SignConfig, req: T): Promise; - -export async function signMessage(config: SignConfig, message: T, req?: U): Promise { - const signingParameters = createSigningParameters(config); - const signatureBase = createSignatureBase(config?.fields ?? [], message as Response, req); - const signatureInput = serializeList([ - [ - signatureBase.map(([item]) => parseItem(item)), - signingParameters, - ], - ]); - signatureBase.push(['"@signature-params"', [signatureInput]]); - const base = formatSignatureBase(signatureBase); - // call sign - const signature = await config.key.sign(Buffer.from(base)); - return { - ...message, - headers: augmentHeaders({...message.headers}, signature, signatureInput, config.name), - }; -} - -export async function verifyMessage(config: VerifyConfig, response: Response, request?: Request): Promise; -export async function verifyMessage(config: VerifyConfig, request: Request): Promise; - -export async function verifyMessage(config: VerifyConfig, message: Request | Response, req?: Request): Promise { - const { signatures, signatureInputs } = Object.entries(message.headers).reduce<{ signatures?: DictionaryType; signatureInputs?: DictionaryType }>((accum, [name, value]) => { - switch (name.toLowerCase()) { - case 'signature': - return Object.assign(accum, { - signatures: parseDictionary(Array.isArray(value) ? value.join(', ') : value), - }); - case 'signature-input': - return Object.assign(accum, { - signatureInputs: parseDictionary(Array.isArray(value) ? value.join(', ') : value), - }); - default: - return accum; - } - }, {}); - // no signatures means an indeterminate result - if (!signatures?.size && !signatureInputs?.size) { - return null; - } - // a missing header means we can't verify the signatures - if (!signatures?.size || !signatureInputs?.size) { - throw new Error('Incomplete signature headers'); - } - const now = Math.floor(Date.now() / 1000); - const tolerance = config.tolerance ?? 0; - const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; - const maxAge = config.maxAge ?? null; - const requiredParams = config.requiredParams ?? []; - const requiredFields = config.requiredFields ?? []; - return Array.from(signatureInputs.entries()).reduce>(async (prev, [name, input]) => { - const result: Error | boolean | null = await prev.catch((e) => e); - if (!config.all && result === true) { - return result; - } - if (config.all && result !== true && result !== null) { - if (result instanceof Error) { - throw result; - } - return result; - } - if (!isInnerList(input)) { - throw new Error('Malformed signature input'); - } - const hasRequiredParams = requiredParams.every((param) => input[1].has(param)); - if (!hasRequiredParams) { - return false; - } - // this could be tricky, what if we say "@method" but there is "@method;req" - const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field)); - if (!hasRequiredFields) { - return false; - } - if (input[1].has('created')) { - const created = input[1].get('created') as number - tolerance; - // maxAge overrides expires. - // signature is older than maxAge - if (maxAge && created - now > maxAge) { - return false; - } - // created after the allowed time (ie: created in the future) - if (created > notAfter) { - return false; - } - } - if (input[1].has('expires')) { - const expires = input[1].get('expires') as number + tolerance; - // expired signature - if (expires > now) { - return false; - } - } - // now look to verify the signature! Build the expected "signing base" and verify it! - const signingBase = createSignatureBase(input[0].map((item) => serializeItem(item)), message as Response, req); - signingBase.push(['"@signature-params"', [serializeList([input])]]); - const base = formatSignatureBase(signingBase); - const signature = signatures.get(name); - if (!signature) { - throw new Error('No signature found for inputs'); - } - if (!isByteSequence(signature[0] as BareItem)) { - throw new Error('Malformed signature'); - } - return config.verifier(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { - let val: Date | number | string; - switch (key.toLowerCase()) { - case 'created': - case 'expires': - val = new Date((value as number) * 1000); - break; - default: { - if (typeof value === 'string' || typeof value=== 'number') { - val = value; - } else { - val = value.toString(); - } - } - } - return Object.assign(params, { - [key]: val, - }); - }, {})); - }, Promise.resolve(null)); -} diff --git a/src/structured-header.ts b/src/structured-header.ts index 6121aa9..26c08e2 100644 --- a/src/structured-header.ts +++ b/src/structured-header.ts @@ -89,3 +89,26 @@ export function parseHeader(header: string): List | Dictionary | Item { } throw new Error('Unable to parse header as structured field'); } + +/** + * This allows consumers of the library to supply field specifications that aren't + * strictly "structured fields". Really a string must start with a `"` but that won't + * tend to happen in our configs. + * + * @param {string} input + * @returns {string} + */ +export function quoteString(input: string): string { + // if it's not quoted, attempt to quote + if (!input.startsWith('"')) { + // try to split the structured field + const [name, ...rest] = input.split(';'); + // no params, just quote the whole thing + if (!rest.length) { + return `"${name}"`; + } + // quote the first part and put the rest back as it was + return `"${name}";${rest.join(';')}`; + } + return input; +} diff --git a/src/types/index.ts b/src/types/index.ts index ce36d2c..46c0b4f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,42 +1,131 @@ -import { Signer, Verifier } from '../algorithm'; - -type HttpLike = { - method: string, - url: string, - headers: Record, +export interface Request { + method: string; + url: string | URL; + headers: Record; } -export type RequestLike = HttpLike; - -export type ResponseLike = HttpLike & { - status: number, +export interface Response { + status: number; + headers: Record; } -// see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-06#section-2.3.1 -export type Parameter = 'created' | 'expires' | 'nonce' | 'alg' | 'keyid' | string; +export type Signer = (data: Buffer) => Promise; +export type Verifier = (data: Buffer, signature: Buffer, parameters?: SignatureParameters) => Promise; -export type Component = '@method' | '@target-uri' | '@authority' | '@scheme' | '@request-target' | '@path' | '@query' | '@query-params' | string; +export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512' | string; -export type ResponseComponent = '@status' | '@request-response' | Component; - -export type Parameters = { [name: Parameter]: string | number | Date | { [Symbol.toStringTag]: () => string } }; +export interface SigningKey { + id?: string; + alg?: Algorithm; + sign: Signer; +} -type CommonOptions = { - format: 'httpbis' | 'cavage', +/** + * The signature parameters to include in signing + */ +export interface SignatureParameters { + /** + * The created time for the signature. `null` indicates not to populate the `created` time + * default: Date.now() + */ + created?: Date | null; + /** + * The time the signature should be deemed to have expired + * default: Date.now() + 5 mins + */ + expires?: Date; + /** + * A nonce for the request + */ + nonce?: string; + /** + * The algorithm the signature is signed with (overrides the alg provided by the signing key) + */ + alg?: string; + /** + * The key id the signature is signed with (overrides the keyid provided by the signing key) + */ + keyid?: string; + /** + * A context parameter for the signature + */ + context?: string; + [param: string]: Date | number | string | null | undefined; } -export type SignOptions = CommonOptions & { - components?: Component[], - parameters?: Parameters, - allowMissingHeaders?: boolean, - keyId: string, - signer: Signer, -}; +/** + * Default parameters to use when signing a request if none are supplied by the consumer + */ +export const defaultParams = [ + 'keyid', + 'alg', + 'created', + 'expires', +]; -export type VerifyOptions = CommonOptions & { - verifier: Verifier, +export interface SignConfig { + key: SigningKey; + /** + * The name to try to use for the signature + * Default: 'sig' + */ + name?: string; + /** + * The parameters to add to the signature + * Default: see defaultParams + */ + params?: string[]; + /** + * The HTTP fields / derived component names to sign + * Default: none + */ + fields?: string[]; + /** + * Specified parameter values to use (eg: created time, expires time, etc) + * This can be used by consumers to override the default expiration time or explicitly opt-out + * of adding creation time (by setting `created: null`) + */ + paramValues?: SignatureParameters, } -export type HeaderExtractionOptions = { - allowMissing: boolean, -}; +/** + * Options when verifying signatures + */ +export interface VerifyConfig { + verifier: Verifier; + /** + * A maximum age for the signature + * Default: Date.now() + tolerance + */ + notAfter?: Date | number; + /** + * The maximum age of the signature - this overrides the `expires` value for the signature + * if provided + */ + maxAge?: number; + /** + * A clock tolerance when verifying created/expires times + * Default: 0 + */ + tolerance?: number; + /** + * Any parameters that *must* be in the signature (eg: require a created time) + * Default: [] + */ + requiredParams?: string[]; + /** + * Any fields that *must* be in the signature (eg: Authorization, Digest, etc) + * Default: [] + */ + requiredFields?: string[]; + /** + * Verify every signature in the request. By default, only 1 signature will need to be valid + * for the verification to pass. + * Default: false + */ + all?: boolean; +} + +export function isRequest(obj: Request | Response): obj is Request { + return !!(obj as Request).method; +} diff --git a/test/algorithm/ecdsa-p256-sha256.ts b/test/algorithm/ecdsa-p256-sha256.ts index f451764..8f16dd1 100644 --- a/test/algorithm/ecdsa-p256-sha256.ts +++ b/test/algorithm/ecdsa-p256-sha256.ts @@ -23,19 +23,18 @@ describe('ecdsa-p256-sha256', () => { }); describe('signing', () => { it('signs a payload', async () => { - const signer = createSigner('ecdsa-p256-sha256', ecdsaKeyPair.privateKey); - const data = 'some random data'; - const sig = await signer(data); + const signer = createSigner(ecdsaKeyPair.privateKey, 'ecdsa-p256-sha256'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); expect(signer.alg).to.equal('ecdsa-p256-sha256'); - expect(sig).to.satisfy((arg: Buffer) => verify('sha256', Buffer.from(data), ecdsaKeyPair.publicKey, arg)); + expect(sig).to.satisfy((arg: Buffer) => verify('sha256', data, ecdsaKeyPair.publicKey, arg)); }); }); describe('verifying', () => { it('verifies a signature', async () => { - const verifier = createVerifier('ecdsa-p256-sha256', ecdsaKeyPair.publicKey); - const data = 'some random data'; - const sig = sign('sha512', Buffer.from(data), ecdsaKeyPair.privateKey); - expect(verifier.alg).to.equal('ecdsa-p256-sha256'); + const verifier = createVerifier(ecdsaKeyPair.publicKey, 'ecdsa-p256-sha256'); + const data = Buffer.from('some random data'); + const sig = sign('sha512', data, ecdsaKeyPair.privateKey); expect(sig).to.satisfy((arg: Buffer) => verifier(data, arg)); }); }); @@ -46,20 +45,20 @@ describe('ecdsa-p256-sha256', () => { ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/ecdsa-p256.pem'))).toString(); }); describe('response signing', () => { - const data = '"content-type": application/json\n' + + const data = Buffer.from('"content-type": application/json\n' + '"digest": SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + '"content-length": 18\n' + - '"@signature-params": ("content-type" "digest" "content-length");created=1618884475;keyid="test-key-ecc-p256"'; + '"@signature-params": ("content-type" "digest" "content-length");created=1618884475;keyid="test-key-ecc-p256"'); it('successfully signs a payload', async () => { - const sig = await (createSigner('ecdsa-p256-sha256', ecKeyPem)(data)); - expect(sig).to.satisfy((arg: Buffer) => verify('sha256', Buffer.from(data), ecKeyPem, arg)); + const sig = await (createSigner(ecKeyPem, 'ecdsa-p256-sha256').sign(data)); + expect(sig).to.satisfy((arg: Buffer) => verify('sha256', data, ecKeyPem, arg)); }); // seems to be broken in node - Error: error:0D07209B:asn1 encoding routines:ASN1_get_object:too long // could be to do with https://stackoverflow.com/a/39575576 it.skip('successfully verifies a signature', async () => { const sig = Buffer.from('n8RKXkj0iseWDmC6PNSQ1GX2R9650v+lhbb6rTGoSrSSx18zmn6fPOtBx48/WffYLO0n1RHHf9scvNGAgGq52Q==', 'base64'); expect(sig).to.satisfy((arg: Buffer) => verify('sha256', Buffer.from(data), ecKeyPem, arg)); - expect(await (createVerifier('ecdsa-p256-sha256', ecKeyPem)(data, sig))).to.equal(true); + expect(await (createVerifier(ecKeyPem, 'ecdsa-p256-sha256')(data, sig))).to.equal(true); }); }); }); diff --git a/test/algorithm/hmac-sha256.ts b/test/algorithm/hmac-sha256.ts index cd47d89..93f511c 100644 --- a/test/algorithm/hmac-sha256.ts +++ b/test/algorithm/hmac-sha256.ts @@ -4,16 +4,15 @@ import { expect } from 'chai'; describe('hmac-sha256', () => { // examples from wikipedia https://en.wikipedia.org/w/index.php?title=HMAC&oldid=1046955366#Examples describe('internal tests', () => { - const data = 'The quick brown fox jumps over the lazy dog'; + const data = Buffer.from('The quick brown fox jumps over the lazy dog'); const sig = Buffer.from('f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8', 'hex'); it('signs a payload correctly', async () => { - const hmac = createSigner('hmac-sha256', 'key'); + const hmac = createSigner('key', 'hmac-sha256'); expect(hmac.alg).to.equal('hmac-sha256'); - expect(await hmac(data)).to.deep.equal(sig); + expect(await hmac.sign(data)).to.deep.equal(sig); }); it('verifies a payload correctly', async () => { - const hmac = createVerifier('hmac-sha256', 'key'); - expect(hmac.alg).to.equal('hmac-sha256'); + const hmac = createVerifier('key', 'hmac-sha256'); expect(await hmac(data, sig)).to.equal(true); }); }); @@ -21,19 +20,18 @@ describe('hmac-sha256', () => { describe('specification examples', () => { const testSharedSecret = Buffer.from('uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==', 'base64'); const expectedSig = Buffer.from('fN3AMNGbx0V/cIEKkZOvLOoC3InI+lM2+gTv22x3ia8=', 'base64'); - const signatureInput = '"@authority": example.com\n' + + const signatureInput = Buffer.from('"@authority": example.com\n' + '"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + '"content-type": application/json\n' + - '"@signature-params": ("@authority" "date" "content-type");created=1618884475;keyid="test-shared-secret"'; + '"@signature-params": ("@authority" "date" "content-type");created=1618884475;keyid="test-shared-secret"'); it('generates an expected hmac', async () => { - const hmac = createSigner('hmac-sha256', testSharedSecret); - const sig = await hmac(signatureInput); + const hmac = createSigner(testSharedSecret, 'hmac-sha256'); + const sig = await hmac.sign(signatureInput); expect(hmac.alg).to.equal('hmac-sha256'); expect(sig).to.deep.equal(expectedSig); }); it('verifies a provided signature', async () => { - const hmac = createVerifier('hmac-sha256', testSharedSecret); - expect(hmac.alg).to.equal('hmac-sha256'); + const hmac = createVerifier(testSharedSecret, 'hmac-sha256'); expect(await hmac(signatureInput, expectedSig)).to.equal(true); }); }); diff --git a/test/algorithm/rsa-pkcs1-sha1.ts b/test/algorithm/rsa-pkcs1-sha1.ts new file mode 100644 index 0000000..00a0ad5 --- /dev/null +++ b/test/algorithm/rsa-pkcs1-sha1.ts @@ -0,0 +1,40 @@ +import { createSign, generateKeyPair, publicDecrypt } from 'crypto'; +import { promisify } from 'util'; +import { createSigner, createVerifier } from '../../src'; +import { expect } from 'chai'; +import { RSA_PKCS1_PADDING } from 'constants'; + +describe('rsa-v1_5-sha1', () => { + let rsaKeyPair: { publicKey: string, privateKey: string }; + before('generate key pair', async () => { + rsaKeyPair = await promisify(generateKeyPair)('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + }); + describe('signing', () => { + it('signs a payload', async () => { + const signer = createSigner(rsaKeyPair.privateKey, 'rsa-v1_5-sha1'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); + expect(signer.alg).to.equal('rsa-v1_5-sha1'); + expect(sig).to.satisfy((arg: Buffer) => publicDecrypt({ key: rsaKeyPair.publicKey, padding: RSA_PKCS1_PADDING }, arg)); + }); + }); + describe('verifying', () => { + it('verifies a signature', async () => { + const verifier = createVerifier(rsaKeyPair.publicKey, 'rsa-v1_5-sha1'); + const data = Buffer.from('some random data'); + const sig = createSign('sha1').update(data).sign({ key: rsaKeyPair.privateKey, padding: RSA_PKCS1_PADDING }); + const verified = await verifier(data, sig); + expect(verified).to.equal(true); + }); + }); +}); diff --git a/test/algorithm/rsa-pkcs1-sha256.ts b/test/algorithm/rsa-pkcs1-sha256.ts index 7acd9a1..ac51bd3 100644 --- a/test/algorithm/rsa-pkcs1-sha256.ts +++ b/test/algorithm/rsa-pkcs1-sha256.ts @@ -1,4 +1,4 @@ -import { generateKeyPair, privateEncrypt, publicDecrypt } from 'crypto'; +import { createSign, generateKeyPair, publicDecrypt } from 'crypto'; import { promisify } from 'util'; import { createSigner, createVerifier } from '../../src'; import { expect } from 'chai'; @@ -21,20 +21,19 @@ describe('rsa-v1_5-sha256', () => { }); describe('signing', () => { it('signs a payload', async () => { - const signer = createSigner('rsa-v1_5-sha256', rsaKeyPair.privateKey); - const data = 'some random data'; - const sig = await signer(data); + const signer = createSigner(rsaKeyPair.privateKey, 'rsa-v1_5-sha256'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); expect(signer.alg).to.equal('rsa-v1_5-sha256'); expect(sig).to.satisfy((arg: Buffer) => publicDecrypt({ key: rsaKeyPair.publicKey, padding: RSA_PKCS1_PADDING }, arg)); }); }); describe('verifying', () => { - it.skip('verifies a signature', async () => { - const verifier = createVerifier('rsa-v1_5-sha256', rsaKeyPair.publicKey); - const data = 'some random data'; - const sig = privateEncrypt({ key: rsaKeyPair.privateKey, padding: RSA_PKCS1_PADDING }, Buffer.from(data)); + it('verifies a signature', async () => { + const verifier = createVerifier(rsaKeyPair.publicKey, 'rsa-v1_5-sha256'); + const data = Buffer.from('some random data'); + const sig = createSign('sha256').update(data).sign({ key: rsaKeyPair.privateKey, padding: RSA_PKCS1_PADDING }); const verified = await verifier(data, sig); - expect(verifier.alg).to.equal('rsa-v1_5-sha256'); expect(verified).to.equal(true); }); }); diff --git a/test/algorithm/rsa-pss-sha512.ts b/test/algorithm/rsa-pss-sha512.ts index 6002791..1206f8f 100644 --- a/test/algorithm/rsa-pss-sha512.ts +++ b/test/algorithm/rsa-pss-sha512.ts @@ -24,11 +24,11 @@ describe('rsa-pss-sha512', () => { }); describe('signing', () => { it('signs a payload', async () => { - const signer = createSigner('rsa-pss-sha512', rsaKeyPair.privateKey); - const data = 'some random data'; - const sig = await signer(data); + const signer = createSigner(rsaKeyPair.privateKey, 'rsa-pss-sha512'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); expect(signer.alg).to.equal('rsa-pss-sha512'); - expect(sig).to.satisfy((arg: Buffer) => verify('sha512', Buffer.from(data), { + expect(sig).to.satisfy((arg: Buffer) => verify('sha512', data, { key: rsaKeyPair.publicKey, padding: RSA_PKCS1_PSS_PADDING, }, arg)); @@ -36,13 +36,12 @@ describe('rsa-pss-sha512', () => { }); describe('verifying', () => { it('verifies a signature', async () => { - const verifier = createVerifier('rsa-pss-sha512', rsaKeyPair.publicKey); - const data = 'some random data'; - const sig = sign('sha512', Buffer.from(data), { + const verifier = createVerifier(rsaKeyPair.publicKey, 'rsa-pss-sha512'); + const data = Buffer.from('some random data'); + const sig = sign('sha512', data, { key: rsaKeyPair.privateKey, padding: RSA_PKCS1_PSS_PADDING, }); - expect(verifier.alg).to.equal('rsa-pss-sha512'); expect(sig).to.satisfy((arg: Buffer) => verifier(data, arg)); }); }); @@ -53,9 +52,9 @@ describe('rsa-pss-sha512', () => { rsaKeyPem = (await promisify(readFile)(join(__dirname, '../etc/rsa-pss.pem'))).toString(); }); describe('minimal example', () => { - const data = '"@signature-params": ();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'; + const data = Buffer.from('"@signature-params": ();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'); it('successfully signs a payload', async () => { - const sig = await createSigner('rsa-pss-sha512', rsaKeyPem)(data); + const sig = await createSigner(rsaKeyPem, 'rsa-pss-sha512').sign(data); expect(sig).to.satisfy((arg: Buffer) => verify('sha512', Buffer.from(data), { key: rsaKeyPem, padding: RSA_PKCS1_PSS_PADDING, @@ -68,15 +67,15 @@ describe('rsa-pss-sha512', () => { 'cZgLxVwialuH5VnqJS4JN8PHD91XLfkjMscTo4jmVMpFd3iLVe0hqVFl7MDt6TMkw' + 'IyVFnEZ7B/VIQofdShO+C/7MuupCSLVjQz5xA+Zs6Hw+W9ESD/6BuGs6LF1TcKLxW' + '+5K+2zvDY/Cia34HNpRW5io7Iv9/b7iQ==', 'base64'); - expect(await createVerifier('rsa-pss-sha512', rsaKeyPem)(data, sig)).to.equal(true); + expect(await createVerifier(rsaKeyPem, 'rsa-pss-sha512')(data, sig)).to.equal(true); }); }); describe('selective example', () => { - const data = '"@authority": example.com\n' + + const data = Buffer.from('"@authority": example.com\n' + '"content-type": application/json\n' + - '"@signature-params": ("@authority" "content-type");created=1618884475;keyid="test-key-rsa-pss"'; + '"@signature-params": ("@authority" "content-type");created=1618884475;keyid="test-key-rsa-pss"'); it('successfully signs a payload', async () => { - const sig = await createSigner('rsa-pss-sha512', rsaKeyPem)(data); + const sig = await createSigner(rsaKeyPem, 'rsa-pss-sha512').sign(data); expect(sig).to.satisfy((arg: Buffer) => verify('sha512', Buffer.from(data), { key: rsaKeyPem, padding: RSA_PKCS1_PSS_PADDING, @@ -89,11 +88,11 @@ describe('rsa-pss-sha512', () => { 'uy2SfZJUhsJqZyEWRk4204x7YEB3VxDAAlVgGt8ewilWbIKKTOKp3ymUeQIwptqYw' + 'v0l8mN404PPzRBTpB7+HpClyK4CNp+SVv46+6sHMfJU4taz10s/NoYRmYCGXyadzY' + 'YDj0BYnFdERB6NblI/AOWFGl5Axhhmjg==', 'base64'); - expect(await createVerifier('rsa-pss-sha512', rsaKeyPem)(data, sig)).to.equal(true); + expect(await createVerifier(rsaKeyPem, 'rsa-pss-sha512')(data, sig)).to.equal(true); }); }); describe('full example', () => { - const data = '"date": Tue, 20 Apr 2021 02:07:56 GMT\n' + + const data = Buffer.from('"date": Tue, 20 Apr 2021 02:07:56 GMT\n' + '"@method": POST\n' + '"@path": /foo\n' + '"@query": ?param=value&pet=dog\n' + @@ -101,9 +100,9 @@ describe('rsa-pss-sha512', () => { '"content-type": application/json\n' + '"digest": SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + '"content-length": 18\n' + - '"@signature-params": ("date" "@method" "@path" "@query" "@authority" "content-type" "digest" "content-length");created=1618884475;keyid="test-key-rsa-pss"'; + '"@signature-params": ("date" "@method" "@path" "@query" "@authority" "content-type" "digest" "content-length");created=1618884475;keyid="test-key-rsa-pss"'); it('successfully signs a payload', async () => { - const sig = await createSigner('rsa-pss-sha512', rsaKeyPem)(data); + const sig = await createSigner(rsaKeyPem, 'rsa-pss-sha512').sign(data); expect(sig).to.satisfy((arg: Buffer) => verify('sha512', Buffer.from(data), { key: rsaKeyPem, padding: RSA_PKCS1_PSS_PADDING, @@ -116,7 +115,7 @@ describe('rsa-pss-sha512', () => { 'T/oBtxPtAn1eFjUyIKyA+XD7kYph82I+ahvm0pSgDPagu917SlqUjeaQaNnlZzO03' + 'Iy1RZ5XpgbNeDLCqSLuZFVID80EohC2CQ1cL5svjslrlCNstd2JCLmhjL7xV3NYXe' + 'rLim4bqUQGRgDwNJRnqobpS6C1NBns/Q==', 'base64'); - expect(await createVerifier('rsa-pss-sha512', rsaKeyPem)(data, sig)).to.equal(true); + expect(await createVerifier(rsaKeyPem, 'rsa-pss-sha512')(data, sig)).to.equal(true); }); }); }); diff --git a/test/cavage/cavage.ts b/test/cavage/cavage.ts index dad02d5..7e45529 100644 --- a/test/cavage/cavage.ts +++ b/test/cavage/cavage.ts @@ -1,26 +1,11 @@ -import { RequestLike } from '../../src'; -import { buildSignatureInputString, buildSignedData } from '../../src/cavage'; +import { Request } from '../../src'; +import { createSignatureBase, formatSignatureBase } from '../../src/cavage'; import { expect } from 'chai'; describe('cavage', () => { - describe('.buildSignatureInputString', () => { - describe('specification tests', () => { - it('creates an input string', () => { - const inputString = buildSignatureInputString(['@request-target', 'Host', 'Date', 'Digest', 'Content-Length'], { - keyid: 'rsa-key-1', - alg: 'hs2019', - created: new Date(1402170695000), - expires: new Date(1402170995000), - }); - expect(inputString).to.equal('keyId="rsa-key-1",algorithm="hs2019",' + - 'created=1402170695,expires=1402170995,' + - 'headers="(request-target) (created) (expires) host date digest content-length"') - }); - }); - }); describe('.buildSignedData', () => { describe('specification examples', () => { - const testRequest: RequestLike = { + const testRequest: Request = { method: 'GET', url: 'https://example.org/foo', headers: { @@ -32,16 +17,15 @@ describe('cavage', () => { }, }; it('builds the signed data payload', () => { - const payload = buildSignedData(testRequest, [ + const payload = formatSignatureBase(createSignatureBase([ '@request-target', + '@created', 'host', 'date', 'cache-control', 'x-emptyheader', 'x-example', - ], { - created: new Date(1402170695000), - }); + ], testRequest, new Map([['created', 1402170695]]))); expect(payload).to.equal('(request-target): get /foo\n' + '(created): 1402170695\n' + 'host: example.org\n' + diff --git a/test/cavage/new.spec.ts b/test/cavage/new.spec.ts index 59f18e6..620f63c 100644 --- a/test/cavage/new.spec.ts +++ b/test/cavage/new.spec.ts @@ -1,4 +1,5 @@ -import * as cavage from '../../src/cavage/new'; +import * as cavage from '../../src/cavage'; +import { Request, Response, SigningKey } from '../../src'; import { expect } from 'chai'; import { describe } from 'mocha'; import * as MockDate from 'mockdate'; @@ -9,7 +10,7 @@ describe('cavage', () => { describe('.deriveComponent', () => { describe('unbound components', () => { it('derives @request-target', () => { - const req: cavage.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -24,7 +25,7 @@ describe('cavage', () => { }); describe('.extractHeader', () => { describe('raw headers', () => { - const request: cavage.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -50,7 +51,7 @@ describe('cavage', () => { }); describe('.createSignatureBase', () => { describe('header fields', () => { - const request: cavage.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -89,7 +90,7 @@ describe('cavage', () => { }); }); describe('derived components', () => { - const request: cavage.Request = { + const request: Request = { method: 'post', url: 'https://www.example.com/path?param=value', headers: { @@ -103,7 +104,7 @@ describe('cavage', () => { }); }); describe('full example', () => { - const request: cavage.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -299,7 +300,7 @@ describe('cavage', () => { }); describe('.signMessage', () => { describe('requests', () => { - const request: cavage.Request = { + const request: Request = { method: 'post', url: 'https://example.org/foo', headers: { @@ -310,7 +311,7 @@ describe('cavage', () => { 'Content-Length': '18', }, }; - let signer: cavage.SigningKey; + let signer: SigningKey; beforeEach('stub signer', () => { signer = { sign: stub().resolves(Buffer.from('a fake signature')), @@ -359,7 +360,7 @@ describe('cavage', () => { }); }); describe('responses', () => { - const response: cavage.Response = { + const response: Response = { status: 503, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', @@ -367,7 +368,7 @@ describe('cavage', () => { 'Content-Length': '62', }, }; - let signer: cavage.SigningKey; + let signer: SigningKey; beforeEach('stub signer', () => { signer = { sign: stub().resolves(Buffer.from('a fake signature')), @@ -398,7 +399,7 @@ describe('cavage', () => { }); describe('.verifyMessage', () => { describe('requests', () => { - const request: cavage.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=value&pet=dog', headers: { @@ -437,7 +438,7 @@ describe('cavage', () => { }); }); describe('responses', () => { - const response: cavage.Response = { + const response: Response = { status: 200, headers: { 'Date': 'Tue, 07 Jun 2014 20:51:35 GMT', diff --git a/test/httpbis/httpbis.ts b/test/httpbis/httpbis.ts index 3dd5fc4..b7aac22 100644 --- a/test/httpbis/httpbis.ts +++ b/test/httpbis/httpbis.ts @@ -1,5 +1,8 @@ -import { Component, Parameters, RequestLike } from '../../src'; -import { buildSignatureInputString, buildSignedData, extractComponent, extractHeader } from '../../src/httpbis'; +import { Request } from '../../src'; +import { + deriveComponent, + extractHeader, +} from '../../src/httpbis'; import { expect } from 'chai'; describe('httpbis', () => { @@ -13,178 +16,75 @@ describe('httpbis', () => { }; Object.entries(headers).forEach(([headerName, expectedValue]) => { it(`successfully extracts a matching header (${headerName})`, () => { - expect(extractHeader({ headers } as unknown as RequestLike, headerName)).to.equal(expectedValue); + expect(extractHeader( headerName, { headers } as unknown as Request)).to.deep.equal([expectedValue]); }); it(`successfully extracts a lower cased header (${headerName})`, () => { - expect(extractHeader({ headers } as unknown as RequestLike, headerName.toLowerCase())).to.equal(expectedValue); + expect(extractHeader( headerName.toLowerCase(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); }); it(`successfully extracts an upper cased header (${headerName})`, () => { - expect(extractHeader({ headers } as unknown as RequestLike, headerName.toUpperCase())).to.equal(expectedValue); + expect(extractHeader( headerName.toUpperCase(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); }); }); - it('allows missing headers to return by default', () => { - expect(extractHeader({ headers } as unknown as RequestLike, 'missing')).to.equal(''); - }); it('throws on missing headers', () => { - expect(() => extractHeader({ headers } as unknown as RequestLike, 'missing', { allowMissing: false })).to.throw(Error, 'Unable to extract header "missing" from message'); - }); - it('does not throw on missing headers', () => { - expect(extractHeader({ headers } as unknown as RequestLike, 'missing', { allowMissing: true })).to.equal(''); + expect(() => extractHeader('missing', { headers } as unknown as Request)).to.throw(Error, 'No header "missing" found in headers'); }); }); - describe('.extractComponent', () => { + describe('.deriveComponent', () => { it('correctly extracts the @method', () => { - const result = extractComponent({ + const result = deriveComponent('@method', { method: 'POST', url: 'https://www.example.com/path?param=value', - } as unknown as RequestLike, '@method'); - expect(result).to.equal('POST'); + } as unknown as Request); + expect(result).to.deep.equal(['POST']); }); it('correctly extracts the @target-uri', () => { - const result = extractComponent({ + const result = deriveComponent('@target-uri', { method: 'POST', url: 'https://www.example.com/path?param=value', - } as unknown as RequestLike, '@target-uri'); - expect(result).to.equal('https://www.example.com/path?param=value'); + } as unknown as Request); + expect(result).to.deep.equal(['https://www.example.com/path?param=value']); }); it('correctly extracts the @authority', () => { - const result = extractComponent({ + const result = deriveComponent('@authority', { method: 'POST', url: 'https://www.example.com/path?param=value', - } as unknown as RequestLike, '@authority'); - expect(result).to.equal('www.example.com'); + } as unknown as Request); + expect(result).to.deep.equal(['www.example.com']); }); it('correctly extracts the @scheme', () => { - const result = extractComponent({ + const result = deriveComponent('@scheme', { method: 'POST', url: 'http://www.example.com/path?param=value', - } as unknown as RequestLike, '@scheme'); - expect(result).to.equal('http'); + } as unknown as Request); + expect(result).to.deep.equal(['http']); }); it('correctly extracts the @request-target', () => { - const result = extractComponent({ + const result = deriveComponent('@request-target', { method: 'POST', url: 'https://www.example.com/path?param=value', - } as unknown as RequestLike, '@request-target'); - expect(result).to.equal('/path?param=value'); + } as unknown as Request); + expect(result).to.deep.equal(['/path?param=value']); }); it('correctly extracts the @path', () => { - const result = extractComponent({ + const result = deriveComponent('@path', { method: 'POST', url: 'https://www.example.com/path?param=value', - } as unknown as RequestLike, '@path'); - expect(result).to.equal('/path'); + } as unknown as Request); + expect(result).to.deep.equal(['/path']); }); it('correctly extracts the @query', () => { - const result = extractComponent({ + const result = deriveComponent('@query', { method: 'POST', url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', - } as unknown as RequestLike, '@query'); - expect(result).to.equal('?param=value&foo=bar&baz=batman'); + } as unknown as Request); + expect(result).to.deep.equal(['?param=value&foo=bar&baz=batman']); }); it('correctly extracts the @query', () => { - const result = extractComponent({ + const result = deriveComponent('@query', { method: 'POST', url: 'https://www.example.com/path?queryString', - } as unknown as RequestLike, '@query'); - expect(result).to.equal('?queryString'); - }); - it.skip('correctly extracts the @query-params', () => { - const result = extractComponent({ - method: 'POST', - url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', - } as unknown as RequestLike, '@query-params'); - expect(result).to.equal(''); - }); - }); - describe('.buildSignatureInputString', () => { - describe('specification test cases', () => { - it('constructs minimal example', () => { - const components: Component[] = []; - const parameters: Parameters = { - created: new Date(1618884475000), - keyid: 'test-key-rsa-pss', - alg: 'rsa-pss-sha512', - }; - const inputString = buildSignatureInputString(components, parameters); - expect(inputString).to.equal('();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'); - }); - it('constructs selective example', () => { - const components: Component[] = ['@authority', 'Content-Type']; - const parameters: Parameters = { - created: new Date(1618884475000), - keyid: 'test-key-rsa-pss', - }; - const inputString = buildSignatureInputString(components, parameters); - expect(inputString).to.equal('("@authority" "content-type");created=1618884475;keyid="test-key-rsa-pss"'); - }); - it('constructs full example', () => { - const components: Component[] = [ - 'Date', - '@method', - '@path', - '@query', - '@authority', - 'Content-Type', - 'Digest', - 'Content-Length', - ]; - const parameters: Parameters = { - created: new Date(1618884475000), - keyid: 'test-key-rsa-pss', - }; - const inputString = buildSignatureInputString(components, parameters); - expect(inputString).to.equal('("date" "@method" "@path" "@query" "@authority" "content-type" "digest" "content-length");created=1618884475;keyid="test-key-rsa-pss"'); - }); - }); - }); - describe('.buildSignedData', () => { - const testRequest: RequestLike = { - method: 'POST', - url: 'https://example.com/foo?param=value&pet=dog', - headers: { - 'Host': 'example.com', - 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', - 'Content-Type': 'application/json', - 'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=', - 'Content-Length': '18', - }, - }; - it('constructs minimal example', () => { - const components: Component[] = []; - const data = buildSignedData(testRequest, components, '();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'); - expect(data).to.equal('"@signature-params": ();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'); - }); - it('constructs selective example', () => { - const components: Component[] = ['@authority', 'Content-Type']; - const data = buildSignedData(testRequest, components, '("@authority" "content-type");created=1618884475;keyid="test-key-rsa-pss"'); - expect(data).to.equal('"@authority": example.com\n' + - '"content-type": application/json\n' + - '"@signature-params": ("@authority" "content-type");created=1618884475;keyid="test-key-rsa-pss"') - }); - it('constructs full example', () => { - const components: Component[] = [ - 'Date', - '@method', - '@path', - '@query', - '@authority', - 'Content-Type', - 'Digest', - 'Content-Length', - ]; - const data = buildSignedData(testRequest, components, '("date" "@method" "@path" "@query" "@authority" "content-type" "digest" "content-length");created=1618884475;keyid="test-key-rsa-pss"'); - expect(data).to.equal('"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + - '"@method": POST\n' + - '"@path": /foo\n' + - '"@query": ?param=value&pet=dog\n' + - '"@authority": example.com\n' + - '"content-type": application/json\n' + - '"digest": SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + - '"content-length": 18\n' + - '"@signature-params": ("date" "@method" "@path" "@query" ' + - '"@authority" "content-type" "digest" "content-length")' + - ';created=1618884475;keyid="test-key-rsa-pss"'); + } as unknown as Request); + expect(result).to.deep.equal(['?queryString']); }); }); }); diff --git a/test/httpbis/new.spec.ts b/test/httpbis/new.spec.ts index 58e8aba..35759bf 100644 --- a/test/httpbis/new.spec.ts +++ b/test/httpbis/new.spec.ts @@ -1,4 +1,5 @@ -import * as httpbis from '../../src/httpbis/new'; +import * as httpbis from '../../src/httpbis'; +import { Request, Response, SigningKey } from '../../src'; import { expect } from 'chai'; import { describe } from 'mocha'; import * as MockDate from 'mockdate'; @@ -9,7 +10,7 @@ describe('httpbis', () => { describe('.deriveComponent', () => { describe('unbound components', () => { it('derives @method component', () => { - const req: httpbis.Request = { + const req: Request = { method: 'get', headers: {}, url: 'https://example.com/test', @@ -22,7 +23,7 @@ describe('httpbis', () => { })).to.deep.equal(['POST']); }); it('derives @target-uri', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -34,7 +35,7 @@ describe('httpbis', () => { ]); }); it('derives @authority', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -66,7 +67,7 @@ describe('httpbis', () => { })).to.deep.equal(['www.example.com:80']); }); it('derives @scheme', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -80,7 +81,7 @@ describe('httpbis', () => { })).to.deep.equal(['http']); }); it('derives @request-target', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -97,7 +98,7 @@ describe('httpbis', () => { ]); }); it('derives @path', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value', headers: { @@ -109,7 +110,7 @@ describe('httpbis', () => { ]); }); it('derives @query', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', headers: { @@ -133,7 +134,7 @@ describe('httpbis', () => { ]); }); it('derives @query-param', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', headers: { @@ -158,14 +159,14 @@ describe('httpbis', () => { ]); }); it('derives @status', () => { - const req: httpbis.Request = { + const req: Request = { method: 'POST', url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', headers: { Host: 'www.example.com', }, }; - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -173,7 +174,7 @@ describe('httpbis', () => { }); }); describe('request-response bound components', () => { - const req: httpbis.Request = { + const req: Request = { method: 'get', headers: { Host: 'www.example.com', @@ -181,7 +182,7 @@ describe('httpbis', () => { url: 'https://www.example.com/path?param=value', }; it('derives @method component', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -193,7 +194,7 @@ describe('httpbis', () => { })).to.deep.equal(['POST']); }); it('derives @target-uri', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -202,7 +203,7 @@ describe('httpbis', () => { ]); }); it('derives @authority', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -231,7 +232,7 @@ describe('httpbis', () => { })).to.deep.equal(['www.example.com:80']); }); it('derives @scheme', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -242,7 +243,7 @@ describe('httpbis', () => { })).to.deep.equal(['http']); }); it('derives @request-target', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -256,7 +257,7 @@ describe('httpbis', () => { ]); }); it('derives @path', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -265,7 +266,7 @@ describe('httpbis', () => { ]); }); it('derives @query', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -286,7 +287,7 @@ describe('httpbis', () => { ]); }); it('derives @query-param', () => { - const res: httpbis.Response = { + const res: Response = { status: 200, headers: {}, }; @@ -320,7 +321,7 @@ describe('httpbis', () => { }); describe('.extractHeader', () => { describe('raw headers', () => { - const request: httpbis.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -344,7 +345,7 @@ describe('httpbis', () => { }); }); describe('sf headers', () => { - const request: httpbis.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -362,7 +363,7 @@ describe('httpbis', () => { }); }); describe('key from structured header', () => { - const request: httpbis.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -384,7 +385,7 @@ describe('httpbis', () => { }); }); describe('bs from header', () => { - const request: httpbis.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -404,7 +405,7 @@ describe('httpbis', () => { }); }); describe('request-response bound header', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -417,7 +418,7 @@ describe('httpbis', () => { 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', }, }; - const response: httpbis.Response = { + const response: Response = { status: 503, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', @@ -434,7 +435,7 @@ describe('httpbis', () => { }); describe('.createSignatureBase', () => { describe('header fields', () => { - const request: httpbis.Request = { + const request: Request = { method: 'POST', url: 'https://www.example.com/', headers: { @@ -505,7 +506,7 @@ describe('httpbis', () => { headers: { 'Example-Header': ['value, with, lots', 'of, commas'], }, - } as httpbis.Request)).to.deep.equal([ + } as Request)).to.deep.equal([ ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']], ]); expect(httpbis.createSignatureBase([ @@ -515,13 +516,13 @@ describe('httpbis', () => { headers: { 'Example-Header': ['value, with, lots, of, commas'], }, - } as httpbis.Request)).to.deep.equal([ + } as Request)).to.deep.equal([ ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHMsIG9mLCBjb21tYXM=:']], ]); }); }); describe('derived components', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://www.example.com/path?param=value', headers: { @@ -608,7 +609,7 @@ describe('httpbis', () => { }); }); describe('full example', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -901,7 +902,7 @@ describe('httpbis', () => { }); describe('.signMessage', () => { describe('requests', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -912,7 +913,7 @@ describe('httpbis', () => { 'Content-Length': '18', }, }; - let signer: httpbis.SigningKey; + let signer: SigningKey; beforeEach('stub signer', () => { signer = { sign: stub().resolves(Buffer.from('a fake signature')), @@ -959,7 +960,7 @@ describe('httpbis', () => { }); }); describe('responses', () => { - const response: httpbis.Response = { + const response: Response = { status: 503, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', @@ -967,7 +968,7 @@ describe('httpbis', () => { 'Content-Length': '62', }, }; - let signer: httpbis.SigningKey; + let signer: SigningKey; beforeEach('stub signer', () => { signer = { sign: stub().resolves(Buffer.from('a fake signature')), @@ -999,7 +1000,7 @@ describe('httpbis', () => { }); }); describe('request bound responses', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -1012,7 +1013,7 @@ describe('httpbis', () => { 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', }, }; - const response: httpbis.Response = { + const response: Response = { status: 503, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', @@ -1020,7 +1021,7 @@ describe('httpbis', () => { 'Content-Length': '62', }, }; - let signer: httpbis.SigningKey; + let signer: SigningKey; beforeEach('stub signer', () => { signer = { sign: stub().resolves(Buffer.from('a fake signature')), @@ -1058,7 +1059,7 @@ describe('httpbis', () => { }); describe('.verifyMessage', () => { describe('requests', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -1096,7 +1097,7 @@ describe('httpbis', () => { }); }); describe('responses', () => { - const response: httpbis.Response = { + const response: Response = { status: 200, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', @@ -1130,7 +1131,7 @@ describe('httpbis', () => { }); }); describe('request bound responses', () => { - const request: httpbis.Request = { + const request: Request = { method: 'post', url: 'https://example.com/foo?param=Value&Pet=dog', headers: { @@ -1143,7 +1144,7 @@ describe('httpbis', () => { 'Signature': 'sig1=:LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', }, }; - const response: httpbis.Response = { + const response: Response = { status: 503, headers: { 'Date': 'Tue, 20 Apr 2021 02:07:56 GMT', From cbb4c35b47b01c284cafa6ebfed47d6a6c9ac945 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 29 Sep 2022 14:16:53 +0100 Subject: [PATCH 06/17] Add ed25519 support --- src/algorithm/index.ts | 38 ++++++++++++++--------- test/algorithm/ed25519.ts | 65 +++++++++++++++++++++++++++++++++++++++ test/etc/ed25519.pem | 3 ++ 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 test/algorithm/ed25519.ts create mode 100644 test/etc/ed25519.pem diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index 7204d46..ecec339 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -9,6 +9,8 @@ import { timingSafeEqual, VerifyKeyObjectInput, VerifyPublicKeyInput, + sign, + verify, } from 'crypto'; import { RSA_PKCS1_PADDING, RSA_PKCS1_PSS_PADDING } from 'constants'; import { SigningKey, Algorithm, Verifier } from '../types'; @@ -25,29 +27,33 @@ export function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | Si const signer = { alg } as SigningKey; switch (alg) { case 'hmac-sha256': - signer.sign = async (data: BinaryLike) => createHmac('sha256', key as BinaryLike).update(data).digest(); + signer.sign = async (data: Buffer) => createHmac('sha256', key as BinaryLike).update(data).digest(); break; case 'rsa-pss-sha512': - signer.sign = async (data: BinaryLike) => createSign('sha512').update(data).sign({ + signer.sign = async (data: Buffer) => createSign('sha512').update(data).sign({ key, padding: RSA_PKCS1_PSS_PADDING, } as SignPrivateKeyInput); break; case 'rsa-v1_5-sha256': - signer.sign = async (data: BinaryLike) => createSign('sha256').update(data).sign({ + signer.sign = async (data: Buffer) => createSign('sha256').update(data).sign({ key, padding: RSA_PKCS1_PADDING, } as SignPrivateKeyInput); break; case 'rsa-v1_5-sha1': // this is legacy for cavage - signer.sign = async (data: BinaryLike) => createSign('sha1').update(data).sign({ + signer.sign = async (data: Buffer) => createSign('sha1').update(data).sign({ key, padding: RSA_PKCS1_PADDING, } as SignPrivateKeyInput); break; case 'ecdsa-p256-sha256': - signer.sign = async (data: BinaryLike) => createSign('sha256').update(data).sign(key as KeyLike); + signer.sign = async (data: Buffer) => createSign('sha256').update(data).sign(key as KeyLike); + break; + case 'ed25519': + signer.sign = async (data: Buffer) => sign(null, data, key as KeyLike); + // signer.sign = async (data: Buffer) => createSign('ed25519').update(data).sign(key as KeyLike); break; default: throw new Error(`Unsupported signing algorithm ${alg}`); @@ -74,32 +80,34 @@ export function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput let verifier; switch (alg) { case 'hmac-sha256': - verifier = async (data: BinaryLike, signature: BinaryLike) => { + verifier = async (data: Buffer, signature: Buffer) => { const expected = createHmac('sha256', key as BinaryLike).update(data).digest(); - const sig = Buffer.from(signature); - return sig.length === expected.length && timingSafeEqual(sig, expected); + return signature.length === expected.length && timingSafeEqual(signature, expected); } break; case 'rsa-pss-sha512': - verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha512').update(data).verify({ + verifier = async (data: Buffer, signature: Buffer) => createVerify('sha512').update(data).verify({ key, padding: RSA_PKCS1_PSS_PADDING, - } as VerifyPublicKeyInput, Buffer.from(signature)); + } as VerifyPublicKeyInput, signature); break; case 'rsa-v1_5-sha1': - verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha1').update(data).verify({ + verifier = async (data: Buffer, signature: Buffer) => createVerify('sha1').update(data).verify({ key, padding: RSA_PKCS1_PADDING, - } as VerifyPublicKeyInput, Buffer.from(signature)); + } as VerifyPublicKeyInput, signature); break; case 'rsa-v1_5-sha256': - verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha256').update(data).verify({ + verifier = async (data: Buffer, signature: Buffer) => createVerify('sha256').update(data).verify({ key, padding: RSA_PKCS1_PADDING, - } as VerifyPublicKeyInput, Buffer.from(signature)); + } as VerifyPublicKeyInput, signature); break; case 'ecdsa-p256-sha256': - verifier = async (data: BinaryLike, signature: BinaryLike) => createVerify('sha256').update(data).verify(key as KeyLike, Buffer.from(signature)); + verifier = async (data: Buffer, signature: Buffer) => createVerify('sha256').update(data).verify(key as KeyLike, signature); + break; + case 'ed25519': + verifier = async (data: Buffer, signature: Buffer) => verify(null, data, key as KeyLike, signature) as unknown as boolean; break; default: throw new Error(`Unsupported signing algorithm ${alg}`); diff --git a/test/algorithm/ed25519.ts b/test/algorithm/ed25519.ts new file mode 100644 index 0000000..44e23b6 --- /dev/null +++ b/test/algorithm/ed25519.ts @@ -0,0 +1,65 @@ +import { generateKeyPair, sign, verify } from 'crypto'; +import { promisify } from 'util'; +import { createSigner, createVerifier } from '../../src'; +import { expect } from 'chai'; +import { readFile } from 'fs'; +import { join } from 'path'; + +describe('ed25519', () => { + describe('internal tests', () => { + let ed25519: { publicKey: string, privateKey: string }; + before('generate key pair', async () => { + ed25519 = await promisify(generateKeyPair)('ed25519', { + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + }); + describe('signing', () => { + it('signs a payload', async () => { + const signer = createSigner(ed25519.privateKey, 'ed25519'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); + expect(signer.alg).to.equal('ed25519'); + expect(sig).to.satisfy((arg: Buffer) => verify(null, data, ed25519.publicKey, arg)); + }); + }); + describe('verifying', () => { + it('verifies a signature', async () => { + const verifier = createVerifier(ed25519.publicKey, 'ed25519'); + const data = Buffer.from('some random data'); + const sig = sign(null, data, ed25519.privateKey); + expect(sig).to.satisfy((arg: Buffer) => verifier(data, arg)); + }); + }); + }); + describe('specification examples', () => { + let ecKeyPem: string; + before('load rsa key', async () => { + ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/ed25519.pem'))).toString(); + }); + describe('response signing', () => { + const data = Buffer.from('"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + + '"@method": POST\n' + + '"@path": /foo\n' + + '"@authority": example.com\n' + + '"content-type": application/json\n' + + '"content-length": 18\n' + + '"@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'); + it('successfully signs a payload', async () => { + const sig = await (createSigner(ecKeyPem, 'ed25519').sign(data)); + expect(sig).to.satisfy((arg: Buffer) => verify(null, data, ecKeyPem, arg)); + }); + it('successfully verifies a signature', async () => { + const sig = Buffer.from('wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==', 'base64'); + expect(sig).to.satisfy((arg: Buffer) => verify(null, Buffer.from(data), ecKeyPem, arg)); + expect(await (createVerifier(ecKeyPem, 'ed25519')(data, sig))).to.equal(true); + }); + }); + }); +}); diff --git a/test/etc/ed25519.pem b/test/etc/ed25519.pem new file mode 100644 index 0000000..66c5820 --- /dev/null +++ b/test/etc/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF +-----END PRIVATE KEY----- From 3678dc58f17fbf1d1dcd0f970251e4d01f7ea152 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 3 Oct 2022 11:39:34 +0100 Subject: [PATCH 07/17] Allow conumer to specify component parser --- src/httpbis/index.ts | 82 +++++++++++------ src/types/index.ts | 23 ++++- test/httpbis/httpbis.ts | 26 +++--- test/httpbis/new.spec.ts | 189 ++++++++++++++++++++++----------------- 4 files changed, 192 insertions(+), 128 deletions(-) diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index da66d6d..74f6bb4 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -11,27 +11,33 @@ import { Parameters, isInnerList, isByteSequence, + Token, } from 'structured-headers'; import { Dictionary, parseHeader, quoteString } from '../structured-header'; -import { Request, Response, SignConfig, VerifyConfig, defaultParams, isRequest } from '../types'; +import { + Request, + Response, + SignConfig, + VerifyConfig, + defaultParams, + isRequest, + CommonConfig, +} from '../types'; -export function deriveComponent(component: string, res: Response, req?: Request): string[]; -export function deriveComponent(component: string, req: Request): string[]; +export function deriveComponent(component: string, params: Map, res: Response, req?: Request): string[]; +export function deriveComponent(component: string, params: Map, req: Request): string[]; /** * Components can be derived from requests or responses (which can also be bound to their request). - * The signature is essentially (component, signingSubject, supplementaryData) - * - * @todo - Allow consumers to register their own component parser somehow + * The signature is essentially (component, params, signingSubject, supplementaryData) */ -export function deriveComponent(component: string, message: Request | Response, req?: Request): string[] { - const [componentName, params] = parseItem(quoteString(component)); +export function deriveComponent(component: string, params: Map, message: Request | Response, req?: Request): string[] { // switch the context of the signing data depending on if the `req` flag was passed const context = params.has('req') ? req : message; if (!context) { throw new Error('Missing request in request-response bound component'); } - switch (componentName.toString().toLowerCase()) { + switch (component) { case '@method': if (!isRequest(context)) { throw new Error('Cannot derive @method from response'); @@ -112,19 +118,17 @@ export function deriveComponent(component: string, message: Request | Response, } } -export function extractHeader(header: string, res: Response, req?: Request): string[]; -export function extractHeader(header: string, req: Request): string[]; +export function extractHeader(header: string, params: Map, res: Response, req?: Request): string[]; +export function extractHeader(header: string, params: Map, req: Request): string[]; -export function extractHeader(header: string, { headers }: Request | Response, req?: Request): string[] { - const [headerName, params] = parseItem(quoteString(header)); +export function extractHeader(header: string, params: Map, { headers }: Request | Response, req?: Request): string[] { const context = params.has('req') ? req?.headers : headers; if (!context) { throw new Error('Missing request in request-response bound component'); } - const lcHeaderName = headerName.toString().toLowerCase(); - const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === lcHeaderName); + const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === header); if (!headerTuple) { - throw new Error(`No header "${headerName}" found in headers`); + throw new Error(`No header "${header}" found in headers`); } const values = (Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]); if (params.has('bs') && params.has('sf')) { @@ -156,16 +160,37 @@ export function extractHeader(header: string, { headers }: Request | Response, r return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; } -export function createSignatureBase(fields: string[], res: Response, req?: Request): [string, string[]][]; -export function createSignatureBase(fields: string[], req: Request): [string, string[]][]; +function normaliseParams(params: Parameters): Map { + const map = new Map; + params.forEach((value, key) => { + if (value instanceof ByteSequence) { + map.set(key, value.toBase64()); + } else if (value instanceof Token) { + map.set(key, value.toString()); + } else { + map.set(key, value); + } + }); + return map; +} + +export function createSignatureBase(config: CommonConfig & { fields: string[] }, res: Response, req?: Request): [string, string[]][]; +export function createSignatureBase(config: CommonConfig & { fields: string[] }, req: Request): [string, string[]][]; -export function createSignatureBase(fields: string[], res: Request | Response, req?: Request): [string, string[]][] { - return (fields).reduce<[string, string[]][]>((base, fieldName) => { - const [field, params] = parseItem(quoteString(fieldName)); - const lcFieldName = field.toString().toLowerCase(); +export function createSignatureBase(config: CommonConfig & { fields: string[] }, res: Request | Response, req?: Request): [string, string[]][] { + return (config.fields).reduce<[string, string[]][]>((base, fieldName) => { + const [field, params] = parseItem(quoteString(fieldName)) as [string, Parameters]; + const fieldParams = normaliseParams(params); + const lcFieldName = field.toLowerCase(); if (lcFieldName !== '@signature-params') { - const value = lcFieldName.startsWith('@') ? deriveComponent(fieldName, res as Response, req) : extractHeader(fieldName, res as Response, req); - base.push([serializeItem([lcFieldName, params]), value]); + let value: string[] | null = null; + if (config.componentParser) { + value = config.componentParser(lcFieldName, fieldParams, res, req) ?? null; + } + if (value === null) { + value = field.startsWith('@') ? deriveComponent(lcFieldName, fieldParams, res as Response, req) : extractHeader(lcFieldName, fieldParams, res as Response, req); + } + base.push([serializeItem([field, params]), value]); } return base; }, []); @@ -275,7 +300,10 @@ export async function signMessage(config: SignConfi export async function signMessage(config: SignConfig, message: T, req?: U): Promise { const signingParameters = createSigningParameters(config); - const signatureBase = createSignatureBase(config?.fields ?? [], message as Response, req); + const signatureBase = createSignatureBase({ + fields: config?.fields ?? [], + componentParser: config.componentParser, + }, message as Response, req); const signatureInput = serializeList([ [ signatureBase.map(([item]) => parseItem(item)), @@ -366,8 +394,10 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res return false; } } + // now look to verify the signature! Build the expected "signing base" and verify it! - const signingBase = createSignatureBase(input[0].map((item) => serializeItem(item)), message as Response, req); + const fields = input[0].map((item) => serializeItem(item)); + const signingBase = createSignatureBase({ fields, componentParser: config.componentParser }, message as Response, req); signingBase.push(['"@signature-params"', [serializeList([input])]]); const base = formatSignatureBase(signingBase); const signature = signatures.get(name); diff --git a/src/types/index.ts b/src/types/index.ts index 46c0b4f..842130e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -63,7 +63,26 @@ export const defaultParams = [ 'expires', ]; -export interface SignConfig { +/** + * A component parser supplied by the consumer to allow applications to define their own logic for + * extracting components for use in the signature base. + * + * This can be useful in circumstances where the application has agreed a specific standard or way + * of extracting components from messages and/or when new components are added to the specification + * but not yet supported by the library. + * + * Return null to defer to internal logic + */ +export type ComponentParser = (name: string, params: Map, message: Request | Response, req?: Request) => string[] | null; + +export interface CommonConfig { + /** + * A component user supplied component parser + */ + componentParser?: ComponentParser; +} + +export interface SignConfig extends CommonConfig { key: SigningKey; /** * The name to try to use for the signature @@ -91,7 +110,7 @@ export interface SignConfig { /** * Options when verifying signatures */ -export interface VerifyConfig { +export interface VerifyConfig extends CommonConfig { verifier: Verifier; /** * A maximum age for the signature diff --git a/test/httpbis/httpbis.ts b/test/httpbis/httpbis.ts index b7aac22..bf8b20e 100644 --- a/test/httpbis/httpbis.ts +++ b/test/httpbis/httpbis.ts @@ -16,71 +16,65 @@ describe('httpbis', () => { }; Object.entries(headers).forEach(([headerName, expectedValue]) => { it(`successfully extracts a matching header (${headerName})`, () => { - expect(extractHeader( headerName, { headers } as unknown as Request)).to.deep.equal([expectedValue]); - }); - it(`successfully extracts a lower cased header (${headerName})`, () => { - expect(extractHeader( headerName.toLowerCase(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); - }); - it(`successfully extracts an upper cased header (${headerName})`, () => { - expect(extractHeader( headerName.toUpperCase(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); + expect(extractHeader(headerName.toLowerCase(), new Map(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); }); }); it('throws on missing headers', () => { - expect(() => extractHeader('missing', { headers } as unknown as Request)).to.throw(Error, 'No header "missing" found in headers'); + expect(() => extractHeader('missing', new Map(), { headers } as unknown as Request)).to.throw(Error, 'No header "missing" found in headers'); }); }); describe('.deriveComponent', () => { it('correctly extracts the @method', () => { - const result = deriveComponent('@method', { + const result = deriveComponent('@method', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['POST']); }); it('correctly extracts the @target-uri', () => { - const result = deriveComponent('@target-uri', { + const result = deriveComponent('@target-uri', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['https://www.example.com/path?param=value']); }); it('correctly extracts the @authority', () => { - const result = deriveComponent('@authority', { + const result = deriveComponent('@authority', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['www.example.com']); }); it('correctly extracts the @scheme', () => { - const result = deriveComponent('@scheme', { + const result = deriveComponent('@scheme', new Map(), { method: 'POST', url: 'http://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['http']); }); it('correctly extracts the @request-target', () => { - const result = deriveComponent('@request-target', { + const result = deriveComponent('@request-target', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['/path?param=value']); }); it('correctly extracts the @path', () => { - const result = deriveComponent('@path', { + const result = deriveComponent('@path', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value', } as unknown as Request); expect(result).to.deep.equal(['/path']); }); it('correctly extracts the @query', () => { - const result = deriveComponent('@query', { + const result = deriveComponent('@query', new Map(), { method: 'POST', url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', } as unknown as Request); expect(result).to.deep.equal(['?param=value&foo=bar&baz=batman']); }); it('correctly extracts the @query', () => { - const result = deriveComponent('@query', { + const result = deriveComponent('@query', new Map(), { method: 'POST', url: 'https://www.example.com/path?queryString', } as unknown as Request); diff --git a/test/httpbis/new.spec.ts b/test/httpbis/new.spec.ts index 35759bf..cf03ef9 100644 --- a/test/httpbis/new.spec.ts +++ b/test/httpbis/new.spec.ts @@ -16,8 +16,8 @@ describe('httpbis', () => { url: 'https://example.com/test', }; // must be in uppercase - expect(httpbis.deriveComponent('@method', req)).to.deep.equal(['GET']); - expect(httpbis.deriveComponent('@method', { + expect(httpbis.deriveComponent('@method', new Map(), req)).to.deep.equal(['GET']); + expect(httpbis.deriveComponent('@method', new Map(), { ...req, method: 'POST', })).to.deep.equal(['POST']); @@ -30,7 +30,7 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('@target-uri', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@target-uri', new Map(), req)).to.deep.equal([ 'https://www.example.com/path?param=value', ]); }); @@ -42,26 +42,26 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('@authority', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@authority', new Map(), req)).to.deep.equal([ 'www.example.com', ]); - expect(httpbis.deriveComponent('@authority', { + expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'https://www.EXAMPLE.com/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority', { + expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'https://www.example.com:8080/path?param=value', })).to.deep.equal(['www.example.com:8080']); - expect(httpbis.deriveComponent('@authority', { + expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'https://www.example.com:443/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority', { + expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'http://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority', { + expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'https://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com:80']); @@ -74,8 +74,8 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('@scheme', req)).to.deep.equal(['https']); - expect(httpbis.deriveComponent('@scheme', { + expect(httpbis.deriveComponent('@scheme', new Map(), req)).to.deep.equal(['https']); + expect(httpbis.deriveComponent('@scheme', new Map(), { ...req, url: 'http://example.com', })).to.deep.equal(['http']); @@ -93,7 +93,7 @@ describe('httpbis', () => { // and not: // GET https://www.example.com/path?param=value HTTP/1.1 // it's not easy to determine this in Node when receiving messages - expect(httpbis.deriveComponent('@request-target', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@request-target', new Map(), req)).to.deep.equal([ '/path?param=value', ]); }); @@ -105,7 +105,7 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('@path', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@path', new Map(), req)).to.deep.equal([ '/path', ]); }); @@ -117,16 +117,16 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('@query', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@query', new Map(), req)).to.deep.equal([ '?param=value&foo=bar&baz=batman', ]); - expect(httpbis.deriveComponent('@query', { + expect(httpbis.deriveComponent('@query', new Map(), { ...req, url: 'https://www.example.com/path?queryString', })).to.deep.equal([ '?queryString', ]); - expect(httpbis.deriveComponent('@query', { + expect(httpbis.deriveComponent('@query', new Map(), { ...req, url: 'https://www.example.com/path', })).to.deep.equal([ @@ -141,16 +141,16 @@ describe('httpbis', () => { Host: 'www.example.com', }, }; - expect(httpbis.deriveComponent('"@query-param";name="baz"', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'baz']]), req)).to.deep.equal([ 'batman', ]); - expect(httpbis.deriveComponent('"@query-param";name="qux"', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'qux']]), req)).to.deep.equal([ '', ]); - expect(httpbis.deriveComponent('@query-param;name=param', req)).to.deep.equal([ + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'param']]), req)).to.deep.equal([ 'value', ]); - expect(httpbis.deriveComponent('@query-param;name=param', { + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'param']]), { ...req, url: 'https://example.com/path?param=value¶m=value2', })).to.deep.equal([ @@ -170,7 +170,7 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@status', res, req)).to.deep.equal(['200']); + expect(httpbis.deriveComponent('@status', new Map(), res, req)).to.deep.equal(['200']); }); }); describe('request-response bound components', () => { @@ -187,8 +187,8 @@ describe('httpbis', () => { headers: {}, }; // must be in uppercase - expect(httpbis.deriveComponent('@method;req', res, req)).to.deep.equal(['GET']); - expect(httpbis.deriveComponent('@method;req', res, { + expect(httpbis.deriveComponent('@method', new Map([['req', true]]), res, req)).to.deep.equal(['GET']); + expect(httpbis.deriveComponent('@method', new Map([['req', true]]), res, { ...req, method: 'POST', })).to.deep.equal(['POST']); @@ -198,7 +198,7 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@target-uri;req', res, req)).to.deep.equal([ + expect(httpbis.deriveComponent('@target-uri', new Map([['req', true]]), res, req)).to.deep.equal([ 'https://www.example.com/path?param=value', ]); }); @@ -207,26 +207,26 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@authority;req', res, req)).to.deep.equal([ + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, req)).to.deep.equal([ 'www.example.com', ]); - expect(httpbis.deriveComponent('@authority;req', res, { + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, { ...req, url: 'https://www.EXAMPLE.com/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority;req', res, { + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, { ...req, url: 'https://www.example.com:8080/path?param=value', })).to.deep.equal(['www.example.com:8080']); - expect(httpbis.deriveComponent('@authority;req', res, { + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, { ...req, url: 'https://www.example.com:443/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority;req', res, { + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, { ...req, url: 'http://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com']); - expect(httpbis.deriveComponent('@authority;req', res, { + expect(httpbis.deriveComponent('@authority', new Map([['req', true]]), res, { ...req, url: 'https://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com:80']); @@ -236,8 +236,8 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@scheme;req', res, req)).to.deep.equal(['https']); - expect(httpbis.deriveComponent('@scheme;req', res, { + expect(httpbis.deriveComponent('@scheme', new Map([['req', true]]), res, req)).to.deep.equal(['https']); + expect(httpbis.deriveComponent('@scheme', new Map([['req', true]]), res, { ...req, url: 'http://example.com', })).to.deep.equal(['http']); @@ -252,7 +252,7 @@ describe('httpbis', () => { // and not: // GET https://www.example.com/path?param=value HTTP/1.1 // it's not easy to determine this in Node when receiving messages - expect(httpbis.deriveComponent('@request-target;req', res, req)).to.deep.equal([ + expect(httpbis.deriveComponent('@request-target', new Map([['req', true]]), res, req)).to.deep.equal([ '/path?param=value', ]); }); @@ -261,7 +261,7 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@path;req', res, req)).to.deep.equal([ + expect(httpbis.deriveComponent('@path', new Map([['req', true]]), res, req)).to.deep.equal([ '/path', ]); }); @@ -270,16 +270,16 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('@query;req', res, req)).to.deep.equal([ + expect(httpbis.deriveComponent('@query', new Map([['req', true]]), res, req)).to.deep.equal([ '?param=value', ]); - expect(httpbis.deriveComponent('@query;req', res, { + expect(httpbis.deriveComponent('@query', new Map([['req', true]]), res, { ...req, url: 'https://www.example.com/path?queryString', })).to.deep.equal([ '?queryString', ]); - expect(httpbis.deriveComponent('@query;req', res, { + expect(httpbis.deriveComponent('@query', new Map([['req', true]]), res, { ...req, url: 'https://www.example.com/path', })).to.deep.equal([ @@ -291,25 +291,25 @@ describe('httpbis', () => { status: 200, headers: {}, }; - expect(httpbis.deriveComponent('"@query-param";req;name="baz"', res, { + expect(httpbis.deriveComponent('@query-param', new Map([['req', true], ['name', 'baz']]), res, { ...req, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ 'batman', ]); - expect(httpbis.deriveComponent('"@query-param";req;name="qux"', res, { + expect(httpbis.deriveComponent('@query-param', new Map([['req', true], ['name', 'qux']]), res, { ...req, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ '', ]); - expect(httpbis.deriveComponent('@query-param;req;name=param', res, { + expect(httpbis.deriveComponent('@query-param', new Map([['req', true], ['name', 'param']]), res, { ...req, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ 'value', ]); - expect(httpbis.deriveComponent('@query-param;req;name=param', res, { + expect(httpbis.deriveComponent('@query-param', new Map([['req', true], ['name', 'param']]), res, { ...req, url: 'https://example.com/path?param=value¶m=value2', })).to.deep.equal([ @@ -335,13 +335,13 @@ describe('httpbis', () => { }, }; it('parses raw fields', () => { - expect(httpbis.extractHeader('host', request)).to.deep.equal(['www.example.com']); - expect(httpbis.extractHeader('date', request)).to.deep.equal(['Tue, 20 Apr 2021 02:07:56 GMT']); - expect(httpbis.extractHeader('X-OWS-Header', request)).to.deep.equal(['Leading and trailing whitespace.']); - expect(httpbis.extractHeader('x-obs-fold-header', request)).to.deep.equal(['Obsolete line folding.']); - expect(httpbis.extractHeader('cache-control', request)).to.deep.equal(['max-age=60, must-revalidate']); - expect(httpbis.extractHeader('example-dict', request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); - expect(httpbis.extractHeader('x-empty-header', request)).to.deep.equal(['']); + expect(httpbis.extractHeader('host', new Map(), request)).to.deep.equal(['www.example.com']); + expect(httpbis.extractHeader('date', new Map(), request)).to.deep.equal(['Tue, 20 Apr 2021 02:07:56 GMT']); + expect(httpbis.extractHeader('x-ows-header', new Map(), request)).to.deep.equal(['Leading and trailing whitespace.']); + expect(httpbis.extractHeader('x-obs-fold-header', new Map(), request)).to.deep.equal(['Obsolete line folding.']); + expect(httpbis.extractHeader('cache-control', new Map(), request)).to.deep.equal(['max-age=60, must-revalidate']); + expect(httpbis.extractHeader('example-dict', new Map(), request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); + expect(httpbis.extractHeader('x-empty-header', new Map(), request)).to.deep.equal(['']); }); }); describe('sf headers', () => { @@ -359,7 +359,7 @@ describe('httpbis', () => { }, }; it('serializes a dictionary', () => { - expect(httpbis.extractHeader('example-dict;sf', request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); + expect(httpbis.extractHeader('example-dict', new Map([['sf', true]]), request)).to.deep.equal(['a=1, b=2;x=1;y=2, c=(a b c)']); }); }); describe('key from structured header', () => { @@ -372,16 +372,16 @@ describe('httpbis', () => { }, }; it('pulls out an integer key', () => { - expect(httpbis.extractHeader('example-dict;key="a"', request)).to.deep.equal(['1']); + expect(httpbis.extractHeader('example-dict', new Map([['key', 'a']]), request)).to.deep.equal(['1']); }); it('pulls out a boolean key', () => { - expect(httpbis.extractHeader('example-dict;key="d"', request)).to.deep.equal(['?1']); + expect(httpbis.extractHeader('example-dict', new Map([['key', 'd']]), request)).to.deep.equal(['?1']); }); it('pulls out parameters', () => { - expect(httpbis.extractHeader('example-dict;key="b"', request)).to.deep.equal(['2;x=1;y=2']); + expect(httpbis.extractHeader('example-dict', new Map([['key', 'b']]), request)).to.deep.equal(['2;x=1;y=2']); }); it('pulls out an inner list', () => { - expect(httpbis.extractHeader('example-dict;key="c"', request)).to.deep.equal(['(a b c)']); + expect(httpbis.extractHeader('example-dict', new Map([['key', 'c']]), request)).to.deep.equal(['(a b c)']); }); }); describe('bs from header', () => { @@ -394,8 +394,8 @@ describe('httpbis', () => { }, }; it('encodes multiple headers separately', () => { - expect(httpbis.extractHeader('Example-Header;bs', request)).to.deep.equal([':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']); - expect(httpbis.extractHeader('Example-Header;bs', { + expect(httpbis.extractHeader('example-header', new Map([['bs', true]]), request)).to.deep.equal([':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']); + expect(httpbis.extractHeader('example-header', new Map([['bs', true]]), { ...request, headers: { ...request.headers, @@ -427,7 +427,7 @@ describe('httpbis', () => { }, }; it('binds requests and responses', () => { - expect(httpbis.extractHeader('Signature;req;key=sig1', response, request)).to.deep.equal([ + expect(httpbis.extractHeader('signature', new Map([['req', true], ['key', 'sig1']]), response, request)).to.deep.equal([ ':LAH8BjcfcOcLojiuOBFWn0P5keD3xAOuJRGziCLuD8r5MW9S0RoXXLzLSRfGY/3SF8kVIkHjE13SEFdTo4Af/fJ/Pu9wheqoLVdwXyY/UkBIS1M8Brc8IODsn5DFIrG0IrburbLi0uCc+E2ZIIb6HbUJ+o+jP58JelMTe0QE3IpWINTEzpxjqDf5/Df+InHCAkQCTuKsamjWXUpyOT1Wkxi7YPVNOjW4MfNuTZ9HdbD2Tr65+BXeTG9ZS/9SWuXAc+BZ8WyPz0QRz//ec3uWXd7bYYODSjRAxHqX+S1ag3LZElYyUKaAIjZ8MGOt4gXEwCSLDv/zqxZeWLj/PDkn6w==:', ]); }); @@ -449,14 +449,14 @@ describe('httpbis', () => { }, }; it('creates a signature base from raw headers', () => { - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ 'host', 'date', 'x-ows-header', 'x-obs-fold-header', 'cache-control', 'example-dict', - ], request)).to.deep.equal([ + ] }, request)).to.deep.equal([ ['"host"', ['www.example.com']], ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], ['"x-ows-header"', ['Leading and trailing whitespace.']], @@ -466,26 +466,26 @@ describe('httpbis', () => { ]); }); it('extracts an empty header', () => { - expect(httpbis.createSignatureBase([ - 'X-Empty-Header', - ], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: [ + 'x-empty-header', + ] }, request)).to.deep.equal([ ['"x-empty-header"', ['']], ]); }); it('extracts strict formatted headers', () => { - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ 'example-dict;sf', - ], request)).to.deep.equal([ + ] }, request)).to.deep.equal([ ['"example-dict";sf', ['a=1, b=2;x=1;y=2, c=(a b c)']], ]); }); it('extracts keys from dictionary headers', () => { - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ 'example-dict;key="a"', 'example-dict;key="d"', 'example-dict;key="b"', 'example-dict;key="c"', - ], { + ] }, { ...request, headers: { ...request.headers, @@ -499,9 +499,9 @@ describe('httpbis', () => { ]); }); it('extracts binary formatted headers', () => { - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ 'example-header;bs', - ], { + ] }, { ...request, headers: { 'Example-Header': ['value, with, lots', 'of, commas'], @@ -509,9 +509,9 @@ describe('httpbis', () => { } as Request)).to.deep.equal([ ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHM=:, :b2YsIGNvbW1hcw==:']], ]); - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ 'example-header;bs', - ], { + ] }, { ...request, headers: { 'Example-Header': ['value, with, lots, of, commas'], @@ -530,49 +530,49 @@ describe('httpbis', () => { }, }; it('derives @method', () => { - expect(httpbis.createSignatureBase(['@method'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@method'] }, request)).to.deep.equal([ ['"@method"', ['POST']], ]); }); it('derives @target-uri', () => { - expect(httpbis.createSignatureBase(['@target-uri'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@target-uri'] }, request)).to.deep.equal([ ['"@target-uri"', ['https://www.example.com/path?param=value']], ]); }); it('derives @authority', () => { - expect(httpbis.createSignatureBase(['@authority'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@authority'] }, request)).to.deep.equal([ ['"@authority"', ['www.example.com']], ]); }); it('derives @scheme', () => { - expect(httpbis.createSignatureBase(['@scheme'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@scheme'] }, request)).to.deep.equal([ ['"@scheme"', ['https']], ]); }); it('derives @request-target', () => { - expect(httpbis.createSignatureBase(['@request-target'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@request-target'] }, request)).to.deep.equal([ ['"@request-target"', ['/path?param=value']], ]); }); it('derives @path', () => { - expect(httpbis.createSignatureBase(['@path'], request)).to.deep.equal([ + expect(httpbis.createSignatureBase({ fields: ['@path'] }, request)).to.deep.equal([ ['"@path"', ['/path']], ]); }); it('derives @query', () => { - expect(httpbis.createSignatureBase(['@query'], { + expect(httpbis.createSignatureBase({ fields: ['@query'] }, { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', })).to.deep.equal([ ['"@query"', ['?param=value&foo=bar&baz=batman']], ]); - expect(httpbis.createSignatureBase(['@query'], { + expect(httpbis.createSignatureBase({ fields: ['@query'] }, { ...request, url: 'https://www.example.com/path?queryString', })).to.deep.equal([ ['"@query"', ['?queryString']], ]); - expect(httpbis.createSignatureBase(['@query'], { + expect(httpbis.createSignatureBase({ fields: ['@query'] }, { ...request, url: 'https://www.example.com/path', })).to.deep.equal([ @@ -580,19 +580,19 @@ describe('httpbis', () => { ]); }); it('derives @query-param', () => { - expect(httpbis.createSignatureBase(['@query-param;name="baz"'], { + expect(httpbis.createSignatureBase({ fields: ['@query-param;name="baz"'] }, { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ ['"@query-param";name="baz"', ['batman']], ]); - expect(httpbis.createSignatureBase(['@query-param;name="qux"'], { + expect(httpbis.createSignatureBase({ fields: ['@query-param;name="qux"'] }, { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ ['"@query-param";name="qux"', ['']], ]); - expect(httpbis.createSignatureBase(['@query-param;name="param"'], { + expect(httpbis.createSignatureBase({ fields: ['@query-param;name="param"'] }, { ...request, url: 'https://www.example.com/path?param=value&foo=bar&baz=batman&qux=', })).to.deep.equal([ @@ -600,7 +600,7 @@ describe('httpbis', () => { ]); }); it('derives @status', () => { - expect(httpbis.createSignatureBase(['@status'], { + expect(httpbis.createSignatureBase({ fields: ['@status'] }, { status: 200, headers: {}, }, request)).to.deep.equal([ @@ -608,6 +608,27 @@ describe('httpbis', () => { ]); }); }); + describe('user derived component', () => { + const req: Request = { + method: 'get', + headers: { + Host: 'www.example.com', + }, + url: 'https://www.example.com/path?param=value', + }; + it('resolves a component with a supplied resolver', () => { + const resolver = stub(); + resolver.withArgs('@custom').returns(['my value']); + resolver.returns(null) + expect(httpbis.createSignatureBase({ fields: ['@custom', '@method'], componentParser: resolver }, req)).to.deep.equal([ + ['"@custom"', ['my value']], + ['"@method"', ['GET']], + ]); + expect(resolver).to.have.callCount(2); + expect(resolver).to.have.been.calledWith('@custom', new Map(), req); + expect(resolver).to.have.been.calledWith('@method', new Map(), req); + }); + }) describe('full example', () => { const request: Request = { method: 'post', @@ -621,14 +642,14 @@ describe('httpbis', () => { }, }; it('produces a signature base for a request', () => { - expect(httpbis.createSignatureBase([ + expect(httpbis.createSignatureBase({ fields: [ '@method', '@authority', '@path', 'content-digest', 'content-length', 'content-type', - ], request)).to.deep.equal([ + ] }, request)).to.deep.equal([ ['"@method"', ['POST']], ['"@authority"', ['example.com']], ['"@path"', ['/foo']], From 465134a2677caa544e384a264b2c1cf99e0c6ad0 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 3 Oct 2022 16:14:52 +0100 Subject: [PATCH 08/17] Consumers need to provide a key lookup function when verifying messages --- src/cavage/index.ts | 7 ++++--- src/httpbis/index.ts | 26 ++++++++++++++++++++++++-- src/types/index.ts | 40 +++++++++++++++++++++++++++++++++++----- test/cavage/new.spec.ts | 7 +++++-- test/httpbis/new.spec.ts | 18 ++++++++++++------ 5 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/cavage/index.ts b/src/cavage/index.ts index b5ef660..a89ca33 100644 --- a/src/cavage/index.ts +++ b/src/cavage/index.ts @@ -265,7 +265,7 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res } } // now look to verify the signature! Build the expected "signing base" and verify it! - return config.verifier(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { + const params = Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { let keyName = key; let val: Date | number | string; switch (key.toLowerCase()) { @@ -284,7 +284,6 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res keyName = 'keyid'; val = value; break; - // no break default: { if (typeof value === 'string' || typeof value=== 'number') { val = value; @@ -296,5 +295,7 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res return Object.assign(params, { [keyName]: val, }); - }, {})); + }, {}); + const key = await config.keyLookup(params); + return key?.verify(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), params) ?? null; } diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index 74f6bb4..62cc64c 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -22,6 +22,7 @@ import { defaultParams, isRequest, CommonConfig, + VerifyingKey, } from '../types'; export function deriveComponent(component: string, params: Map, res: Response, req?: Request): string[]; @@ -353,7 +354,28 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res const requiredParams = config.requiredParams ?? []; const requiredFields = config.requiredFields ?? []; return Array.from(signatureInputs.entries()).reduce>(async (prev, [name, input]) => { - const result: Error | boolean | null = await prev.catch((e) => e); + const [result, key]: [Error | boolean | null, VerifyingKey] = await Promise.all([ + prev.catch((e) => e), + config.keyLookup(Array.from(input[1].entries()).reduce((params, [key, value]) => { + if (value instanceof ByteSequence) { + Object.assign(params, { + [key]: value.toBase64(), + }); + } else if (value instanceof Token) { + Object.assign(params, { + [key]: value.toString(), + }); + } else { + Object.assign(params, { + [key]: value, + }); + } + return params; + }, {})), + ]); + if (!config.all && !key) { + return null; + } if (!config.all && result === true) { return result; } @@ -407,7 +429,7 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res if (!isByteSequence(signature[0] as BareItem)) { throw new Error('Malformed signature'); } - return config.verifier(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { + return key.verify(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { let val: Date | number | string; switch (key.toLowerCase()) { case 'created': diff --git a/src/types/index.ts b/src/types/index.ts index 842130e..f85712f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,15 +11,40 @@ export interface Response { export type Signer = (data: Buffer) => Promise; export type Verifier = (data: Buffer, signature: Buffer, parameters?: SignatureParameters) => Promise; +export type VerifierFinder = (parameters: SignatureParameters) => Promise; export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512' | string; export interface SigningKey { + /** + * The ID of this key + */ id?: string; + /** + * The algorithm to sign with + */ alg?: Algorithm; + /** + * The Signer function + */ sign: Signer; } +export interface VerifyingKey { + /** + * The ID of this key + */ + id?: string; + /** + * The supported algorithms for this key + */ + algs?: Algorithm[]; + /** + * The Verify function + */ + verify: Verifier; +} + /** * The signature parameters to include in signing */ @@ -47,9 +72,9 @@ export interface SignatureParameters { */ keyid?: string; /** - * A context parameter for the signature + * A tag parameter for the signature */ - context?: string; + tag?: string; [param: string]: Date | number | string | null | undefined; } @@ -105,20 +130,25 @@ export interface SignConfig extends CommonConfig { * of adding creation time (by setting `created: null`) */ paramValues?: SignatureParameters, + /** + * A list of supported algorithms + */ + algs?: Algorithm[]; } /** * Options when verifying signatures */ export interface VerifyConfig extends CommonConfig { - verifier: Verifier; + keyLookup: VerifierFinder; /** - * A maximum age for the signature + * A date that the signature can't have been marked as `created` after * Default: Date.now() + tolerance */ notAfter?: Date | number; /** - * The maximum age of the signature - this overrides the `expires` value for the signature + * The maximum age of the signature - this effectively overrides the `expires` value for the + * signature (unless the expires age is less than the maxAge specified) * if provided */ maxAge?: number; diff --git a/test/cavage/new.spec.ts b/test/cavage/new.spec.ts index 620f63c..397cc73 100644 --- a/test/cavage/new.spec.ts +++ b/test/cavage/new.spec.ts @@ -413,10 +413,12 @@ describe('cavage', () => { }; it('verifies a request', async () => { const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-a' ? { verify: verifierStub } : null); const valid = await cavage.verifyMessage({ - verifier: verifierStub, + keyLookup, }, request); expect(valid).to.equal(true); + expect(keyLookup).to.have.callCount(1); expect(verifierStub).to.have.callCount(1); expect(verifierStub).to.have.been.calledOnceWithExactly( Buffer.from( @@ -450,8 +452,9 @@ describe('cavage', () => { }; it('verifies a response', async () => { const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-a' ? { verify: verifierStub } : null); const result = await cavage.verifyMessage({ - verifier: verifierStub, + keyLookup, }, response); expect(result).to.equal(true); expect(verifierStub).to.have.callCount(1); diff --git a/test/httpbis/new.spec.ts b/test/httpbis/new.spec.ts index cf03ef9..adf35dd 100644 --- a/test/httpbis/new.spec.ts +++ b/test/httpbis/new.spec.ts @@ -1095,10 +1095,12 @@ describe('httpbis', () => { }; it('verifies a request', async () => { const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); const valid = await httpbis.verifyMessage({ - verifier: verifierStub, + keyLookup, }, request); expect(valid).to.equal(true); + expect(keyLookup).to.have.callCount(1); expect(verifierStub).to.have.callCount(1); expect(verifierStub).to.have.been.calledOnceWithExactly( Buffer.from('"@method": POST\n' + @@ -1131,10 +1133,12 @@ describe('httpbis', () => { }; it('verifies a response', async () => { const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-ecc-p256' ? { verify: verifierStub } : null); const result = await httpbis.verifyMessage({ - verifier: verifierStub, + keyLookup, }, response); expect(result).to.equal(true); + expect(keyLookup).to.have.callCount(1); expect(verifierStub).to.have.callCount(1); expect(verifierStub).to.have.been.calledOnceWithExactly( Buffer.from('"@status": 200\n' + @@ -1176,13 +1180,15 @@ describe('httpbis', () => { }, }; it('verifies a response bound to a request', async () => { - const stubVerifier = stub().resolves(true); + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-ecc-p256' ? { verify: verifierStub } : null); const result = await httpbis.verifyMessage({ - verifier: stubVerifier, + keyLookup, }, response, request); expect(result).to.equal(true); - expect(stubVerifier).to.have.callCount(1); - expect(stubVerifier).to.have.been.calledOnceWithExactly( + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( Buffer.from('"@status": 503\n' + '"content-length": 62\n' + '"content-type": application/json\n' + From 89e8703c7c5f77466981b726da08cafb50903427 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 3 Oct 2022 16:16:14 +0100 Subject: [PATCH 09/17] Update some docs and todos as well as code issues --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ src/httpbis/index.ts | 21 +++++++++++++-------- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dedf4a8..ab967d6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,47 @@ The Cavage/RichAnna specifications have changed over time, introducing new featu the [latest version of the specification](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures) and not to try to support each version in isolation. +## Limitations in compliance with the specification + +As with many libraries and environments, HTTP Requests and Responses are abstracted away from the +developer. This fact is noted in the specification. As such (in compliance with the specification), +consumers of this library should take care to make sure that they are processing signatures that +only cover fields/components whose values can be reliably resolved. Below is a list of limitations +that you should be aware of when selecting a list of parameters to sign or accept. + +### Derived component limitations + +Many of the derived components are expected to be sourced from what are effectively http2 pseudo +headers. However, if the application is not running in http2 mode or the message being signed is +not being built as a http2 message, then some of these pseudo headers will not be available to the +application and must be derived from a URL. + +#### @request-target + +The [`@request-target`](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.5) +component is intended to be the equivalent to the "request target portion of the request line". +See the specification for examples of what this means. In NodeJS, this line in requests is automatically +constructed for consumers, so it's not possible to know for certainty what this will be. For incoming +requests, it is possible to extract, but for simplicity’s sake this library does not process the raw +headers for the incoming request and, as such, cannot calculate this value with certainty. It is +recommended that this component is avoided. + +### Multiple message component contexts + +As described in [section 7.4.4](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-7.4.4) +it is deemed that complex message context resolution is outside the scope of this library. + +This means that it is the responsibility of the consumer of this library to construct the equivalent +message context for signatures that need to be reinterpreted based on other signer contexts. + + +### Padding attacks + +As described in [section 7.5.7](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-13#section-7.5.7) +it is expected that the NodeJS application has taken steps to ensure that headers are valid and not +"garbage". For this library to take on that obligation would be to widen the scope of the library to +a complete HTTP Message validator. + ## Examples ### Signing a request diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index 62cc64c..b5664cd 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -31,6 +31,8 @@ export function deriveComponent(component: string, params: Map, message: Request | Response, req?: Request): string[] { // switch the context of the signing data depending on if the `req` flag was passed @@ -94,12 +96,6 @@ export function deriveComponent(component: string, params: Map Date: Mon, 3 Oct 2022 17:19:50 +0100 Subject: [PATCH 10/17] Add support for ecdsa-p384-sha384 signatures --- src/algorithm/index.ts | 6 ++ test/algorithm/ecdsa-p256-sha256.ts | 2 +- test/algorithm/ecdsa-p384-sha384.ts | 40 +++++++++ test/httpbis/{new.spec.ts => httpbis.spec.ts} | 17 ++++ test/httpbis/httpbis.ts | 84 ------------------- 5 files changed, 64 insertions(+), 85 deletions(-) create mode 100644 test/algorithm/ecdsa-p384-sha384.ts rename test/httpbis/{new.spec.ts => httpbis.spec.ts} (96%) delete mode 100644 test/httpbis/httpbis.ts diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index ecec339..6dae820 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -51,6 +51,9 @@ export function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | Si case 'ecdsa-p256-sha256': signer.sign = async (data: Buffer) => createSign('sha256').update(data).sign(key as KeyLike); break; + case 'ecdsa-p384-sha384': + signer.sign = async (data: Buffer) => createSign('sha384').update(data).sign(key as KeyLike); + break; case 'ed25519': signer.sign = async (data: Buffer) => sign(null, data, key as KeyLike); // signer.sign = async (data: Buffer) => createSign('ed25519').update(data).sign(key as KeyLike); @@ -106,6 +109,9 @@ export function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput case 'ecdsa-p256-sha256': verifier = async (data: Buffer, signature: Buffer) => createVerify('sha256').update(data).verify(key as KeyLike, signature); break; + case 'ecdsa-p384-sha384': + verifier = async (data: Buffer, signature: Buffer) => createVerify('sha384').update(data).verify(key as KeyLike, signature); + break; case 'ed25519': verifier = async (data: Buffer, signature: Buffer) => verify(null, data, key as KeyLike, signature) as unknown as boolean; break; diff --git a/test/algorithm/ecdsa-p256-sha256.ts b/test/algorithm/ecdsa-p256-sha256.ts index 8f16dd1..688ca1c 100644 --- a/test/algorithm/ecdsa-p256-sha256.ts +++ b/test/algorithm/ecdsa-p256-sha256.ts @@ -34,7 +34,7 @@ describe('ecdsa-p256-sha256', () => { it('verifies a signature', async () => { const verifier = createVerifier(ecdsaKeyPair.publicKey, 'ecdsa-p256-sha256'); const data = Buffer.from('some random data'); - const sig = sign('sha512', data, ecdsaKeyPair.privateKey); + const sig = sign('sha256', data, ecdsaKeyPair.privateKey); expect(sig).to.satisfy((arg: Buffer) => verifier(data, arg)); }); }); diff --git a/test/algorithm/ecdsa-p384-sha384.ts b/test/algorithm/ecdsa-p384-sha384.ts new file mode 100644 index 0000000..28ecfef --- /dev/null +++ b/test/algorithm/ecdsa-p384-sha384.ts @@ -0,0 +1,40 @@ +import { generateKeyPair, sign, verify } from 'crypto'; +import { promisify } from 'util'; +import { createSigner, createVerifier } from '../../src'; +import { expect } from 'chai'; + +describe('ecdsa-p384-sha384', () => { + describe('internal tests', () => { + let ecdsaKeyPair: { publicKey: string, privateKey: string }; + before('generate key pair', async () => { + ecdsaKeyPair = await promisify(generateKeyPair)('ec', { + namedCurve: 'P-384', + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + }); + describe('signing', () => { + it('signs a payload', async () => { + const signer = createSigner(ecdsaKeyPair.privateKey, 'ecdsa-p384-sha384'); + const data = Buffer.from('some random data'); + const sig = await signer.sign(data); + expect(signer.alg).to.equal('ecdsa-p384-sha384'); + expect(sig).to.satisfy((arg: Buffer) => verify('sha384', data, ecdsaKeyPair.publicKey, arg)); + }); + }); + describe('verifying', () => { + it('verifies a signature', async () => { + const verifier = createVerifier(ecdsaKeyPair.publicKey, 'ecdsa-p384-sha384'); + const data = Buffer.from('some random data'); + const sig = sign('sha384', data, ecdsaKeyPair.privateKey); + expect(sig).to.satisfy((arg: Buffer) => verifier(data, arg)); + }); + }); + }); +}); diff --git a/test/httpbis/new.spec.ts b/test/httpbis/httpbis.spec.ts similarity index 96% rename from test/httpbis/new.spec.ts rename to test/httpbis/httpbis.spec.ts index adf35dd..e5597b4 100644 --- a/test/httpbis/new.spec.ts +++ b/test/httpbis/httpbis.spec.ts @@ -320,6 +320,23 @@ describe('httpbis', () => { }); }); describe('.extractHeader', () => { + describe('general header extraction', () => { + const headers = { + 'testheader': 'test', + 'test-header-1': 'test1', + 'Test-Header-2': 'test2', + 'test-Header-3': 'test3', + 'TEST-HEADER-4': 'test4', + }; + Object.entries(headers).forEach(([headerName, expectedValue]) => { + it(`successfully extracts a matching header (${headerName})`, () => { + expect(httpbis.extractHeader(headerName.toLowerCase(), new Map(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); + }); + }); + it('throws on missing headers', () => { + expect(() => httpbis.extractHeader('missing', new Map(), { headers } as unknown as Request)).to.throw(Error, 'No header "missing" found in headers'); + }); + }); describe('raw headers', () => { const request: Request = { method: 'POST', diff --git a/test/httpbis/httpbis.ts b/test/httpbis/httpbis.ts deleted file mode 100644 index bf8b20e..0000000 --- a/test/httpbis/httpbis.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Request } from '../../src'; -import { - deriveComponent, - extractHeader, -} from '../../src/httpbis'; -import { expect } from 'chai'; - -describe('httpbis', () => { - describe('.extractHeader', () => { - const headers = { - 'testheader': 'test', - 'test-header-1': 'test1', - 'Test-Header-2': 'test2', - 'test-Header-3': 'test3', - 'TEST-HEADER-4': 'test4', - }; - Object.entries(headers).forEach(([headerName, expectedValue]) => { - it(`successfully extracts a matching header (${headerName})`, () => { - expect(extractHeader(headerName.toLowerCase(), new Map(), { headers } as unknown as Request)).to.deep.equal([expectedValue]); - }); - }); - it('throws on missing headers', () => { - expect(() => extractHeader('missing', new Map(), { headers } as unknown as Request)).to.throw(Error, 'No header "missing" found in headers'); - }); - }); - describe('.deriveComponent', () => { - it('correctly extracts the @method', () => { - const result = deriveComponent('@method', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['POST']); - }); - it('correctly extracts the @target-uri', () => { - const result = deriveComponent('@target-uri', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['https://www.example.com/path?param=value']); - }); - it('correctly extracts the @authority', () => { - const result = deriveComponent('@authority', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['www.example.com']); - }); - it('correctly extracts the @scheme', () => { - const result = deriveComponent('@scheme', new Map(), { - method: 'POST', - url: 'http://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['http']); - }); - it('correctly extracts the @request-target', () => { - const result = deriveComponent('@request-target', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['/path?param=value']); - }); - it('correctly extracts the @path', () => { - const result = deriveComponent('@path', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value', - } as unknown as Request); - expect(result).to.deep.equal(['/path']); - }); - it('correctly extracts the @query', () => { - const result = deriveComponent('@query', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?param=value&foo=bar&baz=batman', - } as unknown as Request); - expect(result).to.deep.equal(['?param=value&foo=bar&baz=batman']); - }); - it('correctly extracts the @query', () => { - const result = deriveComponent('@query', new Map(), { - method: 'POST', - url: 'https://www.example.com/path?queryString', - } as unknown as Request); - expect(result).to.deep.equal(['?queryString']); - }); - }); -}); From e61237b979791bc590d3178c10d75446fe4e850c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 3 Oct 2022 17:22:16 +0100 Subject: [PATCH 11/17] Start throwing specific error classes --- src/errors/index.ts | 1 + src/errors/unsupported-algorithm-error.ts | 6 ++++++ src/httpbis/index.ts | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 src/errors/index.ts create mode 100644 src/errors/unsupported-algorithm-error.ts diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..cc1f5da --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1 @@ +export { UnsupportedAlgorithmError } from './unsupported-algorithm-error'; diff --git a/src/errors/unsupported-algorithm-error.ts b/src/errors/unsupported-algorithm-error.ts new file mode 100644 index 0000000..b770515 --- /dev/null +++ b/src/errors/unsupported-algorithm-error.ts @@ -0,0 +1,6 @@ +/** + * Thrown when a key is presented to verify a signature with + * an algorithm that is not supported + */ +export class UnsupportedAlgorithmError extends Error { +} diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index b5664cd..3586b16 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -24,6 +24,7 @@ import { CommonConfig, VerifyingKey, } from '../types'; +import { UnsupportedAlgorithmError } from '../errors'; export function deriveComponent(component: string, params: Map, res: Response, req?: Request): string[]; export function deriveComponent(component: string, params: Map, req: Request): string[]; @@ -378,6 +379,10 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res return params; }, {})), ]); + if (input[1].has('alg') && key.algs?.includes(input[1].get('alg') as string) === false) { + throw new UnsupportedAlgorithmError('Unsupported key algorithm'); + } + // @todo - confirm this is all working as expected if (!config.all && !key) { return null; } From 674aac391b29e136525025d76d86e4859e9ac07a Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 4 Oct 2022 09:22:36 +0100 Subject: [PATCH 12/17] Add code coverage --- .github/workflows/nodejs.yml | 17 +- .gitignore | 2 + .nycrc | 5 + package-lock.json | 2694 +++++++++++++++++++++++++++++++++- package.json | 4 +- 5 files changed, 2718 insertions(+), 4 deletions(-) create mode 100644 .nycrc diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f2342fc..8410532 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -28,7 +28,20 @@ jobs: cache: 'npm' - run: npm ci - run: npm run lint - +# coverage: +# name: Coverage check +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - name: Code coverage +# uses: actions/setup-node@v2 +# with: +# node-version: 12.x +# cache: 'npm' +# - run: npm ci +# - run: npm run test:coverage +# - name: Code Coverage Report +# uses: romeovs/lcov-reporter-action@v0.2.11 tests: name: Unit tests runs-on: ubuntu-latest @@ -46,4 +59,4 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci - - run: npm test + - run: npm run test:coverage diff --git a/.gitignore b/.gitignore index 74fc3a2..330cec9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /lib/ /node_modules/ +/.nyc_output/ +/coverage/ diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..93adc24 --- /dev/null +++ b/.nycrc @@ -0,0 +1,5 @@ +{ + "all": true, + "extension": [".ts"], + "include": ["src/**"] +} diff --git a/package-lock.json b/package-lock.json index 09c3713..8e690a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^8.24.0", "mocha": "^10.0.0", "mockdate": "^3.0.5", + "nyc": "^15.1.0", "sinon": "^14.0.0", "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", @@ -39,6 +40,447 @@ "node": ">=0.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -140,6 +582,137 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -149,6 +722,15 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -541,6 +1123,19 @@ "node": ">=0.4.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -603,6 +1198,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -676,6 +1289,53 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -697,6 +1357,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001518", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz", + "integrity": "sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -779,6 +1459,15 @@ "node": ">= 6" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -808,12 +1497,24 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -881,6 +1582,21 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -914,12 +1630,24 @@ "node": ">=6.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.480", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.480.tgz", + "integrity": "sha512-IXTgg+bITkQv/FLP9FjX6f9KFCs5hQWeh5uNSKxB9mqYj/JXhHDbu+ekS43LVvbkL3eW6/oZy4+r9Om6lan1Uw==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1062,6 +1790,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -1201,6 +1942,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1245,6 +2003,39 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "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/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1265,6 +2056,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1283,6 +2083,15 @@ "node": "*" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -1350,6 +2159,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1365,6 +2180,31 @@ "node": ">=8" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1374,6 +2214,12 @@ "he": "bin/he" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1408,6 +2254,15 @@ "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", @@ -1493,6 +2348,24 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -1505,6 +2378,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -1517,6 +2399,130 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1529,6 +2535,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1541,6 +2559,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -1575,6 +2605,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1624,6 +2660,30 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1813,6 +2873,24 @@ "type-detect": "4.0.8" } }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1822,6 +2900,192 @@ "node": ">=0.10.0" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1878,6 +3142,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1944,6 +3244,12 @@ "node": "*" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1956,6 +3262,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1965,6 +3335,18 @@ "node": ">= 0.8.0" } }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -2015,6 +3397,18 @@ "node": ">=8.10.0" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2024,6 +3418,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2125,6 +3525,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2146,6 +3552,12 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/sinon": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", @@ -2183,6 +3595,38 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2209,6 +3653,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2232,7 +3685,21 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, "engines": { "node": ">=8" @@ -2244,6 +3711,15 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2368,6 +3844,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -2381,6 +3866,36 @@ "node": ">=14.17" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2390,6 +3905,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -2411,6 +3935,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -2440,6 +3970,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -2526,6 +4068,351 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true + }, + "@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "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 + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2596,12 +4483,118 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -2887,6 +4880,16 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2930,6 +4933,21 @@ "picomatch": "^2.0.4" } }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2991,6 +5009,30 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + } + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3003,6 +5045,12 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001518", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz", + "integrity": "sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==", + "dev": true + }, "chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -3061,6 +5109,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -3087,12 +5141,24 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3140,6 +5206,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -3164,12 +5239,24 @@ "esutils": "^2.0.2" } }, + "electron-to-chromium": { + "version": "1.4.480", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.480.tgz", + "integrity": "sha512-IXTgg+bITkQv/FLP9FjX6f9KFCs5hQWeh5uNSKxB9mqYj/JXhHDbu+ekS43LVvbkL3eW6/oZy4+r9Om6lan1Uw==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3272,6 +5359,12 @@ "eslint-visitor-keys": "^3.4.1" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -3387,6 +5480,17 @@ "to-regex-range": "^5.0.1" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3419,6 +5523,22 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3432,6 +5552,12 @@ "dev": true, "optional": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3444,6 +5570,12 @@ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -3490,6 +5622,12 @@ "slash": "^3.0.0" } }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3502,12 +5640,36 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3530,6 +5692,12 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "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 + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3594,12 +5762,30 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -3612,6 +5798,104 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3621,6 +5905,12 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3633,6 +5923,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -3658,6 +5954,12 @@ "p-locate": "^5.0.0" } }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -3698,6 +6000,23 @@ "yallist": "^4.0.0" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3858,12 +6177,176 @@ } } }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3905,6 +6388,33 @@ "p-limit": "^3.0.2" } }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3953,18 +6463,81 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, "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 }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3995,12 +6568,27 @@ "picomatch": "^2.2.1" } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4055,6 +6643,12 @@ "randombytes": "^2.1.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4070,6 +6664,12 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "sinon": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", @@ -4097,6 +6697,32 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4117,6 +6743,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4137,12 +6769,29 @@ "has-flag": "^4.0.0" } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "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 }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4223,12 +6872,31 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4238,6 +6906,12 @@ "punycode": "^2.1.0" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4253,6 +6927,12 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -4276,6 +6956,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 2499c97..70c4c5d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint:fix": "npm run lint --silent -- --fix", "prepare": "npm run build", "preversion": "npm run lint", - "test": "mocha -r ts-node/register -r test/bootstrap.ts test/**/*.ts" + "test": "mocha -r ts-node/register -r test/bootstrap.ts test/**/*.ts", + "test:coverage": "nyc --reporter=lcov --reporter=text-summary npm run test" }, "repository": { "type": "git", @@ -43,6 +44,7 @@ "eslint": "^8.24.0", "mocha": "^10.0.0", "mockdate": "^3.0.5", + "nyc": "^15.1.0", "sinon": "^14.0.0", "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", From ec3d2c9dbbfe54ad73353d0bbf72eed4a659f897 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 11 Oct 2022 15:36:33 +0100 Subject: [PATCH 13/17] Test coverage for httpbis spec --- src/algorithm/index.ts | 5 +- src/errors/expired-error.ts | 4 + src/errors/index.ts | 6 + src/errors/malformed-signature-error.ts | 4 + src/errors/unacceptable-signature-error.ts | 4 + src/errors/unknown-algorithm-error.ts | 5 + src/errors/unknown-key-error.ts | 4 + src/errors/unsupported-algorithm-error.ts | 4 +- src/errors/verification-error.ts | 2 + src/httpbis/index.ts | 115 +-- src/index.ts | 3 +- src/types/index.ts | 2 +- test/algorithm/algorithm.spec.ts | 35 + test/algorithm/ecdsa-p256-sha256.ts | 18 +- test/algorithm/ed25519.ts | 2 +- test/algorithm/rsa-pss-sha512.ts | 2 +- test/errors/errors.spec.ts | 8 + .../{ecdsa-p256.pem => test-key-ecc-p256.pem} | 0 .../etc/{ed25519.pem => test-key-ed25519.pem} | 0 .../etc/{rsa-pss.pem => test-key-rsa-pss.pem} | 0 test/etc/{rsa.pem => test-key-rsa.pem} | 0 test/etc/test-shared-secret.txt | 1 + test/httpbis/httpbis.int.ts | 702 +++++++++++++ test/httpbis/httpbis.spec.ts | 976 +++++++++++++++++- test/structured-headers.spec.ts | 66 ++ 25 files changed, 1883 insertions(+), 85 deletions(-) create mode 100644 src/errors/expired-error.ts create mode 100644 src/errors/malformed-signature-error.ts create mode 100644 src/errors/unacceptable-signature-error.ts create mode 100644 src/errors/unknown-algorithm-error.ts create mode 100644 src/errors/unknown-key-error.ts create mode 100644 src/errors/verification-error.ts create mode 100644 test/algorithm/algorithm.spec.ts create mode 100644 test/errors/errors.spec.ts rename test/etc/{ecdsa-p256.pem => test-key-ecc-p256.pem} (100%) rename test/etc/{ed25519.pem => test-key-ed25519.pem} (100%) rename test/etc/{rsa-pss.pem => test-key-rsa-pss.pem} (100%) rename test/etc/{rsa.pem => test-key-rsa.pem} (100%) create mode 100644 test/etc/test-shared-secret.txt create mode 100644 test/httpbis/httpbis.int.ts create mode 100644 test/structured-headers.spec.ts diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index 6dae820..e78ba35 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -14,6 +14,7 @@ import { } from 'crypto'; import { RSA_PKCS1_PADDING, RSA_PKCS1_PSS_PADDING } from 'constants'; import { SigningKey, Algorithm, Verifier } from '../types'; +import { UnknownAlgorithmError } from '../errors'; /** * A helper method for easier consumption of the library. @@ -59,7 +60,7 @@ export function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | Si // signer.sign = async (data: Buffer) => createSign('ed25519').update(data).sign(key as KeyLike); break; default: - throw new Error(`Unsupported signing algorithm ${alg}`); + throw new UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`); } if (id) { signer.id = id; @@ -116,7 +117,7 @@ export function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput verifier = async (data: Buffer, signature: Buffer) => verify(null, data, key as KeyLike, signature) as unknown as boolean; break; default: - throw new Error(`Unsupported signing algorithm ${alg}`); + throw new UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`); } return Object.assign(verifier, { alg }); } diff --git a/src/errors/expired-error.ts b/src/errors/expired-error.ts new file mode 100644 index 0000000..75197d9 --- /dev/null +++ b/src/errors/expired-error.ts @@ -0,0 +1,4 @@ +import { VerificationError } from './verification-error'; + +export class ExpiredError extends VerificationError { +} diff --git a/src/errors/index.ts b/src/errors/index.ts index cc1f5da..8927c01 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1 +1,7 @@ +export { ExpiredError } from './expired-error'; +export { MalformedSignatureError } from './malformed-signature-error'; +export { UnacceptableSignatureError } from './unacceptable-signature-error'; +export { UnknownAlgorithmError } from './unknown-algorithm-error'; +export { UnknownKeyError } from './unknown-key-error'; export { UnsupportedAlgorithmError } from './unsupported-algorithm-error'; +export { VerificationError } from './verification-error'; diff --git a/src/errors/malformed-signature-error.ts b/src/errors/malformed-signature-error.ts new file mode 100644 index 0000000..7ed0441 --- /dev/null +++ b/src/errors/malformed-signature-error.ts @@ -0,0 +1,4 @@ +import { VerificationError } from './verification-error'; + +export class MalformedSignatureError extends VerificationError { +} diff --git a/src/errors/unacceptable-signature-error.ts b/src/errors/unacceptable-signature-error.ts new file mode 100644 index 0000000..a6bc23d --- /dev/null +++ b/src/errors/unacceptable-signature-error.ts @@ -0,0 +1,4 @@ +import { VerificationError } from './verification-error'; + +export class UnacceptableSignatureError extends VerificationError { +} diff --git a/src/errors/unknown-algorithm-error.ts b/src/errors/unknown-algorithm-error.ts new file mode 100644 index 0000000..4939f83 --- /dev/null +++ b/src/errors/unknown-algorithm-error.ts @@ -0,0 +1,5 @@ +/** + * Thrown when a verifier/signer is created with an unknown algorithm + */ +export class UnknownAlgorithmError extends Error { +} diff --git a/src/errors/unknown-key-error.ts b/src/errors/unknown-key-error.ts new file mode 100644 index 0000000..587f2e3 --- /dev/null +++ b/src/errors/unknown-key-error.ts @@ -0,0 +1,4 @@ +import { VerificationError } from './verification-error'; + +export class UnknownKeyError extends VerificationError { +} diff --git a/src/errors/unsupported-algorithm-error.ts b/src/errors/unsupported-algorithm-error.ts index b770515..980b1f6 100644 --- a/src/errors/unsupported-algorithm-error.ts +++ b/src/errors/unsupported-algorithm-error.ts @@ -1,6 +1,8 @@ +import { VerificationError } from './verification-error'; + /** * Thrown when a key is presented to verify a signature with * an algorithm that is not supported */ -export class UnsupportedAlgorithmError extends Error { +export class UnsupportedAlgorithmError extends VerificationError { } diff --git a/src/errors/verification-error.ts b/src/errors/verification-error.ts new file mode 100644 index 0000000..619ce5b --- /dev/null +++ b/src/errors/verification-error.ts @@ -0,0 +1,2 @@ +export class VerificationError extends Error { +} diff --git a/src/httpbis/index.ts b/src/httpbis/index.ts index 3586b16..005bd41 100644 --- a/src/httpbis/index.ts +++ b/src/httpbis/index.ts @@ -17,6 +17,7 @@ import { Dictionary, parseHeader, quoteString } from '../structured-header'; import { Request, Response, + SignatureParameters, SignConfig, VerifyConfig, defaultParams, @@ -24,7 +25,13 @@ import { CommonConfig, VerifyingKey, } from '../types'; -import { UnsupportedAlgorithmError } from '../errors'; +import { + ExpiredError, + MalformedSignatureError, + UnacceptableSignatureError, + UnknownKeyError, + UnsupportedAlgorithmError, +} from '../errors'; export function deriveComponent(component: string, params: Map, res: Response, req?: Request): string[]; export function deriveComponent(component: string, params: Map, req: Request): string[]; @@ -49,7 +56,7 @@ export function deriveComponent(component: string, params: Map, signa let signatureName = name ?? 'sig'; if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) { let count = 0; - while (signatureHeader?.has(`${signatureName}${count}`) || inputHeader?.has(`${signatureName}${count}`)) { + while (signatureHeader.has(`${signatureName}${count}`) || inputHeader.has(`${signatureName}${count}`)) { count++; } signatureName += count.toString(); @@ -308,7 +315,7 @@ export async function signMessage(config: SignConfi export async function signMessage(config: SignConfig, message: T, req?: U): Promise { const signingParameters = createSigningParameters(config); const signatureBase = createSignatureBase({ - fields: config?.fields ?? [], + fields: config.fields ?? [], componentParser: config.componentParser, }, message as Response, req); const signatureInput = serializeList([ @@ -323,7 +330,7 @@ export async function signMessage>(async (prev, [name, input]) => { - const [result, key]: [Error | boolean | null, VerifyingKey] = await Promise.all([ + const signatureParams: SignatureParameters = Array.from(input[1].entries()).reduce((params, [key, value]) => { + if (value instanceof ByteSequence) { + Object.assign(params, { + [key]: value.toBase64(), + }); + } else if (value instanceof Token) { + Object.assign(params, { + [key]: value.toString(), + }); + } else if (key === 'created' || key === 'expired') { + Object.assign(params, { + [key]: new Date((value as number) * 1000), + }); + } else { + Object.assign(params, { + [key]: value, + }); + } + return params; + }, {}); + const [result, key]: [Error | boolean | null, VerifyingKey | null] = await Promise.all([ prev.catch((e) => e), - config.keyLookup(Array.from(input[1].entries()).reduce((params, [key, value]) => { - if (value instanceof ByteSequence) { - Object.assign(params, { - [key]: value.toBase64(), - }); - } else if (value instanceof Token) { - Object.assign(params, { - [key]: value.toString(), - }); - } else { - Object.assign(params, { - [key]: value, - }); - } - return params; - }, {})), + config.keyLookup(signatureParams), ]); - if (input[1].has('alg') && key.algs?.includes(input[1].get('alg') as string) === false) { - throw new UnsupportedAlgorithmError('Unsupported key algorithm'); - } // @todo - confirm this is all working as expected - if (!config.all && !key) { - return null; - } - if (!config.all && result === true) { - return result; + if (config.all && !key) { + throw new UnknownKeyError('Unknown key'); } - if (config.all && result !== true && result !== null) { + if (!key) { if (result instanceof Error) { throw result; } return result; } + if (input[1].has('alg') && key.algs?.includes(input[1].get('alg') as string) === false) { + throw new UnsupportedAlgorithmError('Unsupported key algorithm'); + } if (!isInnerList(input)) { - throw new Error('Malformed signature input'); + throw new MalformedSignatureError('Malformed signature input'); } const hasRequiredParams = requiredParams.every((param) => input[1].has(param)); if (!hasRequiredParams) { - return false; + throw new UnacceptableSignatureError('Missing required signature parameters'); } // this could be tricky, what if we say "@method" but there is "@method;req" const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field)); if (!hasRequiredFields) { - return false; + throw new UnacceptableSignatureError('Missing required signed fields'); } if (input[1].has('created')) { const created = input[1].get('created') as number - tolerance; // maxAge overrides expires. // signature is older than maxAge - if (maxAge && created - now > maxAge) { - return false; - } - // created after the allowed time (ie: created in the future) - if (created > notAfter) { - return false; + if ((maxAge && now - created > maxAge) || created > notAfter) { + throw new ExpiredError('Signature is too old'); } } if (input[1].has('expires')) { const expires = input[1].get('expires') as number + tolerance; // expired signature - if (expires > now) { - return false; + if (now > expires) { + throw new ExpiredError('Signature has expired'); } } @@ -434,29 +439,11 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res const base = formatSignatureBase(signingBase); const signature = signatures.get(name); if (!signature) { - throw new Error('No signature found for inputs'); + throw new MalformedSignatureError('No corresponding signature for input'); } if (!isByteSequence(signature[0] as BareItem)) { - throw new Error('Malformed signature'); + throw new MalformedSignatureError('Malformed signature'); } - return key.verify(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => { - let val: Date | number | string; - switch (key.toLowerCase()) { - case 'created': - case 'expires': - val = new Date((value as number) * 1000); - break; - default: { - if (typeof value === 'string' || typeof value=== 'number') { - val = value; - } else { - val = value.toString(); - } - } - } - return Object.assign(params, { - [key]: val, - }); - }, {})); + return key.verify(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), signatureParams); }, Promise.resolve(null)); } diff --git a/src/index.ts b/src/index.ts index fbf44a9..26251d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export * from './algorithm'; export * from './types'; +export * from './errors'; export * as default from './httpbis'; -export * as httpis from './httpbis'; +export * as httpbis from './httpbis'; export * as cavage from './cavage'; diff --git a/src/types/index.ts b/src/types/index.ts index f85712f..85a7460 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,7 +11,7 @@ export interface Response { export type Signer = (data: Buffer) => Promise; export type Verifier = (data: Buffer, signature: Buffer, parameters?: SignatureParameters) => Promise; -export type VerifierFinder = (parameters: SignatureParameters) => Promise; +export type VerifierFinder = (parameters: SignatureParameters) => Promise; export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'hmac-sha256' | 'rsa-pss-sha512' | string; diff --git a/test/algorithm/algorithm.spec.ts b/test/algorithm/algorithm.spec.ts new file mode 100644 index 0000000..9f05c43 --- /dev/null +++ b/test/algorithm/algorithm.spec.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import { createSigner, createVerifier, UnknownAlgorithmError } from '../../src'; + +describe('algorithm', () => { + describe('.createSigner', () => { + it('throws for unknown algs', () => { + try { + createSigner(Buffer.from(''), 'unknown-alg'); + } catch (e) { + expect(e).to.be.instanceOf(UnknownAlgorithmError); + return; + } + expect.fail('Expected to throw'); + }); + it('adds the id prop if provided', () => { + const signer = createSigner(Buffer.from(''), 'hmac-sha256', 'my-id'); + expect(signer).to.have.property('id', 'my-id'); + }); + it('has no id prop if not provided', () => { + const signer = createSigner(Buffer.from(''), 'hmac-sha256'); + expect(signer).to.not.have.property('id'); + }); + }); + describe('.createVerifier', () => { + it('throws for unknown algs', () => { + try { + createVerifier(Buffer.from(''), 'unknown-alg'); + } catch (e) { + expect(e).to.be.instanceOf(UnknownAlgorithmError); + return; + } + expect.fail('Expected to throw'); + }); + }); +}); diff --git a/test/algorithm/ecdsa-p256-sha256.ts b/test/algorithm/ecdsa-p256-sha256.ts index 688ca1c..4678349 100644 --- a/test/algorithm/ecdsa-p256-sha256.ts +++ b/test/algorithm/ecdsa-p256-sha256.ts @@ -41,23 +41,23 @@ describe('ecdsa-p256-sha256', () => { }); describe('specification examples', () => { let ecKeyPem: string; - before('load rsa key', async () => { - ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/ecdsa-p256.pem'))).toString(); + before('load key', async () => { + ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/test-key-ecc-p256.pem'))).toString(); }); describe('response signing', () => { - const data = Buffer.from('"content-type": application/json\n' + - '"digest": SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n' + - '"content-length": 18\n' + - '"@signature-params": ("content-type" "digest" "content-length");created=1618884475;keyid="test-key-ecc-p256"'); + const data = Buffer.from('"@status": 200\n' + + '"content-type": application/json\n' + + '"content-digest": sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:\n' + + '"content-length": 23\n' + + '"@signature-params": ("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"'); it('successfully signs a payload', async () => { const sig = await (createSigner(ecKeyPem, 'ecdsa-p256-sha256').sign(data)); expect(sig).to.satisfy((arg: Buffer) => verify('sha256', data, ecKeyPem, arg)); }); - // seems to be broken in node - Error: error:0D07209B:asn1 encoding routines:ASN1_get_object:too long + // seems to be broken in node - Error: error:0D07207B:asn1 encoding routines:ASN1_get_object:header too long // could be to do with https://stackoverflow.com/a/39575576 it.skip('successfully verifies a signature', async () => { - const sig = Buffer.from('n8RKXkj0iseWDmC6PNSQ1GX2R9650v+lhbb6rTGoSrSSx18zmn6fPOtBx48/WffYLO0n1RHHf9scvNGAgGq52Q==', 'base64'); - expect(sig).to.satisfy((arg: Buffer) => verify('sha256', Buffer.from(data), ecKeyPem, arg)); + const sig = Buffer.from('wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==', 'base64'); expect(await (createVerifier(ecKeyPem, 'ecdsa-p256-sha256')(data, sig))).to.equal(true); }); }); diff --git a/test/algorithm/ed25519.ts b/test/algorithm/ed25519.ts index 44e23b6..75a86d4 100644 --- a/test/algorithm/ed25519.ts +++ b/test/algorithm/ed25519.ts @@ -41,7 +41,7 @@ describe('ed25519', () => { describe('specification examples', () => { let ecKeyPem: string; before('load rsa key', async () => { - ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/ed25519.pem'))).toString(); + ecKeyPem = (await promisify(readFile)(join(__dirname, '../etc/test-key-ed25519.pem'))).toString(); }); describe('response signing', () => { const data = Buffer.from('"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + diff --git a/test/algorithm/rsa-pss-sha512.ts b/test/algorithm/rsa-pss-sha512.ts index 1206f8f..6bf7ac3 100644 --- a/test/algorithm/rsa-pss-sha512.ts +++ b/test/algorithm/rsa-pss-sha512.ts @@ -49,7 +49,7 @@ describe('rsa-pss-sha512', () => { describe('specification examples', () => { let rsaKeyPem: string; before('load rsa key', async () => { - rsaKeyPem = (await promisify(readFile)(join(__dirname, '../etc/rsa-pss.pem'))).toString(); + rsaKeyPem = (await promisify(readFile)(join(__dirname, '../etc/test-key-rsa-pss.pem'))).toString(); }); describe('minimal example', () => { const data = Buffer.from('"@signature-params": ();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"'); diff --git a/test/errors/errors.spec.ts b/test/errors/errors.spec.ts new file mode 100644 index 0000000..276aaa7 --- /dev/null +++ b/test/errors/errors.spec.ts @@ -0,0 +1,8 @@ +import * as errors from '../../src/errors'; +import { expect } from 'chai'; + +describe('errors', () => { + it('has all errors', () => { + expect(Object.values(errors)).to.have.lengthOf(7); + }); +}); diff --git a/test/etc/ecdsa-p256.pem b/test/etc/test-key-ecc-p256.pem similarity index 100% rename from test/etc/ecdsa-p256.pem rename to test/etc/test-key-ecc-p256.pem diff --git a/test/etc/ed25519.pem b/test/etc/test-key-ed25519.pem similarity index 100% rename from test/etc/ed25519.pem rename to test/etc/test-key-ed25519.pem diff --git a/test/etc/rsa-pss.pem b/test/etc/test-key-rsa-pss.pem similarity index 100% rename from test/etc/rsa-pss.pem rename to test/etc/test-key-rsa-pss.pem diff --git a/test/etc/rsa.pem b/test/etc/test-key-rsa.pem similarity index 100% rename from test/etc/rsa.pem rename to test/etc/test-key-rsa.pem diff --git a/test/etc/test-shared-secret.txt b/test/etc/test-shared-secret.txt new file mode 100644 index 0000000..558db87 --- /dev/null +++ b/test/etc/test-shared-secret.txt @@ -0,0 +1 @@ +uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ== diff --git a/test/httpbis/httpbis.int.ts b/test/httpbis/httpbis.int.ts new file mode 100644 index 0000000..acd86af --- /dev/null +++ b/test/httpbis/httpbis.int.ts @@ -0,0 +1,702 @@ +import * as http from 'http'; +import * as http2 from 'http2'; +import { Server } from 'net'; +import { expect } from 'chai'; +import { + createVerifier, + httpbis, + Request, + UnacceptableSignatureError, + VerifyingKey, +} from '../../src'; +import { promises as fs } from 'fs'; +import { parse } from 'path'; +import { stub } from 'sinon'; + +interface ServerConfig { + port: number; + privateKey?: string; +} + +interface TestServer { + server: Server, + start: () => Promise; + stop: () => Promise; + requests: Request[]; + clear: () => void; +} + +function createHttpServer(config: ServerConfig): TestServer { + const requests: Request[] = []; + const server = http.createServer((req) => { + const domain = req.headers.host ?? 'localhost'; + const request: Request = { + method: req.method as string, + headers: req.headers as Record, + url: `http://${domain}${req.url}`, + }; + requests.push(request); + }); + return { + server, + start: () => new Promise((resolve) => { + server.once('listening', () => resolve()); + server.listen(config.port); + }), + stop: () => new Promise((resolve) => server.close(() => resolve())), + requests, + clear: () => requests.splice(0), + }; +} + +function createHttp2Server(config: ServerConfig): TestServer { + const requests: Request[] = []; + const server = http2.createServer(); + server.on('stream', (stream, headers) => { + const domain = headers[':authority'] ?? 'localhost'; + const request: Request = { + method: headers[':method'] as string, + headers: headers as Record, + url: `http://${domain}${headers[':path']}`, + }; + requests.push(request); + }); + return { + server, + start: () => new Promise((resolve) => { + server.once('listening', () => resolve()); + server.listen(config.port); + }), + stop: () => new Promise((resolve) => server.close(() => resolve())), + requests, + clear: () => requests.splice(0), + }; +} + +function makeHttpRequest(request: Request, port?: number): Promise { + return new Promise((resolve, reject) => { + const url = typeof request.url === 'string' ? new URL(request.url) : request.url; + const req = http.request({ + lookup: (hostname, options, callback) => { + if (options.family === 6) { + callback(null, '::1', 6); + } else { + callback(null, '127.0.0.1', 4); + } + }, + hostname: url.hostname, + port: port ?? url.port ?? 80, + path: `${url.pathname}${url.search}`, + method: request.method, + headers: request.headers, + }, resolve).once('error', reject); + req.end(); + }); +} + +function makeHttp2Request(request: Request, port?: number): Promise<{ headers: Record; body: Buffer; }> { + return new Promise<{ headers: Record; body: Buffer; }>((resolve, reject) => { + const url = typeof request.url === 'string' ? new URL(request.url) : request.url; + const client = http2.connect(request.url, { + lookup: (hostname, options, callback) => { + if (options.family === 6) { + callback(null, '::1', 6); + } else { + callback(null, '127.0.0.1', 4); + } + }, + // host: url.host, + port: port ?? parseInt(url.port, 10) ?? 80, + }); + const req: http2.ClientHttp2Stream = client.request({ + ...request.headers, + ':method': request.method, + ':path': `${url.pathname}${url.search}`, + }); + let headers: Record; + req.end(); + req.on('response', (h) => { + headers = h as Record; + }); + req.on('error', (e) => { + reject(e); + client.close(); + }); + const chunks: Buffer[] = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', () => { + client.close(); + resolve({ + headers, + body: Buffer.concat(chunks), + }); + }); + }); +} + +describe('httpbis', () => { + let keys: VerifyingKey[]; + before('load keys', async () => { + keys = await Promise.all([{ + file: 'test-key-rsa.pem', + alg: 'rsa-v1_5-sha256', + }, { + file: 'test-key-rsa-pss.pem', + alg: 'rsa-pss-sha512', + }, { + file: 'test-key-ecc-p256.pem', + alg: 'ecdsa-p256-sha256', + }, { + file: 'test-key-ed25519.pem', + alg: 'ed25519', + }, { + file: 'test-shared-secret.txt', + alg: 'hmac-sha256', + }].map(async ({file, alg}) => { + const key = await fs.readFile(`./test/etc/${file}`); + return { + id: parse(file).name, + algs: [alg], + verify: createVerifier(alg.startsWith('hmac-') ? Buffer.from(key.toString(), 'base64') : key, alg), + }; + })); + }); + describe('http', () => { + let server: TestServer; + before('create server', async () => { + server = createHttpServer({port: 8080}); + server.server.on('request', (req, res) => { + res.setHeader('Date', 'Tue, 20 Apr 2021 02:07:56 GMT'); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Digest', 'sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:'); + res.setHeader('Content-Length', '23'); + res.setHeader('Signature-Input', 'sig-b24=("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"'); + res.setHeader('Signature', 'sig-b24=:MEYCIQDXrmWrcxKWLQQm0zlwbFr5/KAlB9oHkfMpNRVCuGVHjQIhAKtljVKRuRoWv5dCKuc+GgP3eqLAq+Eg0d3olyR67BYK:'); + res.end('{"message": "good dog"}'); + }); + return server.start(); + }); + beforeEach('reset requests', () => server.clear()); + after('stop server', async () => { + return server.stop(); + }); + describe('rsa-pss-sha512', () => { + it('verifies minimal example', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({keyid}) => { + if (keyid) { + return keys.find(({id}) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + created: new Date(1618884473 * 1000), + }) + expect(valid).to.equal(true); + }); + it('rejects minimal example if we add required params', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({keyid}) => { + if (keyid) { + return keys.find(({id}) => id === keyid) ?? null; + } + return null; + }); + try { + await httpbis.verifyMessage({ + keyLookup, + requiredFields: ['content-digest'], + }, request); + } catch (e) { + expect(e).to.be.instanceOf(UnacceptableSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + created: new Date(1618884473 * 1000), + }); + return; + } + expect.fail('Expected to throw'); + }); + it('verifies selective components', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + 'Signature': 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + tag: 'header-example', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + it('verifies full coverage', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + }); + describe('ecdsa-p256-sha256', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it.skip('verifies a response', async () => { + const response = await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + status: response.statusCode as number, + headers: response.headers as Record, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-ecc-p256', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + }); + describe('hmac-sha256', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it('verifies a request', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b25=("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret"', + 'Signature': 'sig-b25=:pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8=:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-shared-secret', + created: new Date(1618884473 * 1000), + }); + expect(valid).to. equal(true); + }); + }); + describe('ed25519', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it('verifies a request', async () => { + await makeHttpRequest({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"', + 'Signature': 'sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-ed25519', + created: new Date(1618884473 * 1000), + }); + expect(valid).to. equal(true); + }); + }); + }); + describe('http2', () => { + let server: TestServer; + before('create server', async () => { + server = createHttp2Server({ port: 8080 }); + server.server.on('stream', (stream) => { + stream.respond({ + ':status': 200, + 'date': 'Tue, 20 Apr 2021 02:07:56 GMT', + 'content-type': 'application/json', + 'content-digest': 'sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:', + 'content-length': '23', + 'signature-input': 'sig-b24=("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"', + 'signature': 'sig-b24=:MEYCIQDXrmWrcxKWLQQm0zlwbFr5/KAlB9oHkfMpNRVCuGVHjQIhAKtljVKRuRoWv5dCKuc+GgP3eqLAq+Eg0d3olyR67BYK:', + }); + stream.end('{"message": "good dog"}'); + stream.close(); + }); + return server.start(); + }); + beforeEach('reset requests', () => server.clear()); + after('stop server', async () => { + return server.stop(); + }); + describe('rsa-pss-sha512', () => { + it('verifies minimal example', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://localhost:8080/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({keyid}) => { + if (keyid) { + return keys.find(({id}) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + created: new Date(1618884473 * 1000), + }) + expect(valid).to.equal(true); + }); + it('rejects minimal example if we add required params', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({keyid}) => { + if (keyid) { + return keys.find(({id}) => id === keyid) ?? null; + } + return null; + }); + try { + await httpbis.verifyMessage({ + keyLookup, + requiredFields: ['content-digest'], + }, request); + } catch (e) { + expect(e).to.be.instanceOf(UnacceptableSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + created: new Date(1618884473 * 1000), + }); + return; + } + expect.fail('Expected to throw'); + }); + it('verifies selective components', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + 'Signature': 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + tag: 'header-example', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + it('verifies full coverage', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-rsa-pss', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + }); + describe('ecdsa-p256-sha256', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it.skip('verifies a response', async () => { + const response = await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + status: response.headers[':status'] as unknown as number, + headers: response.headers as Record, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-ecc-p256', + created: new Date(1618884473 * 1000), + }); + expect(valid).to.equal(true); + }); + }); + describe('hmac-sha256', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it('verifies a request', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b25=("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret"', + 'Signature': 'sig-b25=:pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8=:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-shared-secret', + created: new Date(1618884473 * 1000), + }); + expect(valid).to. equal(true); + }); + }); + describe('ed25519', () => { + // There seems to be a problem in node in verifying ecdsa signatures from external sources + it('verifies a request', async () => { + await makeHttp2Request({ + method: 'POST', + url: 'http://example.com/foo?param=Value&Pet=dog', + headers: { + ':authority': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"', + 'Signature': 'sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:', + }, + }, 8080); + expect(server.requests).to.have.lengthOf(1); + const [request] = server.requests; + const keyLookup = stub().callsFake(async ({ keyid }) => { + if (keyid) { + return keys.find(({ id }) => id === keyid) ?? null; + } + return null; + }); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, request); + expect(keyLookup).to.have.callCount(1); + expect(keyLookup).to.have.been.calledOnceWithExactly({ + keyid: 'test-key-ed25519', + created: new Date(1618884473 * 1000), + }); + expect(valid).to. equal(true); + }); + }); + }); +}); diff --git a/test/httpbis/httpbis.spec.ts b/test/httpbis/httpbis.spec.ts index e5597b4..879aea5 100644 --- a/test/httpbis/httpbis.spec.ts +++ b/test/httpbis/httpbis.spec.ts @@ -1,5 +1,14 @@ import * as httpbis from '../../src/httpbis'; -import { Request, Response, SigningKey } from '../../src'; +import { + ExpiredError, + MalformedSignatureError, + Request, + Response, + SigningKey, + UnacceptableSignatureError, + UnknownKeyError, + UnsupportedAlgorithmError, +} from '../../src'; import { expect } from 'chai'; import { describe } from 'mocha'; import * as MockDate from 'mockdate'; @@ -61,10 +70,45 @@ describe('httpbis', () => { ...req, url: 'http://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: 'http://www.example.com:8080/path?param=value', + })).to.deep.equal(['www.example.com:8080']); expect(httpbis.deriveComponent('@authority', new Map(), { ...req, url: 'https://www.example.com:80/path?param=value', })).to.deep.equal(['www.example.com:80']); + // with URL objects + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + 'www.example.com', + ]); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('https://www.EXAMPLE.com/path?param=value'), + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('https://www.example.com:8080/path?param=value'), + })).to.deep.equal(['www.example.com:8080']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('https://www.example.com:443/path?param=value'), + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('http://www.example.com:80/path?param=value'), + })).to.deep.equal(['www.example.com']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('http://www.example.com:8080/path?param=value'), + })).to.deep.equal(['www.example.com:8080']); + expect(httpbis.deriveComponent('@authority', new Map(), { + ...req, + url: new URL('https://www.example.com:80/path?param=value'), + })).to.deep.equal(['www.example.com:80']); }); it('derives @scheme', () => { const req: Request = { @@ -79,6 +123,15 @@ describe('httpbis', () => { ...req, url: 'http://example.com', })).to.deep.equal(['http']); + // with URL objects + expect(httpbis.deriveComponent('@scheme', new Map(), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal(['https']); + expect(httpbis.deriveComponent('@scheme', new Map(), { + ...req, + url: new URL('http://example.com'), + })).to.deep.equal(['http']); }); it('derives @request-target', () => { const req: Request = { @@ -96,6 +149,13 @@ describe('httpbis', () => { expect(httpbis.deriveComponent('@request-target', new Map(), req)).to.deep.equal([ '/path?param=value', ]); + // with URL objects + expect(httpbis.deriveComponent('@request-target', new Map(), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + '/path?param=value', + ]); }); it('derives @path', () => { const req: Request = { @@ -108,6 +168,12 @@ describe('httpbis', () => { expect(httpbis.deriveComponent('@path', new Map(), req)).to.deep.equal([ '/path', ]); + expect(httpbis.deriveComponent('@path', new Map(), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + '/path', + ]); }); it('derives @query', () => { const req: Request = { @@ -132,6 +198,25 @@ describe('httpbis', () => { })).to.deep.equal([ '?', ]); + // with URL objects + expect(httpbis.deriveComponent('@query', new Map(), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + '?param=value&foo=bar&baz=batman', + ]); + expect(httpbis.deriveComponent('@query', new Map(), { + ...req, + url: new URL('https://www.example.com/path?queryString'), + })).to.deep.equal([ + '?queryString', + ]); + expect(httpbis.deriveComponent('@query', new Map(), { + ...req, + url: new URL('https://www.example.com/path'), + })).to.deep.equal([ + '?', + ]); }); it('derives @query-param', () => { const req: Request = { @@ -157,6 +242,32 @@ describe('httpbis', () => { 'value', 'value2', ]); + // with URL objects + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'baz']]), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + 'batman', + ]); + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'qux']]), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + '', + ]); + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'param']]), { + ...req, + url: new URL(req.url as string), + })).to.deep.equal([ + 'value', + ]); + expect(httpbis.deriveComponent('@query-param', new Map([['name', 'param']]), { + ...req, + url: new URL('https://example.com/path?param=value¶m=value2'), + })).to.deep.equal([ + 'value', + 'value2', + ]); }); it('derives @status', () => { const req: Request = { @@ -317,6 +428,89 @@ describe('httpbis', () => { 'value2', ]); }); + it('throws if no req supplied for req bound component', () => { + try { + httpbis.deriveComponent('@method', new Map([['req', false]]), {} as Request); + } catch (e) { + expect(e).to.have.property('message', 'Missing request in request-response bound component'); + return; + } + expect.fail('Expected to throw'); + }); + }); + describe('error conditions', () => { + const response: Response = { + status: 200, + headers: {}, + }; + [ + '@method', + '@target-uri', + '@authority', + '@scheme', + '@request-target', + '@path', + '@query', + '@unknown', + ].forEach((component) => { + it(`throws for ${component} on response`, () => { + try { + httpbis.deriveComponent(component, new Map(), response); + } catch (e) { + expect(e).to.be.instanceOf(Error); + return; + } + expect.fail('Expected to throw'); + }); + }); + it('throws for @query-param on response', () => { + try { + httpbis.deriveComponent('@query-param', new Map(), response); + } catch (e) { + expect(e).to.be.instanceOf(Error); + return; + } + expect.fail('Expected to throw'); + }); + it('throws for missing @query-param name', () => { + try { + httpbis.deriveComponent('@query-param', new Map(), { + method: 'POST', + url: 'http://example.com/?name=test', + headers: {}, + }); + } catch (e) { + expect(e).to.be.instanceOf(Error); + return; + } + expect.fail('Expected to throw'); + }); + it('throws for missing @query-param', () => { + try { + httpbis.deriveComponent('@query-param', new Map([['name', 'missing']]), { + method: 'POST', + url: 'http://example.com/?name=test', + headers: {}, + }); + } catch (e) { + expect(e).to.be.instanceOf(Error); + return; + } + expect.fail('Expected to throw'); + }); + it('throws for @status on request', () => { + try { + httpbis.deriveComponent('@status', new Map(), { + method: 'POST', + url: 'http://example.com/?name=test', + headers: {}, + }); + } catch (e) { + expect(e).to.be.instanceOf(Error); + return; + } + expect.fail('Expected to throw'); + }); }); }); describe('.extractHeader', () => { @@ -449,6 +643,70 @@ describe('httpbis', () => { ]); }); }); + describe('error cases', () => { + const response: Response = { + status: 200, + headers: { + Date: 'Tue, 20 Apr 2021 02:07:56 GMT', + structured: 'test=123', + notadict: '(a b c)', + }, + }; + it('throws if no request context', () => { + try { + httpbis.extractHeader('structured', new Map([['req', false]]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + it('throws if both bs/sf params provided', () => { + try { + httpbis.extractHeader('structured', new Map([['sf', false], ['bs', false]]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + it('throws if both bs and implicit sf params provided', () => { + try { + httpbis.extractHeader('structured', new Map([['bs', false], ['key', 'val']]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + it('throws if sf params provided for non structured field', () => { + try { + httpbis.extractHeader('date', new Map([['sf', false], ['key', 'val']]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + it('throws if sf params provided for non dictionary', () => { + try { + httpbis.extractHeader('notadict', new Map([['sf', false], ['key', 'val']]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + it('throws if key is missing for structured field', () => { + try { + httpbis.extractHeader('structured', new Map([['key', 'val']]), response); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + return; + } + expect.fail('Expected to fail'); + }); + }); }); describe('.createSignatureBase', () => { describe('header fields', () => { @@ -537,6 +795,42 @@ describe('httpbis', () => { ['"example-header";bs', [':dmFsdWUsIHdpdGgsIGxvdHMsIG9mLCBjb21tYXM=:']], ]); }); + it('ignores @signature-params component', () => { + expect(httpbis.createSignatureBase({ fields: [ + 'host', + 'date', + 'x-ows-header', + 'x-obs-fold-header', + 'cache-control', + 'example-dict', + '@signature-params', + ] }, request)).to.deep.equal([ + ['"host"', ['www.example.com']], + ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['"x-ows-header"', ['Leading and trailing whitespace.']], + ['"x-obs-fold-header"', ['Obsolete line folding.']], + ['"cache-control"', ['max-age=60, must-revalidate']], + ['"example-dict"', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ]); + }); + it('ignores @signature-params component with arbitrary params', () => { + expect(httpbis.createSignatureBase({ fields: [ + 'host', + 'date', + 'x-ows-header', + 'x-obs-fold-header', + 'cache-control', + 'example-dict', + '@signature-params;test=:AAA=:;test2=test', + ] }, request)).to.deep.equal([ + ['"host"', ['www.example.com']], + ['"date"', ['Tue, 20 Apr 2021 02:07:56 GMT']], + ['"x-ows-header"', ['Leading and trailing whitespace.']], + ['"x-obs-fold-header"', ['Obsolete line folding.']], + ['"cache-control"', ['max-age=60, must-revalidate']], + ['"example-dict"', ['a=1, b=2;x=1;y=2, c=(a b c)']], + ]); + }); }); describe('derived components', () => { const request: Request = { @@ -794,6 +1088,21 @@ describe('httpbis', () => { ['alg', 'rsa123'], ]); }); + it('calculates expires if created passed', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + sign: () => Promise.resolve(Buffer.from('')), + alg: 'rsa123', + }, + paramValues: { created: new Date() }, + }, ).entries())).to.deep.equal([ + ['keyid', '123'], + ['alg', 'rsa123'], + ['created', 1664267652], + ['expires', 1664267952], + ]); + }); it('uses a custom expires if passed', () => { expect(Array.from(httpbis.createSigningParameters({ key: { @@ -901,6 +1210,36 @@ describe('httpbis', () => { ['custom', 'value'], ]); }); + it('returns arbitrary date param as number', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg', 'custom'], + paramValues: { custom: new Date(Date.now() + 1000) }, + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ['custom', 1664267653], + ]); + }); + it('ignores arbitrary param with no value', () => { + expect(Array.from(httpbis.createSigningParameters({ + key: { + id: '123', + alg: 'rsa', + sign: () => Promise.resolve(Buffer.from('')), + }, + params: ['created', 'keyid', 'alg', 'custom'], + }).entries())).to.deep.equal([ + ['created', 1664267652], + ['keyid', '123'], + ['alg', 'rsa'], + ]); + }); }); }); describe('.augmentHeaders', () => { @@ -921,11 +1260,11 @@ describe('httpbis', () => { }); it('avoids naming clashes with existing signatures', () => { expect(httpbis.augmentHeaders({ - 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', - 'signature-input': 'sig=("@method";req);created=12345', + 'signature': ['sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', 'sig0=:YSBmYWtlIHNpZ25hdHVyZQ==:'], + 'signature-input': ['sig=("@method";req);created=12345', 'sig0=("@method";req);created=12345'], }, Buffer.from('another fake signature'), '("@request-target";req);created=12345')).to.deep.equal({ - 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:, sig0=:YW5vdGhlciBmYWtlIHNpZ25hdHVyZQ==:', - 'signature-input': 'sig=("@method";req);created=12345, sig0=("@request-target";req);created=12345', + 'signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:, sig0=:YSBmYWtlIHNpZ25hdHVyZQ==:, sig1=:YW5vdGhlciBmYWtlIHNpZ25hdHVyZQ==:', + 'signature-input': 'sig=("@method";req);created=12345, sig0=("@method";req);created=12345, sig1=("@request-target";req);created=12345', }); }); it('uses a provided signature name', () => { @@ -996,6 +1335,31 @@ describe('httpbis', () => { '"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"' )); }); + it('signs a request with no fields', async () => { + const signed = await httpbis.signMessage({ + key: signer, + params: [ + 'created', + 'keyid', + ], + paramValues: { + keyid: 'test-key-rsa-pss', + created: new Date(1618884473 * 1000), + }, + }, request); + expect(signed.headers).to.deep.equal({ + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature': 'sig=:YSBmYWtlIHNpZ25hdHVyZQ==:', + 'Signature-Input': 'sig=();created=1618884473;keyid="test-key-rsa-pss"', + }); + expect(signer.sign).to.have.been.calledOnceWithExactly(Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss"' + )); + }); }); describe('responses', () => { const response: Response = { @@ -1135,6 +1499,103 @@ describe('httpbis', () => { }, ); }); + it('parses arbitrary params', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss";p1=:AAA=:;p2=p1', + }, + }); + expect(valid).to.equal(true); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledOnceWithExactly( + Buffer.from('"@method": POST\n' + + '"@authority": example.com\n' + + '"@path": /foo\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"content-length": 18\n' + + '"content-type": application/json\n' + + '"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss";p1=:AAA=:;p2=p1', + ), + Buffer.from('HIbjHC5rS0BYaa9v4QfD4193TORw7u9edguPh0AW3dMq9WImrlFrCGUDih47vAxi4L2YRZ3XMJc1uOKk/J0ZmZ+wcta4nKIgBkKq0rM9hs3CQyxXGxHLMCy8uqK488o+9jrptQ+xFPHK7a9sRL1IXNaagCNN3ZxJsYapFj+JXbmaI5rtAdSfSvzPuBCh+ARHBmWuNo1UzVVdHXrl8ePL4cccqlazIJdC4QEjrF+Sn4IxBQzTZsL9y9TP5FsZYzHvDqbInkTNigBcE9cKOYNFCn4D/WM7F6TNuZO9EgtzepLWcjTymlHzK7aXq6Am6sfOrpIC49yXjj3ae6HRalVc/g==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + p1: 'AAA=', + p2: 'p1', + }, + ); + }); + it('verifies a request with multiple signatures', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");alg="rsa-pss-sha512";created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + ], + }, + }); + expect(valid).to.equal(true); + expect(keyLookup).to.have.callCount(2); + expect(verifierStub).to.have.callCount(2); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + ), + Buffer.from('d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3 +7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + }, + ); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from('"@authority": example.com\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"@query-param";name="Pet": dog\n' + + '"@signature-params": ("@authority" "content-digest" "@query-param";name="Pet");alg="rsa-pss-sha512";created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + ), + Buffer.from('LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==', 'base64'), + { + alg: 'rsa-pss-sha512', + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + tag: 'header-example', + }, + ); + }); + it('returns null for requests without signatures', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const headers = { ... request.headers }; + delete headers['Signature']; + delete headers['Signature-Input']; + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers, + }); + expect(valid).to.equal(null); + expect(keyLookup).to.have.callCount(0); + expect(verifierStub).to.have.callCount(0); + }); }); describe('responses', () => { const response: Response = { @@ -1222,5 +1683,510 @@ describe('httpbis', () => { ); }); }); + describe('error conditions', () => { + const request: Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:HIbjHC5rS0BYaa9v4QfD4193TORw7u9edguPh0AW3dMq9WImrlFrCGUDih47vAxi4L2YRZ3XMJc1uOKk/J0ZmZ+wcta4nKIgBkKq0rM9hs3CQyxXGxHLMCy8uqK488o+9jrptQ+xFPHK7a9sRL1IXNaagCNN3ZxJsYapFj+JXbmaI5rtAdSfSvzPuBCh+ARHBmWuNo1UzVVdHXrl8ePL4cccqlazIJdC4QEjrF+Sn4IxBQzTZsL9y9TP5FsZYzHvDqbInkTNigBcE9cKOYNFCn4D/WM7F6TNuZO9EgtzepLWcjTymlHzK7aXq6Am6sfOrpIC49yXjj3ae6HRalVc/g==:', + }, + }; + it('throws if there are missing inputs', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const headers = { ... request.headers }; + delete headers['Signature-Input']; + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers, + }); + } catch (e) { + expect(keyLookup).to.have.callCount(0); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if there are missing signatures', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const headers = { ... request.headers }; + delete headers['Signature']; + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers, + }); + } catch (e) { + expect(keyLookup).to.have.callCount(0); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if it cannot validate all signatures when required', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + all: true, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="unknwon-key";tag="header-example"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(UnknownKeyError); + expect(keyLookup).to.have.callCount(3); + expect(verifierStub).to.have.callCount(2); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + ), + Buffer.from('d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3 +7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + }, + ); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from('"@authority": example.com\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"@query-param";name="Pet": dog\n' + + '"@signature-params": ("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + ), + Buffer.from('LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + tag: 'header-example', + }, + ); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if it cannot validate signatures when required', async () => { + const syntheticError = new Error('failed to verify'); + const verifierStub = stub().rejects(syntheticError); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + ], + }, + }); + } catch (e) { + expect(e).to.equal(syntheticError); + expect(keyLookup).to.have.callCount(2); + expect(verifierStub).to.have.callCount(2); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + ), + Buffer.from('d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3 +7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + }, + ); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from('"@authority": example.com\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"@query-param";name="Pet": dog\n' + + '"@signature-params": ("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', + ), + Buffer.from('LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + tag: 'header-example', + }, + ); + return; + } + expect.fail('Expected to throw'); + }); + it('shortcuts validation if not all signatures can be validated', async () => { + const syntheticError = new Error('failed to verify'); + const verifierStub = stub().rejects(syntheticError); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="unknown";tag="header-example"', + ], + }, + }); + } catch (e) { + expect(e).to.equal(syntheticError); + expect(keyLookup).to.have.callCount(2); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + ), + Buffer.from('d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3 +7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + }, + ); + return; + } + expect.fail('Expected to throw'); + }); + it('ignores keys it does not know', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const valid = await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="unknown";tag="header-example"', + 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="unknown";tag="header-example"', + ], + }, + }); + expect(valid).to.equal(true); + expect(keyLookup).to.have.callCount(3); + expect(verifierStub).to.have.callCount(1); + expect(verifierStub).to.have.been.calledWithExactly( + Buffer.from( + '"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + ), + Buffer.from('d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3 +7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==', 'base64'), + { + created: new Date(1618884473 * 1000), + keyid: 'test-key-rsa-pss', + nonce: 'b3k2pp5k7z-50gnwp.yemd', + }, + ); + }); + it('throws if key does not support alg', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub, algs: ['hmac-sha256'] } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', + 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;alg="rsa-pss-sha512";keyid="unknown";tag="header-example"', + 'sig-b21=();created=1618884473;alg="rsa-pss-sha512";keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', + 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");alg="rsa-pss-sha512";created=1618884473;keyid="unknown";tag="header-example"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(UnsupportedAlgorithmError); + expect(keyLookup).to.have.callCount(3); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws for malformed signature input', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub, algs: ['hmac-sha256'] } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=123;keyid="test-key-rsa-pss"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(MalformedSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signatures do not have required params', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + requiredParams: ['tag'], + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;alg="rsa-pss-sha512";keyid="test-key-rsa-pss"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(UnacceptableSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signatures do not have required signed fields', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + requiredFields: ['@method'], + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;alg="rsa-pss-sha512";keyid="test-key-rsa-pss"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(UnacceptableSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signatures is missing', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig1=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;alg="rsa-pss-sha512";keyid="test-key-rsa-pss"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(MalformedSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signatures is malformed', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature': [ + 'sig="LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw=="', + ], + 'Signature-Input': [ + 'sig=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;alg="rsa-pss-sha512";keyid="test-key-rsa-pss"', + ], + }, + }); + } catch (e) { + expect(e).to.be.instanceOf(MalformedSignatureError); + expect(keyLookup).to.have.callCount(1); + expect(verifierStub).to.have.callCount(0); + return; + } + expect.fail('Expected to throw'); + }); + }); + describe('config tests', () => { + const request: Request = { + method: 'post', + url: 'https://example.com/foo?param=Value&Pet=dog', + headers: { + 'Host': 'example.com', + 'Date': 'Tue, 20 Apr 2021 02:07:55 GMT', + 'Content-Type': 'application/json', + 'Content-Digest': 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + 'Content-Length': '18', + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;expires=1618884773;keyid="test-key-rsa-pss"', + 'Signature': 'sig1=:HIbjHC5rS0BYaa9v4QfD4193TORw7u9edguPh0AW3dMq9WImrlFrCGUDih47vAxi4L2YRZ3XMJc1uOKk/J0ZmZ+wcta4nKIgBkKq0rM9hs3CQyxXGxHLMCy8uqK488o+9jrptQ+xFPHK7a9sRL1IXNaagCNN3ZxJsYapFj+JXbmaI5rtAdSfSvzPuBCh+ARHBmWuNo1UzVVdHXrl8ePL4cccqlazIJdC4QEjrF+Sn4IxBQzTZsL9y9TP5FsZYzHvDqbInkTNigBcE9cKOYNFCn4D/WM7F6TNuZO9EgtzepLWcjTymlHzK7aXq6Am6sfOrpIC49yXjj3ae6HRalVc/g==:', + }, + }; + before('mock time', () => { + // expires time plus 5 seconds + MockDate.set(new Date((1618884773 + 5) * 1000)); + }); + after('reset time', () => MockDate.reset()); + it('allows expired signatures within tolerance', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const result = await httpbis.verifyMessage({ + keyLookup, + tolerance: 5, + }, request); + expect(result).to.equal(true); + try { + await httpbis.verifyMessage({ + keyLookup, + }, request); + } catch (e) { + expect(e).to.be.instanceOf(ExpiredError); + return; + } + expect.fail('Expected to throw'); + }); + it('enforces maxAge of signature', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + try { + await httpbis.verifyMessage({ + keyLookup, + maxAge: 150, + }, request); + } catch (e) { + expect(e).to.be.instanceOf(ExpiredError); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signature is created too early', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const result = await httpbis.verifyMessage({ + keyLookup, + notAfter: new Date(1618884472 * 1000), + tolerance: 5, + }, request); + expect(result).to.equal(true); + try { + await httpbis.verifyMessage({ + keyLookup, + notAfter: new Date(1618884472 * 1000), + }, request); + } catch (e) { + expect(e).to.be.instanceOf(ExpiredError); + return; + } + expect.fail('Expected to throw'); + }); + it('throws if signature is created too early', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const result = await httpbis.verifyMessage({ + keyLookup, + notAfter: 1618884472, + tolerance: 5, + }, request); + expect(result).to.equal(true); + try { + await httpbis.verifyMessage({ + keyLookup, + notAfter: 1618884472 * 1000, + }, request); + } catch (e) { + expect(e).to.be.instanceOf(ExpiredError); + return; + } + expect.fail('Expected to throw'); + }); + it('validates signatures with no created param', async () => { + const verifierStub = stub().resolves(true); + const keyLookup = stub().callsFake(async ({ keyid }) => keyid === 'test-key-rsa-pss' ? { verify: verifierStub } : null); + const result = await httpbis.verifyMessage({ + keyLookup, + }, { + ...request, + headers: { + ...request.headers, + 'Signature-Input': 'sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");keyid="test-key-rsa-pss"', + }, + }); + expect(result).to.equal(true); + }); + }); }); }); diff --git a/test/structured-headers.spec.ts b/test/structured-headers.spec.ts new file mode 100644 index 0000000..b297588 --- /dev/null +++ b/test/structured-headers.spec.ts @@ -0,0 +1,66 @@ +import { Dictionary, Item, List } from '../src/structured-header'; +import { expect } from 'chai'; + +describe('structured-headers', () => { + describe('Dictionary', () => { + it('parses a dictionary', () => { + const dict = new Dictionary('a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid'); + expect(dict).to.be.instanceOf(Dictionary); + expect(dict.has('a')).to.equal(true); + expect(dict.has('b')).to.equal(true); + expect(dict.has('c')).to.equal(true); + expect(dict.has('d')).to.equal(true); + expect(dict.get('a')).to.equal('(1 2)'); + expect(dict.get('b')).to.equal('3'); + expect(dict.get('c')).to.equal('4;aa=bb'); + expect(dict.get('d')).to.equal('(5 6);valid'); + expect(dict.get('e')).to.equal(undefined); + }); + }); + describe('List', () => { + it('parses a list', () => { + const list = new List('sugar, tea, rum'); + expect(list).to.be.instanceOf(List); + expect(list.toString()).to.equal('sugar, tea, rum'); + expect(list.serialize()).to.equal('sugar, tea, rum'); + }); + }); + describe('Item', () => { + it('parses an integer', () => { + const item = new Item('42'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal('42'); + expect(item.serialize()).to.equal('42'); + }); + it('parses a decimal', () => { + const item = new Item('42.1'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal('42.1'); + expect(item.serialize()).to.equal('42.1'); + }); + it('parses a string', () => { + const item = new Item('"a string"'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal('"a string"'); + expect(item.serialize()).to.equal('"a string"'); + }); + it('parses a token', () => { + const item = new Item('token'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal('token'); + expect(item.serialize()).to.equal('token'); + }); + it('parses a byte sequence', () => { + const item = new Item(':AAA=:'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal(':AAA=:'); + expect(item.serialize()).to.equal(':AAA=:'); + }); + it('parses a boolean', () => { + const item = new Item('?1'); + expect(item).to.be.instanceOf(Item); + expect(item.toString()).to.equal('?1'); + expect(item.serialize()).to.equal('?1'); + }); + }); +}); From 55facd527b7425793037a2d26ebffa0960be25a9 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 11 Oct 2022 15:52:51 +0100 Subject: [PATCH 14/17] Set an agressive timeout for tests --- .github/workflows/nodejs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8410532..28ac7e9 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -17,6 +17,7 @@ jobs: lint: name: Linting check runs-on: ubuntu-latest + timeout-minutes: 1 steps: - uses: actions/checkout@v3 with: @@ -45,6 +46,7 @@ jobs: tests: name: Unit tests runs-on: ubuntu-latest + timeout-minutes: 1 strategy: matrix: node-version: [12.x, 14.x, 16.x, 18.x] From 1b2411829beed6a18f1544d4b16a4d69c97c79af Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 11 Oct 2022 15:59:57 +0100 Subject: [PATCH 15/17] Fix node v18 failures --- test/httpbis/httpbis.int.ts | 52 +++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/test/httpbis/httpbis.int.ts b/test/httpbis/httpbis.int.ts index acd86af..2dd558b 100644 --- a/test/httpbis/httpbis.int.ts +++ b/test/httpbis/httpbis.int.ts @@ -1,6 +1,6 @@ import * as http from 'http'; import * as http2 from 'http2'; -import { Server } from 'net'; +import { Server, Socket } from 'net'; import { expect } from 'chai'; import { createVerifier, @@ -51,7 +51,11 @@ function createHttpServer(config: ServerConfig): TestServer { function createHttp2Server(config: ServerConfig): TestServer { const requests: Request[] = []; + const connections: Socket[] = []; const server = http2.createServer(); + server.on('connection', (conn) => { + connections.push(conn); + }); server.on('stream', (stream, headers) => { const domain = headers[':authority'] ?? 'localhost'; const request: Request = { @@ -67,9 +71,28 @@ function createHttp2Server(config: ServerConfig): TestServer { server.once('listening', () => resolve()); server.listen(config.port); }), - stop: () => new Promise((resolve) => server.close(() => resolve())), + stop: () => new Promise((resolve) => { + Promise.all(connections.map((conn) => new Promise((done) => { + if (conn.destroyed) { + done(); + } else { + conn.destroy(); + conn.on('close', done); + } + }))).then(() => server.close(() => resolve())); + }), requests, - clear: () => requests.splice(0), + clear: () => new Promise((resolve) => { + requests.splice(0); + Promise.all(connections.map((conn) => new Promise((closed) => { + if (conn.destroyed) { + closed(); + } else { + conn.destroy(); + conn.on('close', closed); + } + }))).then(() => resolve()); + }), }; } @@ -94,7 +117,7 @@ function makeHttpRequest(request: Request, port?: number): Promise; body: Buffer; }> { +function makeHttp2Request(request: Request & { body?: string; }, port?: number): Promise<{ headers: Record; body: Buffer; }> { return new Promise<{ headers: Record; body: Buffer; }>((resolve, reject) => { const url = typeof request.url === 'string' ? new URL(request.url) : request.url; const client = http2.connect(request.url, { @@ -114,22 +137,20 @@ function makeHttp2Request(request: Request, port?: number): Promise<{ headers: R ':path': `${url.pathname}${url.search}`, }); let headers: Record; - req.end(); + req.end(request.body); req.on('response', (h) => { headers = h as Record; }); req.on('error', (e) => { - reject(e); - client.close(); + client.close(() => reject(e)); }); const chunks: Buffer[] = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { - client.close(); - resolve({ + client.close(() => resolve({ headers, body: Buffer.concat(chunks), - }); + })); }); }); } @@ -443,7 +464,9 @@ describe('httpbis', () => { 'signature': 'sig-b24=:MEYCIQDXrmWrcxKWLQQm0zlwbFr5/KAlB9oHkfMpNRVCuGVHjQIhAKtljVKRuRoWv5dCKuc+GgP3eqLAq+Eg0d3olyR67BYK:', }); stream.end('{"message": "good dog"}'); - stream.close(); + stream.close(undefined, () => { + console.log('closed'); + }); }); return server.start(); }); @@ -465,6 +488,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -498,6 +522,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"', 'Signature': 'sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -537,6 +562,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"', 'Signature': 'sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -570,6 +596,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -605,6 +632,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"', 'Signature': 'sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -643,6 +671,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b25=("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret"', 'Signature': 'sig-b25=:pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8=:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; @@ -678,6 +707,7 @@ describe('httpbis', () => { 'Signature-Input': 'sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"', 'Signature': 'sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:', }, + body: '{"hello": "world"}', }, 8080); expect(server.requests).to.have.lengthOf(1); const [request] = server.requests; From 9bf65001719f303ed04478661783446ba2ef4a8c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 2 Aug 2023 18:07:06 +0200 Subject: [PATCH 16/17] cleanup test --- test/httpbis/httpbis.int.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/httpbis/httpbis.int.ts b/test/httpbis/httpbis.int.ts index 2dd558b..5437bcc 100644 --- a/test/httpbis/httpbis.int.ts +++ b/test/httpbis/httpbis.int.ts @@ -464,9 +464,7 @@ describe('httpbis', () => { 'signature': 'sig-b24=:MEYCIQDXrmWrcxKWLQQm0zlwbFr5/KAlB9oHkfMpNRVCuGVHjQIhAKtljVKRuRoWv5dCKuc+GgP3eqLAq+Eg0d3olyR67BYK:', }); stream.end('{"message": "good dog"}'); - stream.close(undefined, () => { - console.log('closed'); - }); + stream.close(); }); return server.start(); }); From b8ff6e92aac7d4de4210c7c4a54be7978c1d9476 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 2 Aug 2023 18:08:48 +0200 Subject: [PATCH 17/17] drop node 12 from test matrix --- .github/workflows/nodejs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 28ac7e9..78ba74c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -25,7 +25,7 @@ jobs: - name: Code linting uses: actions/setup-node@v2 with: - node-version: 12.x + node-version: 14.x cache: 'npm' - run: npm ci - run: npm run lint @@ -45,11 +45,13 @@ jobs: # uses: romeovs/lcov-reporter-action@v0.2.11 tests: name: Unit tests + needs: + - lint runs-on: ubuntu-latest timeout-minutes: 1 strategy: matrix: - node-version: [12.x, 14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v3