diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..446815de --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy this file to `.env` and fill in the connection info for a real Roku device. +# The device test suite (`npm run test:device`) reads these to know which device to target. +# `.env` is gitignored so your device credentials never get committed. + +# IP address or hostname of the Roku device to run the device tests against +ROKU_HOST=192.168.1.31 + +# Developer password for that device (set when you enabled developer mode) +ROKU_PASSWORD=aaaa diff --git a/.eslintrc.js b/.eslintrc.js index bd06f0fa..9f269480 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,6 +51,7 @@ module.exports = { 'error', 'always' ], + '@typescript-eslint/parameter-properties': 'off', '@typescript-eslint/prefer-readonly': 'off', '@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/promise-function-async': 'off', @@ -111,6 +112,7 @@ module.exports = { 'no-constant-condition': 'off', 'no-console': 'off', 'no-continue': 'off', + 'no-duplicate-imports': 'off', 'no-else-return': 'off', 'no-empty': 'off', 'no-implicit-coercion': 'off', diff --git a/.github/workflows/on-device-tests.yml b/.github/workflows/on-device-tests.yml new file mode 100644 index 00000000..caf002cf --- /dev/null +++ b/.github/workflows/on-device-tests.yml @@ -0,0 +1,17 @@ +name: on-device-tests +on: + push: + branches: + - master + pull_request: + +jobs: + on-device-tests: + runs-on: [self-hosted, roku-latest, enabled] + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: "18.20.4" + - run: npm ci + - run: npm run test:device diff --git a/.gitignore b/.gitignore index 8c0cf879..f1041036 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ out .roku-deploy-staging coverage .nyc_output -*.zip \ No newline at end of file +*.zip +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 047dd795..33a8d7bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "**/bower_components": true, "**/*.code-search": true, "**/dist": true - } -} \ No newline at end of file + }, + "js/ts.tsdk.path": "node_modules/typescript/lib" +} diff --git a/package-lock.json b/package-lock.json index 6144d464..0dc4afd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "3.17.7", "license": "MIT", "dependencies": { - "@types/request": "^2.48.13", "chalk": "^2.4.2", "fast-glob": "^3.2.12", "fs-extra": "^7.0.1", @@ -17,8 +16,8 @@ "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", "micromatch": "^4.0.4", + "needle": "^3.5.0", "picomatch": "^2.3.2", - "postman-request": "^2.88.1-postman.48", "semver": "^7.7.3", "xml2js": "^0.5.0" }, @@ -31,6 +30,7 @@ "@types/is-glob": "^4.0.2", "@types/micromatch": "^4.0.2", "@types/mocha": "^10.0.0", + "@types/needle": "^3.3.0", "@types/node": "^18.19.0", "@types/picomatch": "^2.3.4", "@types/semver": "^7.7.1", @@ -40,6 +40,7 @@ "@typescript-eslint/parser": "5.1.0", "audit-ci": "^7.1.0", "chai": "^4.3.4", + "dotenv": "^17.4.2", "eslint": "8.0.1", "mocha": "^10.8.2", "nyc": "^15.1.0", @@ -622,53 +623,6 @@ "node": ">= 8" } }, - "node_modules/@postman/form-data": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", - "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@postman/tough-cookie": { - "version": "4.1.3-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", - "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@postman/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@postman/tunnel-agent": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.8.tgz", - "integrity": "sha512-2U42SmZW5G+suEcS++zB94sBWNO4qD4bvETGFRFDTqSpYl5ksfjcPqzYpgQgXgUmb6dfz+fAGbkcRamounGm0w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -734,11 +688,6 @@ "integrity": "sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==", "dev": true }, - "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "node_modules/@types/chai": { "version": "4.2.22", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", @@ -782,10 +731,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/needle": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/needle/-/needle-3.3.0.tgz", + "integrity": "sha512-UFIuc1gdyzAqeVUYpSL+cliw2MmU/ZUhVZKE7Zo4wPbgc8hbljeKSnn6ls6iG8r5jpegPXLUIhJ+Wb2kLVs8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "18.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.0.tgz", "integrity": "sha512-667KNhaD7U29mT5wf+TZUnrzPrlL2GNQ5N0BMjO2oNULhBxX0/FKCkm6JMu0Jh7Z+1LwUlR21ekd7KhIboNFNw==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -798,18 +758,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", - "license": "MIT", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" - } - }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -826,11 +774,6 @@ "@sinonjs/fake-timers": "^7.1.0" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" - }, "node_modules/@types/xml2js": { "version": "0.4.9", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", @@ -1027,15 +970,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1150,24 +1084,6 @@ "node": ">=8" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1177,11 +1093,6 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, "node_modules/audit-ci": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/audit-ci/-/audit-ci-7.1.0.tgz", @@ -1270,34 +1181,12 @@ "node": ">=12" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, "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/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1307,11 +1196,6 @@ "node": ">=8" } }, - "node_modules/bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==" - }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -1383,35 +1267,6 @@ "node": ">=8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1451,11 +1306,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "node_modules/chai": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", @@ -1567,17 +1417,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1624,22 +1463,11 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1692,14 +1520,6 @@ "node": ">=8" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/diff": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", @@ -1734,18 +1554,17 @@ "node": ">=6.0.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/duplexer": { @@ -1755,16 +1574,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.3.877", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.877.tgz", @@ -1789,51 +1598,6 @@ "node": ">=8.6" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -2196,20 +1960,6 @@ "through": "^2.3.8" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2375,50 +2125,6 @@ "node": ">=8.0.0" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", - "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data/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==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -2479,15 +2185,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -2521,30 +2218,6 @@ "node": "*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2554,28 +2227,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -2644,18 +2295,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", @@ -2669,33 +2308,6 @@ "node": ">=4" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -2721,17 +2333,6 @@ "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2747,18 +2348,16 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-signature": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, "node_modules/ignore": { @@ -2825,15 +2424,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2897,7 +2487,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -2931,11 +2522,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -3112,12 +2698,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "license": "MIT" - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -3130,12 +2710,6 @@ "node": ">=4" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3148,11 +2722,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3205,21 +2774,6 @@ "node": ">=10" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "node_modules/jszip": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.8.0.tgz", @@ -3414,15 +2968,6 @@ "dev": true, "license": "MIT" }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3443,25 +2988,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3612,6 +3138,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -3620,6 +3147,22 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/nise": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", @@ -3877,26 +3420,6 @@ "node": ">=6" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4161,45 +3684,6 @@ "node": ">=8" } }, - "node_modules/postman-request": { - "version": "2.88.1-postman.48", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.48.tgz", - "integrity": "sha512-E32FGh8ig2KDvzo4Byi7Ibr+wK2gNKPSqXoNsvjdCHgDBxSK4sCUwv+aa3zOBUwfiibPImHMy0WdlDSSCTqTuw==", - "license": "Apache-2.0", - "dependencies": { - "@postman/form-data": "~3.1.1", - "@postman/tough-cookie": "~4.1.3-postman.1", - "@postman/tunnel-agent": "^0.6.8", - "aws-sign2": "~0.7.0", - "aws4": "^1.12.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "^2.1.35", - "oauth-sign": "~0.9.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "socks-proxy-agent": "^8.0.5", - "stream-length": "^1.0.2", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/postman-request/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==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4235,39 +3719,15 @@ "node": ">=0.4.0" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4362,11 +3822,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4463,93 +3918,21 @@ "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/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", + "dev": true, "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/signal-exit": { @@ -4607,44 +3990,6 @@ "node": ">=8" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4725,31 +4070,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stream-combiner": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", @@ -4761,14 +4081,6 @@ "through": "~2.3.4" } }, - "node_modules/stream-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", - "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", - "dependencies": { - "bluebird": "^2.6.2" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4969,12 +4281,6 @@ "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/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5041,6 +4347,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -5060,15 +4367,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5090,26 +4388,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5778,42 +5056,6 @@ "fastq": "^1.6.0" } }, - "@postman/form-data": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", - "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "@postman/tough-cookie": { - "version": "4.1.3-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", - "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "dependencies": { - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" - } - } - }, - "@postman/tunnel-agent": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.8.tgz", - "integrity": "sha512-2U42SmZW5G+suEcS++zB94sBWNO4qD4bvETGFRFDTqSpYl5ksfjcPqzYpgQgXgUmb6dfz+fAGbkcRamounGm0w==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -5879,11 +5121,6 @@ "integrity": "sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==", "dev": true }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "@types/chai": { "version": "4.2.22", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", @@ -5926,10 +5163,20 @@ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true }, + "@types/needle": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/needle/-/needle-3.3.0.tgz", + "integrity": "sha512-UFIuc1gdyzAqeVUYpSL+cliw2MmU/ZUhVZKE7Zo4wPbgc8hbljeKSnn6ls6iG8r5jpegPXLUIhJ+Wb2kLVs8cg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "18.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.0.tgz", "integrity": "sha512-667KNhaD7U29mT5wf+TZUnrzPrlL2GNQ5N0BMjO2oNULhBxX0/FKCkm6JMu0Jh7Z+1LwUlR21ekd7KhIboNFNw==", + "dev": true, "requires": { "undici-types": "~5.26.4" } @@ -5940,17 +5187,6 @@ "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", "dev": true }, - "@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" - } - }, "@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -5966,11 +5202,6 @@ "@sinonjs/fake-timers": "^7.1.0" } }, - "@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" - }, "@types/xml2js": { "version": "0.4.9", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", @@ -6082,11 +5313,6 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, - "agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -6175,30 +5401,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" - }, "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 }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, "audit-ci": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/audit-ci/-/audit-ci-7.1.0.tgz", @@ -6262,41 +5470,18 @@ } } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" - }, - "aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, "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 }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "requires": { - "tweetnacl": "^0.14.3" - } - }, "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 }, - "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==" - }, "brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -6352,24 +5537,6 @@ "write-file-atomic": "^3.0.0" } }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, - "call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6388,11 +5555,6 @@ "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "chai": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", @@ -6480,14 +5642,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -6531,18 +5685,11 @@ "which": "^2.0.1" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "requires": { - "assert-plus": "^1.0.0" - } - }, "debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "requires": { "ms": "^2.1.3" } @@ -6577,11 +5724,6 @@ "strip-bom": "^4.0.0" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, "diff": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", @@ -6606,15 +5748,11 @@ "esutils": "^2.0.2" } }, - "dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "requires": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - } + "dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true }, "duplexer": { "version": "0.1.2", @@ -6622,15 +5760,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "electron-to-chromium": { "version": "1.3.877", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.877.tgz", @@ -6652,35 +5781,6 @@ "ansi-colors": "^4.1.1" } }, - "es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "requires": { - "es-errors": "^1.3.0" - } - }, - "es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - } - }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -6949,16 +6049,6 @@ "through": "^2.3.8" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7088,31 +6178,6 @@ "signal-exit": "^3.0.2" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" - }, - "form-data": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", - "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "dependencies": { - "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==" - } - } - }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -7148,11 +6213,6 @@ "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -7177,46 +6237,12 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, - "get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - } - }, "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 }, - "get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "requires": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "requires": { - "assert-plus": "^1.0.0" - } - }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -7263,11 +6289,6 @@ "slash": "^3.0.0" } }, - "gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", @@ -7278,19 +6299,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, - "has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "requires": { - "has-symbols": "^1.0.3" - } - }, "hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -7309,14 +6317,6 @@ } } }, - "hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "requires": { - "function-bind": "^1.1.2" - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -7329,14 +6329,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "http-signature": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "ignore": { @@ -7387,11 +6385,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7434,7 +6427,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "is-unicode-supported": { "version": "0.1.0", @@ -7459,11 +6453,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -7604,22 +6593,12 @@ "esprima": "^4.0.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7632,11 +6611,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7672,17 +6646,6 @@ "through2": "^4.0.2" } }, - "jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "jszip": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.8.0.tgz", @@ -7835,11 +6798,6 @@ "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", "dev": true }, - "math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7854,19 +6812,6 @@ "picomatch": "^2.3.1" } }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, "minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7976,7 +6921,8 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "natural-compare": { "version": "1.4.0", @@ -7984,6 +6930,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "requires": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + } + }, "nise": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", @@ -8188,16 +7143,6 @@ } } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8395,40 +7340,6 @@ } } }, - "postman-request": { - "version": "2.88.1-postman.48", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.48.tgz", - "integrity": "sha512-E32FGh8ig2KDvzo4Byi7Ibr+wK2gNKPSqXoNsvjdCHgDBxSK4sCUwv+aa3zOBUwfiibPImHMy0WdlDSSCTqTuw==", - "requires": { - "@postman/form-data": "~3.1.1", - "@postman/tough-cookie": "~4.1.3-postman.1", - "@postman/tunnel-agent": "^0.6.8", - "aws-sign2": "~0.7.0", - "aws4": "^1.12.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "^2.1.35", - "oauth-sign": "~0.9.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "socks-proxy-agent": "^8.0.5", - "stream-length": "^1.0.2", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8455,28 +7366,11 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "requires": { - "side-channel": "^1.1.0" - } - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "queue-microtask": { "version": "1.2.3", @@ -8539,11 +7433,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -8615,50 +7504,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "requires": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - } - }, - "side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "requires": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - } - }, - "side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "requires": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - } - }, - "side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "requires": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - } - }, "signal-exit": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", @@ -8702,30 +7547,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "requires": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - } - }, - "socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "requires": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -8790,22 +7611,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, "stream-combiner": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", @@ -8816,14 +7621,6 @@ "through": "~2.3.4" } }, - "stream-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", - "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", - "requires": { - "bluebird": "^2.6.2" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8968,11 +7765,6 @@ "tslib": "^1.8.1" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9018,7 +7810,8 @@ "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "universalify": { "version": "0.1.2", @@ -9034,15 +7827,6 @@ "punycode": "^2.1.0" } }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9060,23 +7844,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - } - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 90c53481..f8c31fa1 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,12 @@ "audit": "npm audit || audit-ci --config ./audit-ci.jsonc", "test": "nyc mocha \"src/**/*.spec.ts\" --exclude \"src/device.spec.ts\"", "test:nocover": "mocha \"src/**/*.spec.ts\" --exclude \"src/device.spec.ts\"", - "test:device": "nyc mocha src/device.spec.ts", + "test:device": "mocha src/device.spec.ts", "test:all": "nyc mocha \"src/**/*.spec.ts\"", "test-without-sourcemaps": "npm run build && nyc mocha dist/**/*.spec.js", "package": "npm run build && npm pack" }, "dependencies": { - "@types/request": "^2.48.13", "chalk": "^2.4.2", "fast-glob": "^3.2.12", "fs-extra": "^7.0.1", @@ -27,8 +26,8 @@ "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", "micromatch": "^4.0.4", + "needle": "^3.5.0", "picomatch": "^2.3.2", - "postman-request": "^2.88.1-postman.48", "semver": "^7.7.3", "xml2js": "^0.5.0" }, @@ -38,6 +37,7 @@ "@types/is-glob": "^4.0.2", "@types/micromatch": "^4.0.2", "@types/mocha": "^10.0.0", + "@types/needle": "^3.3.0", "@types/node": "^18.19.0", "@types/picomatch": "^2.3.4", "@types/semver": "^7.7.1", @@ -47,6 +47,7 @@ "@typescript-eslint/parser": "5.1.0", "audit-ci": "^7.1.0", "chai": "^4.3.4", + "dotenv": "^17.4.2", "eslint": "8.0.1", "mocha": "^10.8.2", "nyc": "^15.1.0", diff --git a/src/Errors.ts b/src/Errors.ts index 846f9a91..27868912 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -1,5 +1,5 @@ import type { HttpResponse, RokuMessages } from './RokuDeploy'; -import type * as requestType from 'request'; +import type { RequestOptions } from './request'; export class InvalidDeviceResponseCodeError extends Error { constructor(message: string, public results?: any) { @@ -87,7 +87,7 @@ export class UpdateCheckRequiredError extends Error { constructor( public response: HttpResponse, - public requestOptions: requestType.OptionsWithUrl, + public requestOptions: RequestOptions, public cause?: Error ) { super(); @@ -107,7 +107,7 @@ export class ConnectionResetError extends Error { static MESSAGE = `The Roku device ended the connection unexpectedly and may need to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`; - constructor(error: Error, requestOptions: requestType.OptionsWithUrl) { + constructor(error: Error, requestOptions: RequestOptions) { super(); this.message = ConnectionResetError.MESSAGE; this.cause = error; diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 8bb5dfd1..b5832dea 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -16,9 +16,9 @@ import { defer, util, standardizePath as s } from './util'; import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; import { cwd, expectPathExists, expectPathNotExists, expectThrowsAsync, outDir, rootDir, stagingDir, tempDir, writeFiles } from './testUtils.spec'; import { createSandbox } from 'sinon'; -import * as r from 'postman-request'; -import type * as requestType from 'request'; -const request = r as typeof requestType; +import * as needle from 'needle'; +import { request } from './request'; +import { PassThrough } from 'stream'; const sinon = createSandbox(); @@ -196,6 +196,189 @@ describe('RokuDeploy', () => { }); }); + describe('error results structure (postman-request compatibility)', () => { + //These tests drive RokuDeploy through the REAL needle shim (by stubbing the low-level + //`needle`), then assert the exact shape of the `results`/`response` object attached to + //each thrown error. roku-deploy historically exposed a `request`/`postman-request`-shaped + //object here, and consumers read specific paths off it (`results.response.statusCode`, + //`results.response.headers.server`, `results.response.request.host`, string `results.body`). + //Getting any of these wrong is a BREAKING API change, so they're pinned down explicitly. + + /** Stub needle.post to deliver the given needle-style response + body */ + function stubNeedlePost(response: any, body: any, error: any = null) { + return sinon.stub(needle, 'post').callsFake(((url: string, data: any, opts: any, callback: any) => { + process.nextTick(callback, error, response, body); + return {} as any; + }) as any); + } + + /** Stub needle.get (callback form) to deliver the given needle-style response + body */ + function stubNeedleGet(response: any, body: any, error: any = null) { + return sinon.stub(needle, 'get').callsFake(((url: string, opts: any, callback: any) => { + if (callback) { + process.nextTick(callback, error, response, body); + } + return new PassThrough() as any; + }) as any); + } + + describe('UnauthorizedDeviceResponseError (401)', () => { + it('attaches results with response.statusCode, request.host, and a string body', async () => { + stubNeedlePost({ statusCode: 401, headers: {} }, Buffer.alloc(0)); + let caught: any; + try { + await rokuDeploy.deleteInstalledChannel({ host: '1.2.3.4', password: 'aaaa' } as any); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.UnauthorizedDeviceResponseError); + //message embeds the host pulled off results.response.request.host + expect(caught.message).to.equal(`Unauthorized. Please verify credentials for host '1.2.3.4'`); + //the postman-style results object + expect(caught.results).to.be.an('object'); + expect(caught.results.response.statusCode).to.equal(401); + expect(caught.results.response.request.host).to.equal('1.2.3.4'); + expect(caught.results.response.headers).to.be.an('object'); + expect(caught.results.body).to.be.a('string'); + }); + + it('attaches the guaranteed results structure (verified against postman-request 3.17.6)', async () => { + //The parity harness confirmed the old postman-request build attached `{ response, body }` + //where `response` is the underlying http.IncomingMessage. The shim reproduces that (it + //returns needle's IncomingMessage), so we assert the GUARANTEED fields a consumer reads + //rather than deep-equaling the full IncomingMessage surface (which we intentionally keep). + const headers = { 'content-length': '0', 'www-authenticate': 'Digest realm="rokudev"' }; + stubNeedlePost({ statusCode: 401, headers: headers }, Buffer.alloc(0)); + let caught: any; + try { + await rokuDeploy.deleteInstalledChannel({ host: '1.2.3.4', password: 'aaaa' } as any); + } catch (e) { + caught = e; + } + expect(caught.results.body).to.equal(''); + expect(caught.results.response.statusCode).to.equal(401); + expect(caught.results.response.headers).to.eql({ 'content-length': '0', 'www-authenticate': 'Digest realm="rokudev"' }); + expect(caught.results.response.body).to.equal(''); + expect(caught.results.response.request.host).to.equal('1.2.3.4'); + expect(caught.results.response.request.href).to.equal('http://1.2.3.4:80/plugin_install'); + }); + }); + + describe('InvalidDeviceResponseCodeError (non-200)', () => { + it('attaches results with the offending statusCode and message', async () => { + stubNeedlePost({ statusCode: 500, headers: {} }, 'oops'); + let caught: any; + try { + await rokuDeploy.deleteInstalledChannel({ host: '1.2.3.4', password: 'aaaa' } as any); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.InvalidDeviceResponseCodeError); + expect(caught.message).to.equal('Invalid response code: 500'); + expect(caught.results.response.statusCode).to.equal(500); + expect(caught.results.body).to.equal('oops'); + }); + }); + + describe('UnparsableDeviceResponseError', () => { + it('is thrown (with results) when the response object is missing', async () => { + //needle delivers no response object and no body + stubNeedlePost(undefined, undefined); + let caught: any; + try { + await rokuDeploy.deleteInstalledChannel({ host: '1.2.3.4', password: 'aaaa' } as any); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.UnparsableDeviceResponseError); + }); + }); + + describe('FailedDeviceResponseError (roku message in body)', () => { + it('attaches the parsed rokuMessages object (errors/infos/successes)', async () => { + const body = getFakeResponseBody(` + Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); + `); + stubNeedlePost({ statusCode: 200, headers: {} }, body); + let caught: any; + try { + await rokuDeploy.deleteInstalledChannel({ host: '1.2.3.4', password: 'aaaa' } as any); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.FailedDeviceResponseError); + expect(caught.message).to.equal('Failure: Form Error: "archive" Field Not Found'); + //for this error, `results` is the rokuMessages object, not the http results + expect(caught.results).to.eql({ + errors: ['Failure: Form Error: "archive" Field Not Found'], + infos: [], + successes: [] + }); + }); + }); + + describe('getDeviceInfo -> EcpNetworkAccessModeDisabledError', () => { + it('detects a Roku server header on the error results', async () => { + //device-info is a GET; a Roku server header on a failing response means ECP is disabled + stubNeedleGet({ statusCode: 403, headers: { server: 'Roku/12.0' } }, 'forbidden'); + let caught: any; + try { + await rokuDeploy.getDeviceInfo({ host: '1.2.3.4' }); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.EcpNetworkAccessModeDisabledError); + }); + + it('does NOT treat a non-Roku server header as ECP-disabled', async () => { + stubNeedleGet({ statusCode: 500, headers: { server: 'Apache' } }, 'err'); + let caught: any; + try { + await rokuDeploy.getDeviceInfo({ host: '1.2.3.4' }); + } catch (e) { + caught = e; + } + //the original (InvalidDeviceResponseCodeError from checkRequest) bubbles up, with results intact + expect(caught).to.be.instanceof(errors.InvalidDeviceResponseCodeError); + expect(caught.results.response.headers.server).to.equal('Apache'); + }); + }); + + describe('publish error mapping', () => { + beforeEach(() => { + options.host = '1.2.3.4'; + options.failOnCompileError = true; + options.retainDeploymentArchive = true; + //make a dummy zip so publish gets past the file-exists check + fsExtra.outputFileSync(rokuDeploy.getOutputZipFilePath(options), 'fake-zip-content'); + }); + + it('maps a 577 response to UpdateCheckRequiredError', async () => { + stubNeedlePost({ statusCode: 577, headers: {} }, ''); + let caught: any; + try { + await rokuDeploy.publish(options); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.UpdateCheckRequiredError); + }); + + it('maps an ECONNRESET to ConnectionResetError', async () => { + const resetError: any = new Error('socket hang up'); + resetError.code = 'ECONNRESET'; + stubNeedlePost(undefined, undefined, resetError); + let caught: any; + try { + await rokuDeploy.publish(options); + } catch (e) { + caught = e; + } + expect(caught).to.be.instanceof(errors.ConnectionResetError); + }); + }); + }); + describe('getRokuMessagesFromResponseBody', () => { it('exits on unknown message type', () => { const result = rokuDeploy['getRokuMessagesFromResponseBody'](` @@ -4486,43 +4669,30 @@ describe('RokuDeploy', () => { expect(currentVersion).to.exist.and.to.match(/^\d+\.\d+\.\d+.*/); }); - it('works when params is undefined', () => { - //undefined - expect( - rokuDeploy['setUserAgentIfMissing'](undefined) - ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); - }); - - it('works when params has no header container', () => { - expect( - rokuDeploy['setUserAgentIfMissing']({} as any) - ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + it('adds the header container when params has none', () => { + const params = {} as any; + rokuDeploy['setUserAgentIfMissing'](params); + expect(params).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); }); - it('works when params has empty header container', () => { - expect( - rokuDeploy['setUserAgentIfMissing']({} as any) - ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + it('sets the user agent when params has an empty header container', () => { + const params = { headers: {} } as any; + rokuDeploy['setUserAgentIfMissing'](params); + expect(params).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); }); - it('works when params has existing header container with no user agent', () => { - expect( - rokuDeploy['setUserAgentIfMissing']({ headers: {} } as any) - ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); - }); - - it('works when params has existing header container with user agent', () => { - expect( - rokuDeploy['setUserAgentIfMissing']({ headers: { 'User-Agent': 'some-other-user-agent' } } as any) - ).to.eql({ headers: { 'User-Agent': 'some-other-user-agent' } }); + it('does not overwrite an existing user agent', () => { + const params = { headers: { 'User-Agent': 'some-other-user-agent' } } as any; + rokuDeploy['setUserAgentIfMissing'](params); + expect(params).to.eql({ headers: { 'User-Agent': 'some-other-user-agent' } }); }); it('works when we fail to load package version', () => { sinon.stub(fsExtra, 'readJsonSync').throws(new Error('Unable to read package.json')); rokuDeploy['_packageVersion'] = undefined; - expect( - rokuDeploy['setUserAgentIfMissing']({} as any) - ).to.eql({ headers: { 'User-Agent': 'roku-deploy/unknown' } }); + const params = {} as any; + rokuDeploy['setUserAgentIfMissing'](params); + expect(params).to.eql({ headers: { 'User-Agent': 'roku-deploy/unknown' } }); }); }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index b8f35b91..08f4e9ca 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1,8 +1,7 @@ import * as path from 'path'; import * as _fsExtra from 'fs-extra'; -import * as r from 'postman-request'; -import type * as requestType from 'request'; -const request = r as typeof requestType; +import { request } from './request'; +import type { RequestOptions } from './request'; import * as JSZip from 'jszip'; import * as errors from './Errors'; import * as isGlob from 'is-glob'; @@ -89,7 +88,7 @@ export class RokuDeploy { * @param files */ public normalizeFilesArray(files: FileEntry[]) { - const result: Array = []; + const result: Array = []; for (let i = 0; i < files.length; i++) { let entry = files[i]; @@ -332,7 +331,7 @@ export class RokuDeploy { * @param pattern the glob pattern originally used to find this file * @param rootDir absolute normalized path to the rootDir */ - private computeFileDestPath(srcPath: string, entry: string | StandardizedFileEntry, rootDir: string) { + private computeFileDestPath(srcPath: string, entry: StandardizedFileEntry | string, rootDir: string) { let result: string; let globstarIdx: number; //files under rootDir with no specified dest @@ -427,7 +426,7 @@ export class RokuDeploy { })); } - private generateBaseRequestOptions(requestPath: string, options: RokuDeployOptions, formData = {} as T): requestType.OptionsWithUrl { + private generateBaseRequestOptions(requestPath: string, options: RokuDeployOptions, formData = {} as T): RequestOptions { options = this.getOptions(options); let url = `http://${options.host}:${options.packagePort}/${requestPath}`; let baseRequestOptions = { @@ -747,21 +746,14 @@ export class RokuDeploy { } /** - * Set the `User-Agent` header if missing from the request params, ensuring it's included in all requests made by roku-deploy + * Set the `User-Agent` header (in place) if missing from the request params, ensuring it's included + * in all requests made by roku-deploy. Mutates `params.headers` because callers pass the same object + * straight on to `request.post`/`request.get`. * @param params - * @returns */ - private setUserAgentIfMissing(params: requestType.OptionsWithUrl) { - if (!params) { - params = {} as requestType.OptionsWithUrl; - } - if (!params.headers) { - params.headers = {}; - } - if (!params.headers['User-Agent']) { - params.headers['User-Agent'] = this.getUserAgent(); - } - return params; + private setUserAgentIfMissing(params: RequestOptions) { + params.headers ??= {}; + params.headers['User-Agent'] ??= this.getUserAgent(); } /** @@ -785,7 +777,7 @@ export class RokuDeploy { * Centralized function for handling POST http requests * @param params */ - private async doPostRequest(params: requestType.OptionsWithUrl, verify = true) { + private async doPostRequest(params: RequestOptions, verify = true) { let results: { response: any; body: any } = await new Promise((resolve, reject) => { this.setUserAgentIfMissing(params); @@ -807,7 +799,7 @@ export class RokuDeploy { * Centralized function for handling GET http requests * @param params */ - private async doGetRequest(params: requestType.OptionsWithUrl) { + private async doGetRequest(params: RequestOptions) { let results: { response: any; body: any } = await new Promise((resolve, reject) => { this.setUserAgentIfMissing(params); @@ -1037,8 +1029,8 @@ export class RokuDeploy { * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ public async takeScreenshot(options: TakeScreenshotOptions) { - options.outDir = options.outDir ?? this.screenshotDir; - options.outFile = options.outFile ?? `screenshot-${formatTimestampForScreenshot()}`; + options.outDir ??= this.screenshotDir; + options.outFile ??= `screenshot-${formatTimestampForScreenshot()}`; let saveFilePath: string; // Ask for the device to make an image @@ -1275,8 +1267,8 @@ export class RokuDeploy { * @param host the host or IP address of the Roku * @param port the port to use for the ECP request (defaults to 8060) */ - public async getDeviceInfo(options?: { enhance: true } & GetDeviceInfoOptions): Promise; - public async getDeviceInfo(options?: GetDeviceInfoOptions): Promise + public async getDeviceInfo(options?: GetDeviceInfoOptions & { enhance: true }): Promise; + public async getDeviceInfo(options?: GetDeviceInfoOptions): Promise; public async getDeviceInfo(options: GetDeviceInfoOptions) { options = this.getOptions(options) as any; @@ -1525,14 +1517,17 @@ export interface ManifestData { } export interface BeforeZipCallbackInfo { + /** * Contains an associative array of the parsed values in the manifest */ manifestData: ManifestData; + /** * @deprecated since 3.9.0. use `stagingDir` instead */ stagingFolderPath: string; + /** * The directory where the files were staged */ @@ -1540,10 +1535,12 @@ export interface BeforeZipCallbackInfo { } export interface StandardizedFileEntry { + /** * The full path to the source file */ src: string; + /** * The path relative to the root of the pkg to where the file should be placed */ @@ -1587,6 +1584,7 @@ export interface HttpResponse { } export interface TakeScreenshotOptions { + /** * The IP address or hostname of the target Roku device. * @example '192.168.1.21' @@ -1612,31 +1610,40 @@ export interface TakeScreenshotOptions { } export interface ValidateDeveloperPasswordOptions { + /** The hostname or IP of the Roku device */ host: string; + /** The developer password to check */ password: string; + /** Defaults to `'rokudev'` */ username?: string; + /** Defaults to `80` (the developer web-server port) */ port?: number; + /** Milliseconds to wait for each HTTP round-trip. Defaults to `3000`. */ timeout?: number; } export interface GetDeviceInfoOptions { + /** * The hostname or IP address to use for the device-info URL */ host: string; + /** * The port to use to send the device-info request (defaults to the standard 8060 ECP port) */ remotePort?: number; + /** * The number of milliseconds at which point this request should timeout and return a rejected promise */ timeout?: number; + /** * Should the device-info be enhanced by camel-casing the property names and converting boolean strings to booleans and number strings to numbers? * @default false @@ -1644,4 +1651,4 @@ export interface GetDeviceInfoOptions { enhance?: boolean; } -export type EcpNetworkAccessMode = 'enabled' | 'disabled' | 'limited' | 'permissive'; +export type EcpNetworkAccessMode = 'disabled' | 'enabled' | 'limited' | 'permissive'; diff --git a/src/device.spec.ts b/src/device.spec.ts index 660dc64a..bf2b1b14 100644 --- a/src/device.spec.ts +++ b/src/device.spec.ts @@ -1,22 +1,58 @@ import * as assert from 'assert'; import * as fsExtra from 'fs-extra'; +import * as net from 'net'; +import * as http from 'http'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as dotenv from 'dotenv'; import * as rokuDeploy from './index'; +import * as errors from './Errors'; import { cwd, expectPathExists, expectThrowsAsync, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; import undent from 'undent'; -//these tests are run against an actual roku device. These cannot be enabled when run on the CI server +//load device connection info from a .env file at the repo root (if present), then fall back to any +//pre-existing environment variables. This is how CI/CD (and local dev) point the device suite at a +//real Roku without hardcoding host/password into the repo. See .env.example. +dotenv.config({ + path: path.resolve(__dirname, '../.env'), + override: true, + quiet: true +}); + +const HOST = process.env.ROKU_HOST; +const PASSWORD = process.env.ROKU_PASSWORD; + +//socket teardown callbacks, drained in afterEach so the suite doesn't hang open +const cleanups: Array<() => void> = []; + +//explicit short per-request timeout for the reboot-prone tests. The roku-deploy default is 150s; a +//short timeout guarantees any request against an unresponsive/rebooting device aborts (and closes its +//socket) quickly instead of hanging open and keeping the mocha process alive after the run. +const REQUEST_TIMEOUT = 15_000; + +//these tests are run against an actual roku device and need to be run on our self-hosted runners. describe('device', function device() { let options: rokuDeploy.RokuDeployOptions; + before(() => { + //fail fast with a clear message rather than letting every test time out against an empty host + if (!HOST || !PASSWORD) { + throw new Error( + `Missing Roku device connection info. Set ROKU_HOST and ROKU_PASSWORD in "${path.resolve(__dirname, '../.env')}" ` + + `(see .env.example) or as environment variables before running "npm run test:device".` + ); + } + }); + beforeEach(() => { fsExtra.emptyDirSync(tempDir); fsExtra.ensureDirSync(rootDir); process.chdir(rootDir); options = rokuDeploy.getOptions({ outDir: outDir, - host: '192.168.1.93', + host: HOST, retainDeploymentArchive: true, - password: 'aaaa', + password: PASSWORD, devId: 'c6fdc2019903ac3332f624b0b2c2fe2c733c3e74', rekeySignedPackage: `${cwd}/testSignedPackage.pkg`, signingPassword: 'drRCEVWP/++K5TYnTtuAfQ==' @@ -34,7 +70,7 @@ describe('device', function device() { splash_color=#000000 `], ['source/main.brs', undent` - Sub RunUserInterface() + Sub Main() screen = CreateObject("roSGScreen") m.scene = screen.CreateScene("HomeScene") port = CreateObject("roMessagePort") @@ -50,11 +86,21 @@ describe('device', function device() { screen = invalid end if End Sub + `], + ['components/HomeScene.xml', undent` + + `] ]); }); afterEach(() => { + //tear down any sockets/connections opened during the test so the suite doesn't hang open + while (cleanups.length > 0) { + try { + cleanups.pop()(); + } catch { } + } //restore the original working directory process.chdir(cwd); fsExtra.emptyDirSync(tempDir); @@ -79,6 +125,14 @@ describe('device', function device() { }); }); + describe('publish', () => { + it('works', async () => { + await rokuDeploy.createPackage(options); + let response = await rokuDeploy.publish(options); + assert.equal(response.message, 'Successful deploy'); + }); + }); + describe('deployAndSignPackage', () => { it('works', async () => { await rokuDeploy.deleteInstalledChannel(options); @@ -116,4 +170,268 @@ describe('device', function device() { }); }); }); + + describe('getDeviceInfo', () => { + it('works', async () => { + const info = await rokuDeploy.rokuDeploy.getDeviceInfo({ host: options.host }); + assert.ok(info); + assert.ok(info['software-version']); + }); + + it('normalizes types when enhanced', async () => { + const info = await rokuDeploy.rokuDeploy.getDeviceInfo({ host: options.host, enhance: true }); + assert.ok(info.softwareVersion); + assert.strictEqual(typeof info.supportsEthernet, 'boolean'); + }); + }); + + describe('getDevId', () => { + it('works', async () => { + const devId = await rokuDeploy.rokuDeploy.getDevId(options); + assert.ok(devId); + }); + }); + + describe('getEcpNetworkAccessMode', () => { + it('works', async () => { + const mode = await rokuDeploy.rokuDeploy.getEcpNetworkAccessMode({ host: options.host }); + assert.ok([undefined, 'enabled', 'disabled', 'limited', 'permissive'].includes(mode)); + }); + }); + + describe('pressHomeButton', () => { + it('works', async () => { + await rokuDeploy.rokuDeploy.pressHomeButton(options.host); + }); + }); + + describe('takeScreenshot', () => { + it('works', async function takeScreenshot() { + this.timeout(60000); + + //A screenshot only works when a side-loaded channel is actively running. Rather than + //guessing that `deploy` left the app running, we make main.brs print a unique, timestamped + //marker once its scene is shown, connect to the debug console (telnet port 8085), and wait + //until we observe THAT marker. Then we cross-check via ECP that the dev channel really is + //foregrounded before asking for the screenshot. + const marker = `ROKU_DEPLOY_SCREENSHOT_TEST ${new Date().toISOString()} ${Math.random().toString(36).slice(2)}`; + writeFiles(rootDir, [ + ['source/main.brs', undent` + Sub Main() + screen = CreateObject("roSGScreen") + m.scene = screen.CreateScene("HomeScene") + port = CreateObject("roMessagePort") + screen.SetMessagePort(port) + screen.Show() + print "${marker}" + + while(true) + msg = wait(0, port) + end while + End Sub + `] + ]); + + //start listening on the debug console BEFORE deploying so we don't miss the marker. + //(the socket's teardown is registered in `cleanups` and drained by afterEach) + const sawMarker = waitForConsoleOutput(options.host, marker, 45000); + + await rokuDeploy.deploy(options); + + //the marker proves our freshly-deployed channel actually reached the "scene shown" point this run + await sawMarker; + + //belt-and-suspenders: confirm the dev channel is the active app via ECP + const activeApp = await getActiveApp(options.host); + assert.ok(/dev/i.test(activeApp), `expected the dev channel to be the active app, got: ${activeApp}`); + + const filePath = await rokuDeploy.rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); + expectPathExists(filePath); + }); + }); + + describe('convertToSquashfs', () => { + it('works', async () => { + await rokuDeploy.deploy(options); + await rokuDeploy.rokuDeploy.convertToSquashfs(options); + }); + }); + + describe('deleteAllComponentLibraries', () => { + it('works', async () => { + await rokuDeploy.rokuDeploy.deleteAllComponentLibraries({ host: options.host, password: options.password }); + }); + }); + + describe('deleteInstalledChannel', () => { + it('works', async () => { + await rokuDeploy.deploy(options); + await rokuDeploy.deleteInstalledChannel(options); + }); + }); + + describe('rebootDevice', () => { + it('works', async function rebootDevice() { + //a reboot takes the device offline for a while; allow time for it to come back + this.timeout(180_000); + //use a short per-request timeout so the reboot POST can't hang open past the device going + //down; without this it would inherit the 150s default and could orphan a socket if mocha's + //test-timeout fired first. + await rokuDeploy.rokuDeploy.rebootDevice({ ...options, timeout: REQUEST_TIMEOUT }); + //wait until the device is reachable again so the next test doesn't run mid-reboot + await waitForDeviceOnline(options.host); + }); + }); + + describe('checkForUpdate', () => { + //checkForUpdate requires firmware >= this version; below it, it throws UnsupportedFirmwareVersionError + const MIN_FIRMWARE = '15.0.4'; + + it('works', async function checkForUpdate() { + //triggers a real update check against Roku's servers, which can be slow and can sometimes + //trigger a reboot, so allow generous time for the device to come back afterward + this.timeout(240_000); + + //Every device call below uses an explicit short `timeout` so no underlying needle request can + //hang open indefinitely (the default is 150s). This guarantees each request either resolves or + //rejects and closes its socket on its own, rather than being orphaned if mocha's test-timeout + //were to fire mid-request. + const reqOptions = { ...options, timeout: REQUEST_TIMEOUT }; + + //we don't know which device the suite runs against, so ask it what firmware it has and + //decide up-front whether checkForUpdate should succeed or be rejected by the version gate. + const softwareVersion = (await rokuDeploy.rokuDeploy.getDeviceInfo({ host: options.host, timeout: REQUEST_TIMEOUT }))['software-version']; + const supported = !!softwareVersion && semver.gte(semver.coerce(softwareVersion), MIN_FIRMWARE); + + if (supported) { + console.log(`[checkForUpdate] device firmware ${softwareVersion} >= ${MIN_FIRMWARE}; expecting success`); + const result = await rokuDeploy.rokuDeploy.checkForUpdate(reqOptions); + assert.ok(result, 'expected a response from checkForUpdate'); + //checkForUpdate can trigger a reboot; make sure the device is back before the next test + await waitForDeviceOnline(options.host); + } else { + console.log(`[checkForUpdate] device firmware ${softwareVersion} < ${MIN_FIRMWARE}; expecting UnsupportedFirmwareVersionError`); + let thrown: Error; + try { + await rokuDeploy.rokuDeploy.checkForUpdate(reqOptions); + } catch (e) { + thrown = e as Error; + } + assert.ok(thrown, 'expected checkForUpdate to throw on unsupported firmware'); + assert.ok( + thrown instanceof errors.UnsupportedFirmwareVersionError, + `expected UnsupportedFirmwareVersionError, got ${thrown?.constructor?.name}: ${thrown?.message}` + ); + } + }); + }); }); + +/** + * Connect to the Roku debug console (telnet, port 8085) and resolve once a line containing `marker` + * is observed. Rejects if the marker isn't seen within `timeout` ms. Used to prove that a + * freshly-deployed channel actually reached a known point in its own code during THIS test run. + * + * The telnet socket's teardown is registered in `cleanups` so the `afterEach` hook always tears it + * down (even on the happy path), otherwise the open connection keeps the mocha process alive after + * the suite finishes. + */ +function waitForConsoleOutput(host: string, marker: string, timeout: number): Promise { + return new Promise((resolve, reject) => { + const socket = net.connect(8085, host); + //don't let this socket alone keep the event loop (and thus the process) alive + socket.unref(); + let buffer = ''; + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out after ${timeout}ms waiting for marker "${marker}" on ${host}:8085`)); + }, timeout); + //setTimeout also holds the loop open; let it be non-blocking too + timer.unref(); + + function cleanup() { + clearTimeout(timer); + socket.removeAllListeners(); + socket.destroy(); + } + //ensure the socket is always torn down, even if the marker arrives and we resolve normally + cleanups.push(cleanup); + + socket.on('data', (chunk) => { + buffer += chunk.toString(); + if (buffer.includes(marker)) { + cleanup(); + resolve(); + } + }); + socket.on('error', (err) => { + cleanup(); + reject(err); + }); + }); +} + +/** + * Query ECP (port 8060) for the currently-active app and return its raw XML body. The dev channel + * reports itself as `dev` in the `` node, so callers can assert against that. + */ +function getActiveApp(host: string): Promise { + return new Promise((resolve, reject) => { + const req = http.get(`http://${host}:8060/query/active-app`, (res) => { + let body = ''; + res.on('data', (chunk) => { + body += chunk.toString(); + }); + res.on('end', () => resolve(body)); + }); + //register teardown so afterEach can kill a hung request; harmless once the request completes + cleanups.push(() => req.destroy()); + req.on('error', reject); + //never let a hung request keep the process open + req.setTimeout(10000, () => { + req.destroy(new Error(`Timed out querying active-app on ${host}:8060`)); + }); + }); +} + +/** + * Wait for a device to be reachable again by polling its device-info over ECP until it responds. + * Used after operations that reboot the device (rebootDevice, and sometimes checkForUpdate) so the + * next test doesn't run against a device that's still rebooting. + * + * @param graceMs how long to wait before the first poll, giving the device time to actually go down + * after the reboot was issued (so we don't immediately see the still-alive pre-reboot device) + */ +async function waitForDeviceOnline(host: string, timeoutMs = 120_000, intervalMs = 3000, graceMs = 5000): Promise { + const deadline = Date.now() + timeoutMs; + //give the device a moment to actually start going down before we begin polling + await sleep(graceMs); + let lastError: Error; + while (Date.now() < deadline) { + try { + await rokuDeploy.rokuDeploy.getDeviceInfo({ host: host, timeout: intervalMs }); + //a successful device-info query means ECP is up and the device is responsive again + return; + } catch (e) { + lastError = e as Error; + await sleep(intervalMs); + } + } + throw new Error(`Device ${host} did not come back online within ${timeoutMs}ms. Last error: ${lastError?.message}`); +} + +/** + * A sleep whose timer is `unref()`'d and registered in `cleanups`, so a pending delay can never keep + * the mocha process alive after the suite finishes (unlike a bare `setTimeout`). + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(resolve, ms); + timer.unref?.(); + cleanups.push(() => { + clearTimeout(timer); + //resolve so any awaiter unblocks during teardown instead of hanging + resolve(); + }); + }); +} diff --git a/src/request.spec.ts b/src/request.spec.ts new file mode 100644 index 00000000..88a92b2d --- /dev/null +++ b/src/request.spec.ts @@ -0,0 +1,432 @@ +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; +import { PassThrough } from 'stream'; +import * as needle from 'needle'; +import { request } from './request'; + +const sinon = createSandbox(); + +/** + * Unit tests for the needle compatibility shim. + * + * These pin down the translation between roku-deploy's `request`/`postman-request`-style + * options/response and needle, by stubbing `needle` directly. The error/response SHAPE + * here is part of roku-deploy's public surface (it's attached to thrown errors), so getting any + * of this wrong is a breaking change. + */ +describe('request (needle shim)', () => { + + /** captured args from the stubbed needle call: { url, data, options, callback } */ + let postArgs: { url: string; data: any; options: any; callback: any }; + let getArgs: { url: string; options: any; callback: any }; + + /** Stub needle.post and capture/drive the callback */ + function stubPost(err: any, response: any, body: any) { + return sinon.stub(needle, 'post').callsFake(((url: string, data: any, options: any, callback: any) => { + postArgs = { url: url, data: data, options: options, callback: callback }; + //invoke async like needle does + process.nextTick(callback, err, response, body); + return {} as any; + }) as any); + } + + /** Stub needle.get (callback form) and capture/drive the callback */ + function stubGet(err: any, response: any, body: any) { + return sinon.stub(needle, 'get').callsFake(((url: string, options: any, callback: any) => { + getArgs = { url: url, options: options, callback: callback }; + if (callback) { + process.nextTick(callback, err, response, body); + } + return new PassThrough() as any; + }) as any); + } + + /** Promise wrapper around the callback-style shim methods */ + function callPost(params: any): Promise<{ error: any; response: any; body: any }> { + return new Promise((resolve) => { + request.post(params, (error, response, body) => resolve({ error: error, response: response, body: body })); + }); + } + function callGet(params: any): Promise<{ error: any; response: any; body: any }> { + return new Promise((resolve) => { + request.get(params, (error, response, body) => resolve({ error: error, response: response, body: body })); + }); + } + + afterEach(() => { + sinon.restore(); + postArgs = undefined; + getArgs = undefined; + }); + + describe('option translation', () => { + it('sets parse_response=false and maps timeout to open_timeout + response_timeout', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/plugin_install', timeout: 12345, formData: { a: 'b' } }); + expect(postArgs.options.parse_response).to.equal(false); + expect(postArgs.options.open_timeout).to.equal(12345); + expect(postArgs.options.response_timeout).to.equal(12345); + }); + + it('does NOT set read_timeout (its lingering re-armed timer leaks a handle in the digest-auth path)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/plugin_install', timeout: 12345, formData: { a: 'b' } }); + expect(postArgs.options.read_timeout).to.be.undefined; + }); + + it('passes through headers', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/x', headers: { 'User-Agent': 'roku-deploy/test' }, formData: { a: 'b' } }); + expect(postArgs.options.headers).to.eql({ 'User-Agent': 'roku-deploy/test' }); + }); + + it('closes the connection by default (needle keeps sockets alive otherwise, which pins the process)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' } }); + expect(postArgs.options.connection).to.equal('close'); + }); + + it('closes the connection when agentOptions.keepAlive is false (request parity)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' }, agentOptions: { keepAlive: false } }); + expect(postArgs.options.connection).to.equal('close'); + }); + + it('leaves the connection alone when the caller explicitly opts into keepAlive', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' }, agentOptions: { keepAlive: true } }); + expect(postArgs.options.connection).to.be.undefined; + }); + + it('closes the connection by default on GET too', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + await callGet({ url: 'http://1.2.3.4:80/x' }); + expect(getArgs.options.connection).to.equal('close'); + }); + + it('translates auth into digest username/password', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ + url: 'http://1.2.3.4:80/x', + auth: { user: 'rokudev', pass: 'aaaa', sendImmediately: false }, + formData: { a: 'b' } + }); + expect(postArgs.options.auth).to.equal('digest'); + expect(postArgs.options.username).to.equal('rokudev'); + expect(postArgs.options.password).to.equal('aaaa'); + }); + + it('accepts auth specified as username/password (request alias)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ + url: 'http://1.2.3.4:80/x', + auth: { username: 'rokudev', password: 'aaaa' }, + formData: { a: 'b' } + }); + expect(postArgs.options.auth).to.equal('digest'); + expect(postArgs.options.username).to.equal('rokudev'); + expect(postArgs.options.password).to.equal('aaaa'); + }); + + it('bakes the qs object into the url', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + await callGet({ url: 'http://1.2.3.4:80/plugin_install', qs: { dcl_enabled: '1', foo: 'bar' } }); + expect(getArgs.url).to.equal('http://1.2.3.4:80/plugin_install?dcl_enabled=1&foo=bar'); + }); + + it('appends qs with & when the url already has a query string', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + await callGet({ url: 'http://1.2.3.4:80/x?already=1', qs: { more: '2' } }); + expect(getArgs.url).to.equal('http://1.2.3.4:80/x?already=1&more=2'); + }); + + it('skips null/undefined qs values', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + await callGet({ url: 'http://1.2.3.4:80/x', qs: { keep: '1', drop: null, gone: undefined } }); + expect(getArgs.url).to.equal('http://1.2.3.4:80/x?keep=1'); + }); + + it('leaves the url untouched when every qs value is null/undefined', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + await callGet({ url: 'http://1.2.3.4:80/x', qs: { drop: null, gone: undefined } }); + expect(getArgs.url).to.equal('http://1.2.3.4:80/x'); + }); + }); + + describe('formData / multipart translation', () => { + it('enables multipart and passes form data when fields are present', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ url: 'http://1.2.3.4:80/x', formData: { mysubmit: 'Replace' } }); + expect(postArgs.options.multipart).to.equal(true); + expect(postArgs.data).to.eql({ mysubmit: 'Replace' }); + }); + + it('drops null/undefined/empty-string fields (request did this implicitly)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + await callPost({ + url: 'http://1.2.3.4:80/x', + formData: { keep: 'yes', empty: '', nothing: null, missing: undefined } + }); + expect(postArgs.data).to.eql({ keep: 'yes' }); + }); + + it('does NOT enable multipart for a bodyless POST (e.g. ECP keypress)', async () => { + stubPost(null, { statusCode: 200, headers: {} }, ''); + await callPost({ url: 'http://1.2.3.4:8060/keypress/Home' }); + expect(postArgs.options.multipart).to.be.undefined; + expect(postArgs.data).to.be.null; + }); + + it('does NOT enable multipart when all form fields were dropped', async () => { + stubPost(null, { statusCode: 200, headers: {} }, ''); + await callPost({ url: 'http://1.2.3.4:80/x', formData: { archive: '', mysubmit: null } }); + expect(postArgs.options.multipart).to.be.undefined; + expect(postArgs.data).to.be.null; + }); + + it('translates a readable stream field into needle file-by-path form', async () => { + stubPost(null, { statusCode: 200, headers: {} }, 'ok'); + //a fake fs.ReadStream-ish object + const fakeStream: any = new PassThrough(); + fakeStream.path = '/tmp/archive.zip'; + await callPost({ url: 'http://1.2.3.4:80/x', formData: { archive: fakeStream, mysubmit: 'Replace' } }); + expect(postArgs.data.archive).to.eql({ file: '/tmp/archive.zip', content_type: 'application/octet-stream' }); + expect(postArgs.data.mysubmit).to.equal('Replace'); + }); + }); + + describe('body coercion', () => { + it('coerces a Buffer body to a string', async () => { + stubPost(null, { statusCode: 200, headers: {} }, Buffer.from('hello world')); + const { body } = await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' } }); + expect(body).to.equal('hello world'); + expect(typeof body).to.equal('string'); + }); + + it('coerces an empty Buffer (typical bare 401 body) to an empty string', async () => { + stubPost(null, { statusCode: 401, headers: {} }, Buffer.alloc(0)); + const { body } = await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' } }); + expect(body).to.equal(''); + expect(typeof body).to.equal('string'); + }); + + it('passes a string body through unchanged', async () => { + stubGet(null, { statusCode: 200, headers: {} }, ''); + const { body } = await callGet({ url: 'http://1.2.3.4:8060/query/device-info' }); + expect(body).to.equal(''); + }); + + it('coerces a null/undefined body to an empty string', async () => { + stubGet(null, { statusCode: 200, headers: {} }, null); + const { body } = await callGet({ url: 'http://1.2.3.4:8060/x' }); + expect(body).to.equal(''); + }); + + it('stringifies a non-string, non-buffer body (defensive fallback)', async () => { + //parse_response:false should keep bodies as Buffers, but guard against a parsed value anyway + stubGet(null, { statusCode: 200, headers: {} }, 1234 as any); + const { body } = await callGet({ url: 'http://1.2.3.4:8060/x' }); + expect(body).to.equal('1234'); + }); + }); + + describe('response reshape (postman-request compatibility)', () => { + it('guarantees the request-compat fields on the response (verified against postman-request 3.17.6)', async () => { + //For maximum parity the shim returns needle's underlying http.IncomingMessage (so consumers + //keep access to statusCode/statusMessage/rawHeaders/httpVersion/req/socket/... just like with + //postman-request) and layers on the `request`-compat extras postman added. We therefore + //assert the GUARANTEED fields are present/correct rather than deep-equaling the whole object + //(it legitimately carries the full IncomingMessage surface, which we intentionally preserve). + const headers = { 'content-length': '0', 'www-authenticate': 'Digest realm="rokudev"' }; + stubPost(null, { statusCode: 401, headers: headers }, Buffer.alloc(0)); + const { response } = await callPost({ + url: 'http://1.2.3.4:80/plugin_install', + auth: { user: 'rokudev', pass: 'aaaa' }, + formData: { mysubmit: 'Delete', archive: '' } + }); + expect(response.statusCode).to.equal(401); + expect(response.headers).to.eql({ 'content-length': '0', 'www-authenticate': 'Digest realm="rokudev"' }); + //postman-request attached the (string) body to response.body too; the shim must match + expect(response.body).to.equal(''); + //the request-compat object postman exposed at response.request + expect(response.request.host).to.equal('1.2.3.4'); + expect(response.request.href).to.equal('http://1.2.3.4:80/plugin_install'); + expect(response.request.uri).to.include({ + host: '1.2.3.4:80', + hostname: '1.2.3.4', + port: '80', + protocol: 'http:', + href: 'http://1.2.3.4:80/plugin_install', + pathname: '/plugin_install' + }); + }); + + it('attaches the string body to response.body (postman-request parity)', async () => { + stubGet(null, { statusCode: 200, headers: {} }, Buffer.from('')); + const { response, body } = await callGet({ url: 'http://1.2.3.4:8060/query/device-info' }); + //the body is on BOTH the callback arg and response.body, and they're the same string + expect(response.body).to.equal(''); + expect(response.body).to.equal(body); + }); + + it('exposes statusCode, headers, request.host and request.href', async () => { + stubPost(null, { statusCode: 401, headers: { server: 'Roku' } }, ''); + const { response } = await callPost({ url: 'http://1.2.3.4:80/plugin_install', formData: { a: 'b' } }); + expect(response.statusCode).to.equal(401); + expect(response.headers).to.eql({ server: 'Roku' }); + //request.host strips the default :80 (matches request's behavior / URL semantics) + expect(response.request.host).to.equal('1.2.3.4'); + expect(response.request.href).to.equal('http://1.2.3.4:80/plugin_install'); + }); + + it('populates request.method and request.headers from the underlying req when present', async () => { + //needle's resp IS an http.IncomingMessage with a `.req` (ClientRequest). Simulate that so the + //shim can surface the outgoing method/headers the way postman-request did. + const fakeResp: any = { + statusCode: 200, + headers: {}, + req: { + method: 'POST', + getHeaders: () => ({ 'user-agent': 'roku-deploy/test' }) + } + }; + stubGet(null, fakeResp, 'ok'); + const { response } = await callGet({ url: 'http://1.2.3.4:80/plugin_install' }); + expect(response.request.method).to.equal('POST'); + //needle lowercases outgoing header names; the shim re-cases them to match postman-request + expect(response.request.headers).to.eql({ 'User-Agent': 'roku-deploy/test' }); + }); + + it('does not clobber a pre-existing response.request', async () => { + //if the underlying response already carries a `request` object, leave it untouched + const preExisting = { host: 'pre.existing', href: 'http://pre.existing/x', custom: 'kept' }; + const fakeResp: any = { statusCode: 200, headers: {}, request: preExisting }; + stubGet(null, fakeResp, 'ok'); + const { response } = await callGet({ url: 'http://1.2.3.4:80/plugin_install' }); + expect(response.request).to.equal(preExisting); + expect(response.request.custom).to.equal('kept'); + }); + + it('leaves request.uri.hostname null when the url cannot be parsed (url.parse parity)', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + const { response } = await callGet({ url: 'not-a-valid-url' }); + //url.parse() of a bare token yields null host/hostname (this is what postman-request produced too) + expect(response.request.host).to.be.null; + expect(response.request.uri.hostname).to.be.null; + }); + + it('exposes the port via request.uri (host=hostname, uri.host=host:port) — postman-request parity', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + const { response } = await callGet({ url: 'http://1.2.3.4:8060/query/device-info' }); + //postman-request set response.request.host to the hostname only; the port lived on uri.host + expect(response.request.host).to.equal('1.2.3.4'); + expect(response.request.uri.host).to.equal('1.2.3.4:8060'); + expect(response.request.uri.port).to.equal('8060'); + }); + + it('passes the raw headers object through (so callers can read e.g. headers.server)', async () => { + stubGet(null, { statusCode: 500, headers: { server: 'Apache', 'content-type': 'text/html' } }, 'body'); + const { response } = await callGet({ url: 'http://1.2.3.4:8060/query/device-info' }); + expect(response.headers.server).to.equal('Apache'); + expect(response.headers['content-type']).to.equal('text/html'); + }); + + it('yields an undefined response (no crash) when needle delivers no response object', async () => { + //request/postman-request could call back with no response; checkRequest guards on this. + stubPost(null, undefined, undefined); + const { error, response } = await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' } }); + expect(error).to.be.null; + expect(response).to.be.undefined; + }); + + it('still exposes the raw url as request.href when the url cannot be parsed', async () => { + stubGet(null, { statusCode: 200, headers: {} }, 'ok'); + const { response } = await callGet({ url: 'not-a-valid-url' }); + expect(response.request.host).to.be.null; + expect(response.request.href).to.equal('not-a-valid-url'); + }); + }); + + describe('error passthrough', () => { + it('forwards a needle error to the callback with undefined response/body (post)', async () => { + const networkError = new Error('socket hang up'); + stubPost(networkError, undefined, undefined); + const { error, response, body } = await callPost({ url: 'http://1.2.3.4:80/x', formData: { a: 'b' } }); + expect(error).to.equal(networkError); + expect(response).to.be.undefined; + expect(body).to.be.undefined; + }); + + it('forwards a needle error to the callback with undefined response/body (get)', async () => { + const networkError = new Error('ECONNREFUSED'); + stubGet(networkError, undefined, undefined); + const { error, response, body } = await callGet({ url: 'http://1.2.3.4:8060/x' }); + expect(error).to.equal(networkError); + expect(response).to.be.undefined; + expect(body).to.be.undefined; + }); + }); + + describe('streaming get (getToFile path)', () => { + it('returns the needle stream when no callback is given', () => { + const fakeStream = new PassThrough(); + sinon.stub(needle, 'get').returns(fakeStream as any); + const result = request.get({ url: 'http://1.2.3.4:80/pkgs/dev.pkg', auth: { user: 'u', pass: 'p' } } as any); + expect(result).to.equal(fakeStream); + }); + + it(`bridges needle's 'err' event to 'error'`, (done) => { + const fakeStream = new PassThrough(); + sinon.stub(needle, 'get').returns(fakeStream as any); + const stream: any = request.get({ url: 'http://1.2.3.4:80/x', auth: { user: 'u', pass: 'p' } } as any); + const theError = new Error('stream blew up'); + stream.on('error', (e) => { + expect(e).to.equal(theError); + done(); + }); + fakeStream.emit('err', theError); + }); + + it('swallows the intermediate 401 response, then forwards the retried 200 (digest auth)', () => { + const fakeStream = new PassThrough(); + sinon.stub(needle, 'get').returns(fakeStream as any); + const stream: any = request.get({ url: 'http://1.2.3.4:80/pkgs/dev.pkg', auth: { user: 'u', pass: 'p' } } as any); + + const seen: number[] = []; + stream.on('response', (resp) => seen.push(resp.statusCode)); + + //needle emits the digest challenge first, then the authenticated response + fakeStream.emit('response', { statusCode: 401 }); + fakeStream.emit('response', { statusCode: 200 }); + + //only the final 200 should have been surfaced + expect(seen).to.eql([200]); + }); + + it('does NOT swallow a 401 when there is no digest auth (no credentials)', () => { + const fakeStream = new PassThrough(); + sinon.stub(needle, 'get').returns(fakeStream as any); + const stream: any = request.get({ url: 'http://1.2.3.4:8060/x' } as any); + + const seen: number[] = []; + stream.on('response', (resp) => seen.push(resp.statusCode)); + fakeStream.emit('response', { statusCode: 401 }); + + expect(seen).to.eql([401]); + }); + + it('forwards a response event that has no response object (digest auth)', () => { + const fakeStream = new PassThrough(); + sinon.stub(needle, 'get').returns(fakeStream as any); + const stream: any = request.get({ url: 'http://1.2.3.4:80/x', auth: { user: 'u', pass: 'p' } } as any); + + let fired = false; + stream.on('response', () => { + fired = true; + }); + //an undefined resp must not be mistaken for the 401 challenge to swallow + fakeStream.emit('response', undefined); + expect(fired).to.be.true; + }); + }); +}); diff --git a/src/request.ts b/src/request.ts new file mode 100644 index 00000000..5f7994df --- /dev/null +++ b/src/request.ts @@ -0,0 +1,413 @@ +/* eslint-disable camelcase */ +import * as needle from 'needle'; +import * as urlModule from 'url'; +import type { ReadStream } from 'fs'; + +/** + * A thin compatibility shim over `needle` that mimics the small slice of the + * `request`/`postman-request` API that roku-deploy relies on. We migrated off + * `postman-request` (unmaintained, pulls in a large dependency tree) but a lot + * of roku-deploy's public surface — most notably the `results`/`response` + * object attached to thrown errors — was shaped by `request`. To keep this a + * non-breaking change, this shim reconstructs that same shape on top of + * needle's response object. + * + * Only `post` and `get` are implemented, since those are the only verbs + * roku-deploy uses. `get` additionally supports the callback-less streaming + * form (`request.get(opts).on(...).pipe(...)`) used when downloading files. + */ +export class Request { + + /** + * POST a multipart/form-data request, `request`-style. Invokes `callback` + * with `(error, response, body)`. + */ + public post(params: RequestOptions, callback: RequestCallback) { + const { url, data, needleOptions } = this.translateOptions(params, 'POST'); + return needle.post(url, data, needleOptions, (error, response, body) => { + if (error) { + return callback(error, undefined, undefined); + } + const coerced = this.coerceBody(body); + return callback(null, this.buildResponse(response, url, coerced), coerced); + }); + } + + /** + * GET a request, `request`-style. With a `callback`, invokes it with + * `(error, response, body)`. Without a callback, returns needle's readable + * stream (which supports `.on('error'|'response', ...)` and `.pipe(...)`) + * for the file-download path. + */ + public get(params: RequestOptions, callback?: RequestCallback) { + const { url, needleOptions } = this.translateOptions(params, 'GET'); + if (callback) { + return needle.get(url, needleOptions, (error, response, body) => { + if (error) { + return callback(error, undefined, undefined); + } + const coerced = this.coerceBody(body); + return callback(null, this.buildResponse(response, url, coerced), coerced); + }); + } + //streaming form (no callback) - used by getToFile to pipe the response to disk. + const stream = needle.get(url, needleOptions); + + //needle's stream emits its failures on the `'err'` event, but `request` (and roku-deploy's + //getToFile) listens on `'error'`, so bridge `'err'` -> `'error'` to preserve that behavior. + stream.on('err', (err) => stream.emit('error', err)); + + //digest auth in streaming mode: needle issues the unauthenticated request, emits a `'response'` + //for the 401 challenge, and *then* transparently retries with the Authorization header (emitting + //a second `'response'`). `request`/`postman-request` only ever surfaced the final response, and + //roku-deploy's `getToFile` treats any non-200 `'response'` as a hard failure. So when we're doing + //digest auth, swallow the intermediate 401 `'response'` event and only forward the retried one. + if (needleOptions.auth && needleOptions.username !== undefined) { + this.interceptIntermediate401(stream); + } + return stream; + } + + /** + * Translate the `request`-style options object that roku-deploy builds into the + * `(url, data, needleOptions)` triple that needle expects. + */ + private translateOptions(params: RequestOptions, method: 'GET' | 'POST') { + const url = this.buildUrl(params); + + const needleOptions: needle.NeedleOptions = { + //Roku responses are HTML/XML that roku-deploy parses by hand; never let needle auto-parse them + parse_response: false, + //`request` had a single `timeout` that governed how long to wait to establish the connection and + //receive a response. Map it to needle's `open_timeout` (connection) and `response_timeout` (time to + //first response byte). Deliberately do NOT set `read_timeout`: needle's read-timer is re-armed on + //every chunk and, in the digest-auth retry path, a read-timer can be left running after the request + //has already completed — it later fires `request.destroy()` and emits a spurious error, and (worse) + //keeps the Node event loop alive so a process that only made roku-deploy requests never exits. + open_timeout: params.timeout, + response_timeout: params.timeout, + headers: params.headers + }; + + //`request` was configured with `agentOptions: { keepAlive: false }` so each exchange used a fresh + //socket that closed when done. needle does NOT honor `agentOptions`, and on modern Node it does not + //send `Connection: close` by default, so the socket to the Roku is left open (keep-alive) after the + //response. That lingering socket keeps the Node event loop alive, so a process that only made + //roku-deploy requests never exits. Translate the old keepAlive:false intent into needle's + //`connection: 'close'` so needle sends `Connection: close` and tears the socket down after each + //response. Only skip this if the caller explicitly asked to keep the connection alive. + if (params.agentOptions?.keepAlive !== true) { + needleOptions.connection = 'close'; + } + + //digest auth. `request` was configured with `auth.sendImmediately: false`, which performs the + //401-challenge/response digest dance. needle does the same when `auth: 'digest'` is set. + const auth = params.auth; + if (auth) { + needleOptions.username = auth.user ?? auth.username; + needleOptions.password = auth.pass ?? auth.password; + needleOptions.auth = 'digest'; + } + + let data: any = null; + if (method === 'POST') { + const formData = this.translateFormData(params.formData); + //only send a multipart body when there's actually form data to send. Some POSTs (e.g. ECP + //keypress) have no body at all; needle's multipart builder throws "Empty multipart body" on an + //empty object, whereas `request` happily sent a bodyless POST. So fall back to a null body. + if (Object.keys(formData).length > 0) { + data = formData; + needleOptions.multipart = true; + } + } + + return { url: url, data: data, needleOptions: needleOptions }; + } + + /** + * Append the `qs` query-string object (if any) onto the url. `request` accepted + * `qs` as a separate option; needle expects it baked into the url. + */ + private buildUrl(params: RequestOptions): string { + let url = params.url; + const qs = params.qs; + if (qs && Object.keys(qs).length > 0) { + const search = new URLSearchParams(); + for (const key in qs) { + if (qs[key] !== undefined && qs[key] !== null) { + search.append(key, String(qs[key])); + } + } + const query = search.toString(); + if (query) { + url += (url.includes('?') ? '&' : '?') + query; + } + } + return url; + } + + /** + * Convert a `request`-style `formData` object into the shape needle's multipart + * builder understands. + * + * - `request` silently dropped `null`/`undefined`/empty-string fields. needle's + * multipart builder instead throws `"value missing for multipart!"` on empty + * values, so we drop those fields entirely (preserving the old behavior). + * - `request` accepted a readable stream (e.g. the zip `fs.ReadStream`) as a + * field value. needle's multipart builder does not handle streams, so we + * translate a stream into needle's documented `{ file, content_type }` form + * using the stream's `path`. + */ + private translateFormData(formData: Record | undefined): Record { + const result: Record = {}; + if (!formData) { + return result; + } + for (const key in formData) { + const value = formData[key]; + //drop empty values (request did this implicitly; needle would throw) + if (value === undefined || value === null || value === '') { + continue; + } + //a readable stream (the zip/pkg archive) -> needle file-by-path form + if (this.isReadableStream(value)) { + const filePath = (value).path; + result[key] = { + file: filePath, + content_type: 'application/octet-stream' + }; + } else { + result[key] = value; + } + } + return result; + } + + private isReadableStream(value: any): value is ReadStream { + return value && typeof value === 'object' && typeof value.pipe === 'function' && value.readable !== false; + } + + /** + * Coerce needle's response body into the `string` that roku-deploy expects. + * With `parse_response: false`, needle hands back a `Buffer` (an *empty* Buffer + * for empty responses such as a bare 401), whereas `request`/`postman-request` + * always delivered a decoded string. roku-deploy's `checkRequest` guards on + * `typeof body === 'string'`, so anything other than a string would be + * misreported as an unparsable response. + */ + private coerceBody(body: any): string { + if (Buffer.isBuffer(body)) { + return body.toString(); + } + if (typeof body === 'string') { + return body; + } + //null/undefined or a parsed object (shouldn't happen with parse_response:false) -> stringify safely + return body === undefined || body === null ? '' : String(body); + } + + /** + * Reshape needle's response into the `request`-compatible response roku-deploy expects (and attaches + * to thrown errors). + * + * Maximum-parity strategy: needle's callback `resp` is the *same* underlying Node + * `http.IncomingMessage` that `postman-request` exposed (needle just augments it with `.body`/`.raw`/ + * `.bytes`). So rather than fabricate a minimal plain object — which would drop everything underneath + * (`statusCode`/`statusMessage`/`rawHeaders`/`httpVersion*`/`socket`/`req`/`complete`/... that a + * consumer might reach into) — we KEEP needle's IncomingMessage and only layer on the two things + * `request`/`postman-request` added on top of it: + * 1. a `.request` object exposing the outgoing-request fields consumers read (`host`, `href`, ...), + * 2. a string `.body` (postman put the decoded string here; needle leaves a Buffer under + * `parse_response:false`). + * This way the vast majority of the old response's reachable surface is reproduced for free. + */ + private buildResponse(needleResponse: any, url: string, body: string): RequestResponse { + //`request`/`postman-request` could hand back a callback with no response object; roku-deploy's + //`checkRequest` explicitly guards on `!results.response`. Preserve that by passing through a missing + //response rather than throwing while trying to augment it. + if (!needleResponse) { + return undefined; + } + + //Parse with the legacy `url.parse()` to match postman-request's `response.request.uri` exactly: it + //was a Node `Url` object (host WITH port, plus auth/hash/query/search/slashes/protocol/port). The + //WHATWG `URL` would strip the default `:80` and omit these fields, so we deliberately use the legacy + //parser here for byte-parity with what consumers read off `response.request.uri.*`. + //`url.parse()` is total for string input (it never throws — a bare token like 'not-a-valid-url' + //yields null host/hostname and the token as path/pathname), so no try/catch is needed. + const u = urlModule.parse(url); + const uri: Record = { + protocol: u.protocol, + slashes: u.slashes, + auth: u.auth, + host: u.host, + port: u.port, + hostname: u.hostname, + hash: u.hash, + search: u.search, + query: u.query, + pathname: u.pathname, + path: u.path, + href: u.href + }; + + //needle's resp IS the http.IncomingMessage. Augment it in place to mirror postman-request's shape. + const response = needleResponse; + + //`request`/`postman-request` attached the (string) body to `response.body`. needle leaves a Buffer + //here under parse_response:false, so overwrite with the decoded string to match. + response.body = body; + + //`request`/`postman-request` exposed its outgoing `Request` object at `response.request`. Its + //library-internal guts (`_auth`, `_form`, `_qs`, `httpModule`, `pool`, ...) can't exist without the + //`request` package, but we reproduce every CONSUMABLE field a caller could portably read. Don't + //clobber it if needle/Node ever populates one. + if (!response.request) { + const outgoingHeaders = this.titleCaseHeaders(response.req?.getHeaders?.()); + response.request = { + uri: uri, + method: response.req?.method ?? undefined, + headers: outgoingHeaders, + host: uri.hostname, + href: uri.href, + path: uri.path, + port: uri.port ?? undefined, + originalHost: uri.hostname, + originalHostHeaderName: 'Host', + protocol: uri.protocol, + readable: true, + writable: true + }; + } + + return response as RequestResponse; + } + + /** + * Wrap a needle stream so that an intermediate `401` `'response'` event (the + * digest challenge that needle answers by retrying) is not propagated to + * listeners. Only the subsequent, authenticated response is forwarded. + */ + private interceptIntermediate401(stream: { emit: (event: string, ...args: any[]) => boolean }) { + const originalEmit = stream.emit.bind(stream); + let swallowedChallenge = false; + stream.emit = ((event: string, ...args: any[]): boolean => { + if (event === 'response' && !swallowedChallenge) { + const resp = args[0]; + if (resp && resp.statusCode === 401) { + //this is the digest challenge; needle will retry. drop it (just this once). + swallowedChallenge = true; + return false; + } + } + return originalEmit(event, ...args); + }) as any; + } + + /** + * Title-case an HTTP header name the way `request`/`postman-request` preserved it on the outgoing + * request (`Content-Type`, `User-Agent`, `WWW-Authenticate`, ...). needle lowercases outgoing header + * names, so we re-case them to maximize parity with what consumers saw on `response.request.headers`. + */ + private titleCaseHeaderName(name: string): string { + //title-case each hyphen-delimited segment (Content-Type, User-Agent, Authorization, ...). This + //covers the outgoing request headers roku-deploy sends; we don't special-case acronym headers + //(WWW-Authenticate etc.) because those are response headers, not outgoing-request headers. + return name.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join('-'); + } + + private titleCaseHeaders(headers: Record | undefined): Record | undefined { + if (!headers || typeof headers !== 'object') { + return headers; + } + const out: Record = {}; + for (const key of Object.keys(headers)) { + out[this.titleCaseHeaderName(key)] = headers[key]; + } + return out; + } +} + +export const request = new Request(); + +/** + * The subset of the legacy `request`/`postman-request` options object that roku-deploy actually + * builds and this shim consumes. We previously typed these as `request.OptionsWithUrl` (from + * `@types/request`), but that pulled a dependency purely for a type and forced `as any` reads for the + * fields the `@types/request` surface didn't cleanly expose. Declaring exactly what we use lets us drop + * `@types/request` entirely and read every field type-safely. + */ +export interface RequestOptions { + + /** The full request url (already includes host/port/path). */ + url: string; + + /** Per-request timeout in ms (connection + first-response-byte). */ + timeout?: number; + + /** Outgoing request headers. */ + headers?: Record; + + /** Digest-auth credentials. `sendImmediately: false` requests the 401-challenge/response dance. */ + auth?: { + user?: string; + username?: string; + pass?: string; + password?: string; + sendImmediately?: boolean; + }; + + /** multipart/form-data fields (string values, or a readable stream for the zip/pkg archive). */ + formData?: Record; + + /** Query-string object appended to the url. */ + qs?: Record; + + /** + * Legacy `request` agent options. Only `keepAlive` is consulted: `request` used + * `{ keepAlive: false }` so each exchange used a fresh socket that closed when done. + */ + agentOptions?: { + keepAlive?: boolean; + }; +} + +/** + * The `response` object roku-deploy (and its consumers) see. Both `postman-request` and `needle` + * hand back a Node `http.IncomingMessage`, so the real object carries far more than the few fields + * roku-deploy reads — `statusCode`, `headers`, `statusMessage`, `rawHeaders`, `httpVersion*`, + * `socket`, `req`, `complete`, etc. We keep all of that (it's needle's actual IncomingMessage) and + * layer on the `request`-compat extras postman added. The interface therefore declares the fields we + * guarantee, and allows the rest of the IncomingMessage surface via the index signature. + */ +export interface RequestResponse { + statusCode: number; + headers: Record; + + /** + * Mirrors `request`'s `response.request` object. roku-deploy reads `response.request.host` when + * constructing the "Unauthorized" error message; other consumers may read `href`/`uri`/`method`. + */ + request: { + host: string; + href: string; + uri?: Record; + method?: string; + headers?: Record; + + /** Plus the other consumable `request` fields we reproduce (path, port, protocol, ...). */ + [key: string]: any; + }; + + /** + * The response body, as a string. `request`/`postman-request` attached the body to + * `response.body` in addition to returning it as the callback's 3rd argument, so we mirror that + * for callers that read `error.results.response.body`. + */ + body: string; + + /** Plus the rest of the underlying http.IncomingMessage surface (statusMessage, rawHeaders, ...). */ + [key: string]: any; +} + +export type RequestCallback = (error: Error | null, response: RequestResponse | undefined, body: string | undefined) => void; diff --git a/src/util.ts b/src/util.ts index 2026128f..b6022c0f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -357,8 +357,8 @@ export function defer() { export interface Deferred { promise: Promise; - tryResolve: (value?: T | PromiseLike) => void; - resolve: (value?: T | PromiseLike) => void; + tryResolve: (value?: PromiseLike | T) => void; + resolve: (value?: PromiseLike | T) => void; tryReject: (reason?: any) => void; reject: (reason?: any) => void; isResolved: boolean;