},
communication: {
+ requestTimeoutMs: 120000,
strictTopicHubLink: true, // If true, deletes topics which do not list us (dingus.selfBaseUrl) as a hub relation.
retryBackoffSeconds: [60, 120, 360, 1440, 7200, 43200, 86400], // failed requests retry according to number of attempts
claimTimeoutSeconds: 600, // how long until an in-progress task is deemed abandoned
"@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0",
"@squeep/logger-json-console": "^2.0.1",
"@squeep/web-linking": "^1.0.8",
- "axios": "^1.4.0",
"feedparser": "^2.2.10",
+ "got": "^13.0.0",
"htmlparser2": "^9.0.0",
"iconv": "^3.0.1"
},
"devDependencies": {
- "eslint": "^8.42.0",
+ "eslint": "^8.43.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^1.7.1",
"mocha-steps": "^1.3.0",
"nyc": "^15.1.0",
"pre-commit": "^1.2.2",
- "sinon": "^15.1.0"
+ "sinon": "^15.2.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@eslint/js": {
- "version": "8.42.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
- "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
+ "version": "8.43.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
+ "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
- "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.14",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
- "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
- "dev": true
- },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
}
},
"node_modules/@sinonjs/fake-timers": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz",
- "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==",
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
"optional": true
},
"node_modules/acorn": {
- "version": "8.8.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
- "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
+ "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
}
},
"node_modules/aproba": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
- "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"node_modules/archy": {
"node": ">=10.0.0"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
- },
- "node_modules/axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
- "dependencies": {
- "follow-redirects": "^1.15.0",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"dev": true
},
"node_modules/browserslist": {
- "version": "4.21.7",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz",
- "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==",
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"dev": true,
"funding": [
{
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001489",
- "electron-to-chromium": "^1.4.411",
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
},
}
},
"node_modules/cacheable-request": {
- "version": "10.2.10",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.10.tgz",
- "integrity": "sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==",
+ "version": "10.2.11",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.11.tgz",
+ "integrity": "sha512-kn0t0oJnlFo1Nzl/AYQzS/oByMtmaqLasFUa7MUMsiTrIHy8TxSkx2KzWCybE3Nuz1F4sJRGnLAfUGsPe47viQ==",
"dependencies": {
"@types/http-cache-semantics": "^4.0.1",
- "get-stream": "^6.0.1",
+ "get-stream": "^7.0.0",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.2",
"mimic-response": "^4.0.0",
"node": ">=14.16"
}
},
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.0.tgz",
+ "integrity": "sha512-ql6FW5b8tgMYvI4UaoxG3EQN3VyZ6VeQpxNBGg5BZ4xD4u+HJeprzhMMA4OCBEGQgSR+m87pstWMpiVW64W8Fw==",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/caching-transform": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001498",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz",
- "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==",
+ "version": "1.0.30001505",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz",
+ "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==",
"dev": true,
"funding": [
{
"color-support": "bin.js"
}
},
- "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/commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"node": ">=10"
}
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.427",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz",
- "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==",
+ "version": "1.4.435",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz",
+ "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==",
"dev": true
},
"node_modules/emoji-regex": {
}
},
"node_modules/eslint": {
- "version": "8.42.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
- "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
+ "version": "8.43.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
+ "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
- "@eslint/js": "8.42.0",
+ "@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
- "node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/foreground-child": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
"node": ">=8.0.0"
}
},
- "node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/form-data-encoder": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
"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/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"node": ">=0.10.0"
}
},
- "node_modules/node-linux-pam/node_modules/aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "optional": true
- },
"node_modules/node-linux-pam/node_modules/are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"node": ">=8"
}
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
- },
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/semver": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
- "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"optional": true,
"dependencies": {
"lru-cache": "^6.0.0"
}
},
"node_modules/sinon": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.0.tgz",
- "integrity": "sha512-cS5FgpDdE9/zx7no8bxROHymSlPLZzq0ChbbLk1DrxBfc+eTeBK3y8nIL+nu/0QeYydhhbLIr7ecHJpywjQaoQ==",
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz",
+ "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0",
- "@sinonjs/fake-timers": "^10.2.0",
+ "@sinonjs/fake-timers": "^10.3.0",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.4",
}
},
"@eslint/js": {
- "version": "8.42.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
- "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
+ "version": "8.43.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
+ "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true
},
"@humanwhocodes/config-array": {
"dev": true
},
"@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@jridgewell/trace-mapping": {
"requires": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
- },
- "dependencies": {
- "@jridgewell/sourcemap-codec": {
- "version": "1.4.14",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
- "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
- "dev": true
- }
}
},
"@mapbox/node-pre-gyp": {
}
},
"@sinonjs/fake-timers": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz",
- "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==",
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dev": true,
"requires": {
"@sinonjs/commons": "^3.0.0"
"optional": true
},
"acorn": {
- "version": "8.8.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
- "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
+ "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true
},
"acorn-jsx": {
}
},
"aproba": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
- "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"archy": {
"integrity": "sha512-5lNGRB5g5i2bGIzb+J1QQE1iKU/WEMVBReFIc5pPDWjcPj23otPL0eI6PB2v7QPi0qU6Mhym5D3y0ZiSIOf3GA==",
"optional": true
},
- "asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
- },
- "axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
- "requires": {
- "follow-redirects": "^1.15.0",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"dev": true
},
"browserslist": {
- "version": "4.21.7",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz",
- "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==",
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"dev": true,
"requires": {
- "caniuse-lite": "^1.0.30001489",
- "electron-to-chromium": "^1.4.411",
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
}
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="
},
"cacheable-request": {
- "version": "10.2.10",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.10.tgz",
- "integrity": "sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==",
+ "version": "10.2.11",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.11.tgz",
+ "integrity": "sha512-kn0t0oJnlFo1Nzl/AYQzS/oByMtmaqLasFUa7MUMsiTrIHy8TxSkx2KzWCybE3Nuz1F4sJRGnLAfUGsPe47viQ==",
"requires": {
"@types/http-cache-semantics": "^4.0.1",
- "get-stream": "^6.0.1",
+ "get-stream": "^7.0.0",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.2",
"mimic-response": "^4.0.0",
"normalize-url": "^8.0.0",
"responselike": "^3.0.0"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.0.tgz",
+ "integrity": "sha512-ql6FW5b8tgMYvI4UaoxG3EQN3VyZ6VeQpxNBGg5BZ4xD4u+HJeprzhMMA4OCBEGQgSR+m87pstWMpiVW64W8Fw=="
+ }
}
},
"caching-transform": {
"devOptional": true
},
"caniuse-lite": {
- "version": "1.0.30001498",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz",
- "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==",
+ "version": "1.0.30001505",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz",
+ "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==",
"dev": true
},
"chalk": {
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"optional": true
},
- "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"
- }
- },
"commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
- },
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
}
},
"electron-to-chromium": {
- "version": "1.4.427",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz",
- "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==",
+ "version": "1.4.435",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz",
+ "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==",
"dev": true
},
"emoji-regex": {
"dev": true
},
"eslint": {
- "version": "8.42.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
- "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
+ "version": "8.43.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
+ "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
- "@eslint/js": "8.42.0",
+ "@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
- "follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
- },
"foreground-child": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
"signal-exit": "^3.0.2"
}
},
- "form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- }
- },
"form-data-encoder": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
"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"
- }
- },
"mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"optional": true
},
- "aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "optional": true
- },
"are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"fromentries": "^1.2.0"
}
},
- "proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
- },
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
- "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"optional": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"sinon": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.0.tgz",
- "integrity": "sha512-cS5FgpDdE9/zx7no8bxROHymSlPLZzq0ChbbLk1DrxBfc+eTeBK3y8nIL+nu/0QeYydhhbLIr7ecHJpywjQaoQ==",
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz",
+ "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==",
"dev": true,
"requires": {
"@sinonjs/commons": "^3.0.0",
- "@sinonjs/fake-timers": "^10.2.0",
+ "@sinonjs/fake-timers": "^10.3.0",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.4",
"@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0",
"@squeep/logger-json-console": "^2.0.1",
"@squeep/web-linking": "^1.0.8",
- "axios": "^1.4.0",
"feedparser": "^2.2.10",
+ "got": "^13.0.0",
"htmlparser2": "^9.0.0",
"iconv": "^3.0.1"
},
"pg-promise": "^11.5.0"
},
"devDependencies": {
- "eslint": "^8.42.0",
+ "eslint": "^8.43.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^1.7.1",
"mocha-steps": "^1.3.0",
"nyc": "^15.1.0",
"pre-commit": "^1.2.2",
- "sinon": "^15.1.0"
+ "sinon": "^15.2.0"
}
}
(async function main () {
let config, logger, db, service;
try {
+ // This provides an async context store for persisting unique log data per request, id est a requestId.
const asyncLocalStorage = new AsyncLocalStorage();
+
config = new Config(process.env.NODE_ENV);
logger = new Logger(config.logger, { nodeId: config.nodeId }, asyncLocalStorage);
db = new DB(logger, config);
http.createServer(async (req, res) => {
await asyncLocalStorage.run({}, async () => {
- await service.dispatch(req, res);
+ return service.dispatch(req, res);
});
}).listen(PORT, ADDR, (err) => {
if (err) {
/**
- * Pick out useful axios response fields.
+ * Pick out useful got response fields.
* @param {*} res
* @returns
*/
-const axiosResponseLogData = (res) => {
+const gotResponseLogData = (res) => {
const data = common.pick(res, [
- 'status',
- 'statusText',
+ 'statusCode',
+ 'statusMessage',
'headers',
- 'elapsedTimeMs',
- 'data',
+ 'body',
+ 'error',
]);
- if (data.data) {
- data.data = logTruncate(data.data, 100);
+ if (typeof res.body === 'string') {
+ data.body = logTruncate(data.body, 100);
+ } else if (res.body instanceof Buffer) {
+ data.body = `<Buffer ${res.body.byteLength} bytes>`;
+ }
+ if (res?.timings?.phases?.total) {
+ data.elapsedTimeMs = res.timings.phases.total;
+ }
+ if (res?.redirectUrls?.length) {
+ data.redirectUrls = res.redirectUrls;
+ }
+ if (res?.retryCount) {
+ data.retryCount = res.retryCount;
}
return data;
};
...common,
arrayChunk,
attemptRetrySeconds,
- axiosResponseLogData,
+ gotResponseLogData,
ensureArray,
freezeDeep,
logTruncate,
* worker which initiates most of them.
*/
-const axios = require('axios');
const common = require('./common');
const crypto = require('crypto');
const Enum = require('./enum');
const LinkHelper = require('./link-helper');
const { version: packageVersion, name: packageName } = require('../package.json'); // For default UA string
-const { performance } = require('perf_hooks');
-
const _fileScope = common.fileScope(__filename);
class Communication {
this.logger.error(_fileScope('constructor'), 'empty dingus.selfBaseUrl value, server responses will not be compliant');
}
- // Set common options
- this.axios = axios.create({
- validateStatus: null, // Non-success responses are not exceptional
- headers: {
- [Enum.Header.UserAgent]: Communication.userAgentString(options.userAgent),
- },
- });
-
- this.axios.interceptors.request.use((request) => {
- request.startTimestampMs = performance.now();
- return request;
- });
- this.axios.interceptors.response.use((response) => {
- response.elapsedTimeMs = performance.now() - response.config.startTimestampMs;
- return response;
- });
+ this.Got = undefined; // Will become the async imported got.
+ this.got = this._init; // First invocation imports got and replaces this.
this.worker = new Worker(logger, db, this.workFeed.bind(this), options);
this.worker.start();
}
+ /**
+ * Do a little dance to cope with ESM dynamic import.
+ * @param {...any} args
+ * @returns {Promise<any>}
+ */
+ async _init(...args) {
+ if (!this.Got) {
+ // For some reason eslint is confused about import being supported here.
+ // eslint-disable-next-line
+ this.Got = await import('got');
+ this.got = this.Got.got.extend({
+ followRedirect: false, // Outgoing API calls should not encounter redirects
+ throwHttpErrors: false, // We will be checking status codes explicitly
+ headers: {
+ [Enum.Header.UserAgent]: Communication.userAgentString(this.options.userAgent),
+ },
+ timeout: {
+ request: this.options.communication.requestTimeoutMs || 120000,
+ },
+ hooks: {
+ beforeRetry: [
+ this._onRetry,
+ ],
+ },
+ });
+ }
+
+ /* istanbul ignore if */
+ if (args.length) {
+ /* istanbul ignore next */
+ return this.got(...args);
+ }
+ }
+
+
+ /**
+ * Take note of transient retries.
+ * @param {*} error
+ * @param {*} retryCount
+ */
+ _onRetry(error, retryCount) {
+ const _scope = _fileScope('_onRetry');
+ this.logger.debug(_scope, 'retry', { retryCount, error });
+ }
+
+
+ /**
+ * Construct a user-agent value.
+ * @param {Object} userAgentConfig
+ * @param {String=} userAgentConfig.product
+ * @param {String=} userAgentConfig.version
+ * @param {String=} userAgentConfig.implementation
+ * @returns {String}
+ */
static userAgentString(userAgentConfig) {
// eslint-disable-next-line security/detect-object-injection
const _conf = (field, def) => (userAgentConfig && field in userAgentConfig) ? userAgentConfig[field] : def;
/**
* Generate a random string.
* @param {Integer} bytes
- * @returns {String}
+ * @returns {Promise<String>}
*/
static async generateChallenge(bytes = 30) {
return (await common.randomBytesAsync(bytes)).toString('base64');
* @returns {String}
*/
static signature(message, secret, algorithm) {
- const hmac = crypto.createHmac(algorithm, secret);
- hmac.update(message);
- return `${algorithm}=${hmac.digest('hex')}`;
+ const hmac = crypto.createHmac(algorithm, secret)
+ .update(message)
+ .digest('hex');
+ return `${algorithm}=${hmac}`;
}
* Generate the hash for content.
* @param {Buffer} content
* @param {String} algorithm
- * @returns
- */
- static contentHash(content, algorithm) {
- const hash = crypto.createHash(algorithm);
- hash.update(content);
- return hash.digest('hex');
- }
-
-
- /**
- * A request skeleton config.
- * @param {String} method
- * @param {String} requestUrl
- * @param {String} body
- * @param {Object} params
- */
- static _axiosConfig(method, requestUrl, body, params = {}, headers = {}) {
- const urlObj = new URL(requestUrl);
- const config = {
- method,
- url: `${urlObj.origin}${urlObj.pathname}`,
- params: urlObj.searchParams,
- headers,
- ...(body && { data: body }),
- // Setting this does not appear to be enough to keep axios from parsing JSON response into object
- responseType: 'text',
- // So force the matter by eliding all response transformations
- transformResponse: [ (res) => res ],
- };
- Object.entries(params).map(([k, v]) => config.params.set(k, v));
- return config;
- }
-
-
- /**
- * Create request config for verifying an intent.
- * @param {URL} requestUrl
- * @param {String} topicUrl
- * @param {String} mode
- * @param {Integer} leaseSeconds
- * @param {String} challenge
- */
- static _intentVerifyAxiosConfig(requestUrl, topicUrl, mode, leaseSeconds, challenge) {
- // Explicitly convert leaseSeconds to string, due to some DB backends. (Looking at you, sqlite..)
- leaseSeconds = leaseSeconds.toString();
-
- return Communication._axiosConfig('GET', requestUrl, undefined, {
- 'hub.mode': mode,
- 'hub.topic': topicUrl,
- 'hub.challenge': challenge,
- 'hub.lease_seconds': leaseSeconds,
- }, {});
- }
-
-
- /**
- * Create request config for denying an intent.
- * @param {String} requestUrl
- * @param {String} topicUrl
- * @param {String} reason
- * @returns {String}
- */
- static _intentDenyAxiosConfig(requestUrl, topicUrl, reason) {
- return Communication._axiosConfig('GET', requestUrl, undefined, {
- 'hub.mode': Enum.Mode.Denied,
- 'hub.topic': topicUrl,
- ...(reason && { 'hub.reason': reason }),
- }, {});
- }
-
-
- /**
- * Create request config for querying publisher for subscription validation.
- * @param {Topic} topic
- * @param {Verification} verification
- * @returns {String}
- */
- static _publisherValidationAxiosConfig(topic, verification) {
- const body = {
- callback: verification.callback,
- topic: topic.url,
- ...(verification.httpFrom && { from: verification.httpFrom }),
- ...(verification.httpRemoteAddr && { address: verification.httpRemoteAddr }),
- };
- return Communication._axiosConfig('POST', topic.publisherValidationUrl, body, {}, {
- [Enum.Header.ContentType]: Enum.ContentType.ApplicationJson,
- });
- }
-
-
- /**
- * Create request config for fetching topic content.
- * Prefer existing content-type, but accept anything.
- * @param {Topic} topic
* @returns {String}
*/
- static _topicFetchAxiosConfig(topic) {
- const acceptWildcard = '*/*' + (topic.contentType ? ';q=0.9' : '');
- const acceptPreferred = [topic.contentType, acceptWildcard].filter((x) => x).join(', ');
- return Communication._axiosConfig('GET', topic.url, undefined, {}, {
- [Enum.Header.Accept]: acceptPreferred,
- ...(topic.httpEtag && { [Enum.Header.IfNoneMatch]: topic.httpEtag }),
- ...(topic.httpLastModified && { [Enum.Header.IfModifiedSince]: topic.httpLastModified }),
- });
+ static contentHash(content, algorithm) {
+ return crypto.createHash(algorithm)
+ .update(content)
+ .digest('hex');
}
}
}
- const u = new URL(verification.callback);
- let callbackRequestConfig, challenge;
+ const callbackRequestConfig = {
+ method: 'GET',
+ url: new URL(verification.callback),
+ responseType: 'text',
+ };
+ const callbackParams = {
+ 'hub.topic': topic.url,
+ 'hub.mode': verification.mode,
+ };
+
+ let challenge;
if (verification.mode === Enum.Mode.Denied) {
- // Denials don't have a challenge.
- callbackRequestConfig = Communication._intentDenyAxiosConfig(u, topic.url, verification.reason);
+ // Denials don't have a challenge, but might have a reason.
+ if (verification.reason) {
+ callbackParams['hub.reason'] = verification.reason;
+ }
} else {
// Subscriptions and unsubscriptions require challenge matching.
challenge = await Communication.generateChallenge();
- callbackRequestConfig = Communication._intentVerifyAxiosConfig(u, topic.url, verification.mode, verification.leaseSeconds, challenge);
+ Object.assign(callbackParams, {
+ 'hub.challenge': challenge,
+ // Explicitly convert leaseSeconds to string, due to some DB backends. (Looking at you, sqlite..)
+ 'hub.lease_seconds': verification.leaseSeconds.toString(),
+ });
}
+ Object.entries(callbackParams)
+ .forEach(([k, v]) => callbackRequestConfig.url.searchParams.set(k, v))
+ ;
const logInfoData = {
- callbackUrl: u.href,
+ callbackUrl: callbackRequestConfig.url.href,
topicUrl: topic.url,
mode: verification.mode,
originalRequestId: verification.requestId,
let response;
try {
- response = await this.axios(callbackRequestConfig);
+ response = await this.got(callbackRequestConfig);
} catch (e) {
this.logger.error(_scope, 'verification request failed', { ...logInfoData, error: e });
await this.db.verificationIncomplete(dbCtx, verificationId, this.options.communication.retryBackoffSeconds);
return;
}
- logInfoData.response = common.axiosResponseLogData(response);
+ logInfoData.response = common.gotResponseLogData(response);
this.logger.debug(_scope, 'verification response', logInfoData );
let verificationAccepted = true; // Presume success.
- switch (common.httpStatusCodeClass(response.status)) {
+ switch (common.httpStatusCodeClass(response.statusCode)) {
case 2:
// Success, fall out of switch.
break;
}
if ([Enum.Mode.Subscribe, Enum.Mode.Unsubscribe].includes(verification.mode)
- && response.data !== challenge) {
+ && response.body !== challenge) {
this.logger.info(_scope, 'verification rejected by challenge', logInfoData);
verificationAccepted = false;
}
case Enum.Mode.Denied:
await this.db.subscriptionDelete(txCtx, verification.callback, verification.topicId);
if (topic.isDeleted) {
- // Remove a deleted topic after he last subscription is notified.
+ // Remove a deleted topic after the last subscription is notified.
await this.db.topicPendingDelete(txCtx, topic.id);
}
break;
* Updates (and persists) verification.
* Returns boolean of status of publisher contact, and hence
* whether to continue verification with client.
+ *
+ * This is not defined by the spec. We opt to speak JSON here.
* @param {*} dbCtx
* @param {TopicData} topic
* @param {VerificationData} verification
*/
async publisherValidate(dbCtx, topic, verification) {
const _scope = _fileScope('publisherValidate');
- const publisherValidationRequestConfig = Communication._publisherValidationAxiosConfig(topic, verification);
const logInfoData = {
topicUrl: topic.url,
callbackUrl: verification.callback,
this.logger.info(_scope, 'publisher validation request', logInfoData);
+ const publisherValidationRequestConfig = {
+ method: 'POST',
+ url: topic.publisherValidationUrl,
+ json: {
+ callback: verification.callback,
+ topic: topic.url,
+ ...(verification.httpFrom && { from: verification.httpFrom }),
+ ...(verification.httpRemoteAddr && { address: verification.httpRemoteAddr }),
+ },
+ responseType: 'json',
+ };
try {
- response = await this.axios(publisherValidationRequestConfig);
+ response = await this.got(publisherValidationRequestConfig);
} catch (e) {
this.logger.error(_scope, 'publisher validation failed', { ...logInfoData, error: e });
return false; // Do not continue with client verification.
}
- logInfoData.response = common.axiosResponseLogData(response);
+ logInfoData.response = common.gotResponseLogData(response);
this.logger.debug(_scope, 'validation response', logInfoData);
let verificationNeedsUpdate = false;
- switch (common.httpStatusCodeClass(response.status)) {
+ switch (common.httpStatusCodeClass(response.statusCode)) {
case 2:
this.logger.info(_scope, 'publisher validation complete, allowed', logInfoData);
break;
return;
}
- const updateRequestConfig = Communication._topicFetchAxiosConfig(topic);
+ const updateRequestConfig = {
+ followRedirect: true,
+ method: 'GET',
+ url: topic.url,
+ headers: {
+ [Enum.Header.Accept]: [topic.contentType, `*/*${topic.contentType ? ';q=0.9' : ''}`].filter((x) => x).join(', '),
+ ...(topic.httpEtag && { [Enum.Header.IfNoneMatch]: topic.httpEtag }),
+ ...(topic.httpLastModified && { [Enum.Header.IfModifiedSince]: topic.httpLastModified }),
+ },
+ responseType: 'buffer',
+ };
this.logger.info(_scope, 'topic update request', logInfoData);
let response;
try {
- response = await this.axios(updateRequestConfig);
+ response = await this.got(updateRequestConfig);
} catch (e) {
this.logger.error(_scope, 'update request failed', logInfoData);
await this.db.topicFetchIncomplete(dbCtx, topicId, this.options.communication.retryBackoffSeconds);
return;
}
- logInfoData.response = common.axiosResponseLogData(response);
+ logInfoData.response = common.gotResponseLogData(response);
this.logger.debug(_scope, 'fetch response', logInfoData);
- switch (common.httpStatusCodeClass(response.status)) {
+ switch (common.httpStatusCodeClass(response.statusCode)) {
case 2:
case 3:
// Fall out of switch on success
return;
}
- if (response.status === 304) {
+ if (response.statusCode === 304) {
this.logger.info(_scope, 'content has not changed, per server', logInfoData);
await this.db.topicFetchComplete(dbCtx, topicId);
return;
}
- const contentHash = Communication.contentHash(response.data, topic.contentHashAlgorithm);
+ const contentHash = Communication.contentHash(response.body, topic.contentHashAlgorithm);
logInfoData.contentHash = contentHash;
if (topic.contentHash === contentHash) {
this.logger.info(_scope, 'content has not changed', logInfoData);
return;
}
- const validHub = await this.linkHelper.validHub(topic.url, response.headers, response.data);
+ const validHub = await this.linkHelper.validHub(topic.url, response.headers, response.body);
if (!validHub) {
this.logger.info(_scope, 'retrieved topic does not list us as hub', { logInfoData });
if (this.options.communication.strictTopicHubLink) {
await this.db.transaction(dbCtx, async (txCtx) => {
await this.db.topicSetContent(txCtx, {
topicId,
- content: Buffer.from(response.data),
+ content: Buffer.from(response.body),
contentHash,
...(contentType && { contentType }),
...(httpETag && { httpETag }),
logInfoData.contentLength = topic.content.length;
logInfoData.contentHash = topic.contentHash;
- const updateAxiosConfig = Communication._axiosConfig('POST', subscription.callback, topic.content, {}, {
- [Enum.Header.Link]: `<${topic.url}>; rel="self"${this.linkHub}`,
- [Enum.Header.ContentType]: topic.contentType || Enum.ContentType.TextPlain,
- ...(subscription.secret && { [Enum.Header.XHubSignature]: Communication.signature(topic.content, subscription.secret, subscription.signatureAlgorithm) }),
- });
+ const updateConfig = {
+ method: 'POST',
+ url: subscription.callback,
+ body: topic.content,
+ headers: {
+ [Enum.Header.Link]: `<${topic.url}>; rel="self"${this.linkHub}`,
+ [Enum.Header.ContentType]: topic.contentType || Enum.ContentType.TextPlain,
+ ...(subscription.secret && { [Enum.Header.XHubSignature]: Communication.signature(topic.content, subscription.secret, subscription.signatureAlgorithm) }),
+ },
+ responseType: 'text',
+ };
this.logger.info(_scope, 'update request', logInfoData);
let response;
try {
- response = await this.axios(updateAxiosConfig);
+ response = await this.got(updateConfig);
} catch (e) {
this.logger.error(_scope, 'update request failed', { ...logInfoData, error: e });
await this.db.subscriptionDeliveryIncomplete(dbCtx, subscription.callback, subscription.topicId, this.options.communication.retryBackoffSeconds);
return;
}
- logInfoData.response = common.axiosResponseLogData(response);
+ logInfoData.response = common.gotResponseLogData(response);
this.logger.debug(_scope, 'update response', logInfoData);
- switch (common.httpStatusCodeClass(response.status)) {
+ switch (common.httpStatusCodeClass(response.statusCode)) {
case 2:
// Fall out of switch on success.
break;
return;
case 4:
- if (response.status === 410) { // GONE
+ if (response.statusCode === 410) { // GONE
this.logger.info(_scope, 'client declined further updates', logInfoData);
await this.db.subscriptionDeliveryGone(dbCtx, subscription.callback, subscription.topicId);
return;
// Log errors
pgpInitOptions.error = (err, event) => {
this.logger.error(_fileScope('pgp:error'), '', { err, event });
+
+ // TODO: close connection on err.code === '57P03' database shutting down
};
// Deophidiate column names in-place, log results
// Parse and validate all the topics in the request.
data.publishTopics = await this._publishTopics(dbCtx, data, requestId);
- if (!data.publishTopics || !data.publishTopics.length) {
+ if (!data?.publishTopics?.length) {
const details = Manager._prettyDetails(['no valid topic urls to publish'], []);
throw new ResponseError(Enum.ErrorResponse.BadRequest, details);
}
this.logger.debug(_scope, 'got topics', { topics: ctx.topics });
// Profile users can only see related topics.
- if (ctx.session && ctx.session.authenticatedProfile) {
+ if (ctx?.session?.authenticatedProfile) {
const profileUrlObj = new URL(ctx.session.authenticatedProfile);
ctx.topics = ctx.topics.filter((topic) => {
const topicUrlObj = new URL(topic.url);
this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions, updates: ctx.publishCount });
// Profile users can only see related topics.
- if (ctx.session && ctx.session.authenticatedProfile) {
+ if (ctx?.session?.authenticatedProfile) {
const profileUrlObj = new URL(ctx.session.authenticatedProfile);
const topicUrlObj = new URL(ctx.topic.url);
if (!Manager._profileControlsTopic(profileUrlObj, topicUrlObj)) {
}
res.end(Template.adminTopicDetailsHTML(ctx, this.options));
- this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic });
+ this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx?.topic?.id || ctx.topic });
}
/**
* Rearrange logging data.
+ * @param {http.ClientRequest} req
+ * @param {http.ServerResponse} res
+ * @param {Object} ctx
*/
async preHandler(req, res, ctx) {
await super.preHandler(req, res, ctx);
const logObject = this.asyncLocalStorage.getStore();
- logObject.requestId = ctx.requestId;
- delete ctx.requestId;
+ // FIXME: for some reason, returning from the super.preHandler loses async context?
+ // Workaround until cause and solution are found.
+ if (logObject) {
+ logObject.requestId = ctx.requestId;
+ delete ctx.requestId;
+ }
}
* Wrapper interface for controlling fake-servers.
*/
-const axios = require('axios');
-
class FakeClient {
constructor(host, subscriberPort, topicPort) {
this.logger = console;
this.host = host;
this.subscriberPort = subscriberPort;
this.topicPort = topicPort;
- this.axios = axios.create({
- validateStatus: (statusCode) => (Math.floor(statusCode / 100)) === 2,
- headers: {
- 'User-Agent': 'FakeClient',
- },
- });
+ this.Got = undefined;
+ this.got = this._init.bind(this);
+ }
+
+ async _init(...args) {
+ if (!this.Got) {
+ // eslint-disable-next-line
+ this.Got = await import('got');
+ this.got = this.Got.got.extend({
+ headers: {
+ 'User-Agent': 'FakeClient',
+ },
+ responseType: 'text',
+ });
+ }
+ if (args.length) {
+ return this.got(...args);
+ }
}
topicUrl(id) {
return `http://${this.host}:${this.subscriberPort}/subscriber/${id}${extra}`;
}
- static _axiosRequestConfig(method, url, params = {}, headers = {}, data) {
- const urlObj = new URL(url);
- const config = {
+ static _requestConfig(method, url, params = {}, headers = {}, body = undefined) {
+ const gotConfig = {
method,
- url: `${urlObj.origin}${urlObj.pathname}`,
- params: urlObj.searchParams,
+ url: new URL(url),
headers,
- ...(data && { data }),
- responseType: 'text',
- transformResponse: [ (res) => res ],
+ ...(body && { body }),
};
- Object.entries(params).map(([k, v]) => config.params.set(k, v));
- return config;
+ Object.entries(params).forEach(([k, v]) => gotConfig.url.searchParams.set(k, v));
+ return gotConfig;
+ }
+
+ static _formData(obj) {
+ return Object.entries(obj)
+ .map((entry) => entry.map(encodeURIComponent).join('='))
+ .join('&')
+ ;
}
async subscribe(hubUrl, subscriberId, topicId, postData = {}) {
const topicUrl = this.topicUrl(topicId);
const subscriberUrl = this.subscriberUrl(subscriberId);
- const data = {
+ const data = FakeClient._formData({
'hub.callback': subscriberUrl,
'hub.mode': 'subscribe',
'hub.topic': topicUrl,
'hub.lease_seconds': 60,
'hub.secret': 'sharedSecret',
...postData,
- };
- const formData = new URLSearchParams(data).toString();
- const headers = {
+ });
+ const config = FakeClient._requestConfig('POST', hubUrl, {}, {
'Content-Type': 'application/x-www-form-urlencoded',
- };
-
+ }, data);
try {
- return this.axios(FakeClient._axiosRequestConfig('POST', hubUrl, {}, headers, formData));
- } catch (e) {
- this.logger.error('subscribe', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('subscribe', error, config);
+ throw error;
}
}
contentType: 'text/plain',
};
const url = this.topicUrl(id);
+ const config = FakeClient._requestConfig('PUT', url, {
+ ...defaultBehavior,
+ ...behavior,
+ });
try {
- return this.axios(FakeClient._axiosRequestConfig('PUT', url, {
- ...defaultBehavior,
- ...behavior,
- }));
- } catch (e) {
- this.logger.error('topicSet', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('topicSet', error, config);
+ throw error;
}
}
* @param {String} id
*/
async topicDelete(id) {
- const url =this.topicUrl(id);
+ const url = this.topicUrl(id);
+ const config = FakeClient._requestConfig('DELETE', url);
try {
- return this.axios(FakeClient._axiosRequestConfig('DELETE', url));
- } catch (e) {
- this.logger.error('topicDelete', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('topicDelete', error, config);
+ throw error;
}
}
matchChallenge: true,
};
const url = this.subscriberUrl(id, '/verify');
+ const config = FakeClient._requestConfig('PUT', url, {
+ ...defaultBehavior,
+ ...behavior,
+ });
try {
- return this.axios(FakeClient._axiosRequestConfig('PUT', url, {
- ...defaultBehavior,
- ...behavior,
- }));
- } catch (e) {
- this.logger.error('subscriberSetVerify', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('subscriberSetVerify', error, config);
+ throw error;
}
}
* @param {Object} behavior
* @param {Number} behavior.statusCode
*/
- async subscriberSetContent(id, behavior = {}) {
- const defaultBehavior = {
- statusCode: 200,
- };
+ async subscriberSetContent(id, behavior = {}) {
+ const defaultBehavior = {
+ statusCode: 200,
+ };
const url = this.subscriberUrl(id, '/content');
+ const config = FakeClient._requestConfig('PUT', url, {
+ ...defaultBehavior,
+ ...behavior,
+ });
try {
- return this.axios(FakeClient._axiosRequestConfig('PUT', url, {
- ...defaultBehavior,
- ...behavior,
- }));
- } catch (e) {
- this.logger.error('subscriberSetContent', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('subscriberSetContent', error, config);
+ throw error;
}
}
*/
async subscriberDelete(id) {
const url = this.subscriberUrl(id);
+ const config = FakeClient._requestConfig('DELETE', url);
try {
- return this.axios(FakeClient._axiosRequestConfig('DELETE', url));
- } catch (e) {
- this.logger.error('subscriberDelete', e);
- throw e;
+ return await this.got(config);
+ } catch (error) {
+ this.logger.error('subscriberDelete', error, config);
+ throw error;
}
}
res.setHeader('Link', behavior.selfLink + (behavior.hubLink ? `, ${behavior.hubLink}` : ''));
res.statusCode = behavior.statusCode;
res.end(behavior.content);
- this.logger.info({ method: req.method, statusCode: res.statusCode, url: req.url });
+ this.logger.info('TopicFake:getId', { method: req.method, statusCode: res.statusCode, url: req.url });
}
async putId(req, res, ctx) {
};
this.topicBehaviors.set(id, behavior);
res.statusCode = 200;
+ this.logger.info('TopicFake:putId', { method: req.method, statusCode: res.statusCode, url: req.url });
res.end();
}
this.setResponseType(this.responseTypes, req, res, ctx);
this.topicBehaviors.delete(ctx.params.id);
res.statusCode = 200;
+ this.logger.info('TopicFake:deleteId', { method: req.method, statusCode: res.statusCode, url: req.url });
res.end();
}
this.setResponseType(this.responseTypes, req, res, ctx);
const behavior = this.verifyBehaviors.get(ctx.params.id);
res.statusCode = behavior ? behavior.statusCode : 404;
- const response = (behavior && behavior.matchChallenge) ? ctx.queryParams['hub.challenge'] : (behavior && behavior.response);
+ const response = (behavior?.matchChallenge) ? ctx.queryParams['hub.challenge'] : (behavior?.response);
res.end(response);
- this.logger.info({ method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior && behavior.matchChallenge), url: req.url });
+ this.logger.info('SubscriberFake:getId', { method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior?.matchChallenge), url: req.url });
}
async postId(req, res, ctx) {
behavior.content = ctx.rawBody;
}
res.end();
- this.logger.info({ content: behavior && behavior.content, method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior && behavior.matchChallenge), url: req.url });
+ this.logger.info('SubscriberFake:postId', { content: behavior?.content, method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior?.matchChallenge), url: req.url });
}
async putVerify(req, res, ctx) {
}
res.statusCode = 200;
res.end();
+ this.logger.info('SubscriberFake:putVerify', { method: req.method, statusCode: res.statusCode, url: req.url });
}
async putContent(req, res, ctx) {
this.contentBehaviors.set(ctx.params.id, behavior);
res.statusCode = 200;
res.end();
+ this.logger.info('SubscriberFake:putContent', { method: req.method, statusCode: res.statusCode, url: req.url });
}
async deleteId(req, res, ctx) {
this.verifyBehaviors.delete(ctx.params.id);
res.statusCode = 200;
res.end();
+ this.logger.info('SubscriberFake:deleteId', { method: req.method, statusCode: res.statusCode, url: req.url });
}
} // SubscriberFake
'use strict';
-const assert = require('assert');
-const uuid = require('uuid');
+const crypto = require('crypto');
const FakeServersClient = require('./fake-servers-client');
const subscriberPort = process.env.FAKE_SUBSCRIBER_PORT || 9876;
const client = new FakeServersClient(listenAddress, subscriberPort, topicPort);
async function newTopic() {
- const id = uuid.v4();
+ const id = crypto.randomUUID();
await client.topicSet(id, { hubUrl });
console.log('created fake topic', id);
return id;
}
async function newSubscriber() {
- const id = uuid.v4();
+ const id = crypto.randomUUID();
await client.subscriberSetVerify(id);
console.log('created fake subscriber', id);
return id;
'use strict';
-const assert = require('assert');
-const uuid = require('uuid');
+const crypto = require('crypto');
const FakeServersClient = require('./fake-servers-client');
const subscriberPort = process.env.FAKE_SUBSCRIBER_PORT || 9876;
const client = new FakeServersClient(listenAddress, subscriberPort, topicPort);
async function newTopic() {
- const id = uuid.v4();
+ const id = crypto.randomUUID();
await client.topicSet(id, { hubUrl });
console.log('created fake topic', id);
return id;
}
async function newSubscriber() {
- const id = uuid.v4();
+ const id = crypto.randomUUID();
await client.subscriberSetVerify(id);
console.log('created fake subscriber', id);
return id;
const subscriberId = await newSubscriber();
const result = await client.subscribe(hubUrl, subscriberId, topicId);
- console.log('subscribed', { status: result.status, headers: result.headers, data: result.data });
+ console.log('subscribed', { status: result.statusCode, headers: result.headers, body: result.body });
console.log('done');
})().catch((e) => {
});
}); // freezeDeep
- describe('axiosResponseLogData', function () {
+ describe('gotResponseLogData', function () {
it('covers', function () {
const response = {
- status: 200,
- statusText: 'OK',
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'Content-Type': 'text/plain',
},
otherData: 'blah',
- data: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green Meadows," said Old Mother West Wind, “and there I saw the Best Thing in the World.”',
+ body: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green Meadows," said Old Mother West Wind, “and there I saw the Best Thing in the World.”',
+ timings: {
+ phases: {
+ total: 87,
+ },
+ },
+ retryCount: 2,
+ redirectUrls: ['https://example.com/clip/Thornton_Burgess'],
};
const expected = {
- status: 200,
- statusText: 'OK',
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'Content-Type': 'text/plain',
},
- data: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green... (184 bytes)',
+ body: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green... (184 bytes)',
+ elapsedTimeMs: 87,
+ retryCount: 2,
+ redirectUrls: ['https://example.com/clip/Thornton_Burgess'],
+ };
+ const result = common.gotResponseLogData(response);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers buffer data', function () {
+ const response = {
+ statusCode: 200,
+ statusMessage: 'OK',
+ body: Buffer.from('Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green Meadows," said Old Mother West Wind, “and there I saw the Best Thing in the World.”'),
+ };
+ const expected = {
+ statusCode: 200,
+ statusMessage: 'OK',
+ body: '<Buffer 188 bytes>',
};
- const result = common.axiosResponseLogData(response);
+ const result = common.gotResponseLogData(response);
assert.deepStrictEqual(result, expected);
});
it('covers no data', function () {
const response = {
- status: 200,
- statusText: 'OK',
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'Content-Type': 'text/plain',
},
};
const expected = {
- status: 200,
- statusText: 'OK',
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'Content-Type': 'text/plain',
},
};
- const result = common.axiosResponseLogData(response);
+ const result = common.gotResponseLogData(response);
assert.deepStrictEqual(result, expected);
});
- }); // axiosResponseLogData
+ }); // gotResponseLogData
describe('topicLeaseDefaults', function () {
it('supplies necessary properties', function () {
communication = new Communication(stubLogger, stubDb, options);
});
- describe('Axios timing coverage', function () {
- const request = {};
- const response = {
- config: request,
- };
- it('tags request', function () {
- communication.axios.interceptors.request.handlers[0].fulfilled(request);
- assert(request.startTimestampMs);
+ describe('_init', function () {
+ it('covers', async function () {
+ await communication._init();
+ await communication._init();
+ assert(communication.Got);
+ assert(communication.got);
});
- it('tags response', function () {
- communication.axios.interceptors.response.handlers[0].fulfilled(response);
- assert(response.elapsedTimeMs);
+ });
+
+ describe('_onRetry', function () {
+ it('covers', function () {
+ const error = {};
+ const retryCount = 1;
+ communication._onRetry(error, retryCount);
+ assert(communication.logger.debug.called);
});
- }); // Axios timing coverage
+ });
describe('userAgentString', function () {
it('has default behavior', function () {
it('hashes', function () {
const result = Communication.contentHash(content, algorithm);
assert.strictEqual(result, expected);
- })
- });
-
- describe('Axios Configurations', function () {
- let requestUrl, expectedUrl, topicUrl;
- beforeEach(function () {
- requestUrl = 'https://example.com/callback/?id=123';
- expectedUrl = 'https://example.com/callback/';
- topicUrl = 'http://example.com/blog/';
- });
- it('_axiosConfig', function () {
- const method = 'GET';
- const contentType = 'text/plain';
- const body = undefined;
- const params = {
- 'extra_parameter': 'foobar',
- };
- const expectedUrlObj = new URL('https://example.com/callback/?id=123&extra_parameter=foobar');
- const expected = {
- method,
- url: 'https://example.com/callback/',
- headers: {
- 'Content-Type': 'text/plain',
- },
- params: expectedUrlObj.searchParams,
- responseType: 'text',
- };
- const result = Communication._axiosConfig(method, requestUrl, body, params, {
- 'Content-Type': contentType,
- });
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
- });
- it('_axiosConfig covers defaults', function () {
- const method = 'OPTIONS';
- const expectedUrlObj = new URL(requestUrl);
- const expected = {
- method,
- url: expectedUrl,
- headers: {},
- params: expectedUrlObj.searchParams,
- responseType: 'text',
- };
- const result = Communication._axiosConfig(method, requestUrl);
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
- });
- it('covers null response transform', function () {
- const result = Communication._axiosConfig('GET', 'https://example.com/', undefined, {}, {});
- result.transformResponse[0]();
- });
- it('_intentVerifyAxiosConfig', function () {
- const mode = 'subscribe';
- const leaseSeconds = 864000;
- const challenge = 'abcxyz';
- const expectedUrlObj = new URL(`${requestUrl}&hub.mode=${mode}&hub.topic=${encodeURIComponent(topicUrl)}&hub.challenge=${challenge}&hub.lease_seconds=${leaseSeconds}`);
- const expected = {
- method: 'GET',
- url: expectedUrl,
- headers: {},
- params: expectedUrlObj.searchParams,
- responseType: 'text',
- };
- const result = Communication._intentVerifyAxiosConfig(requestUrl, topicUrl, mode, leaseSeconds, challenge);
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
- });
- it('_intentDenyAxiosConfig', function () {
- const reason = 'something';
- const expectedUrlObj = new URL(`${requestUrl}&hub.mode=denied&hub.topic=${encodeURIComponent(topicUrl)}&hub.reason=${reason}`);
- const expected = {
- method: 'GET',
- url: expectedUrl,
- headers: {},
- params: expectedUrlObj.searchParams,
- responseType: 'text',
- };
- const result = Communication._intentDenyAxiosConfig(requestUrl, topicUrl, reason);
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
- });
- it('_publisherValidationAxiosConfig', function () {
- const topic = {
- url: topicUrl,
- publisherValidationUrl: 'https://example.com/publisher/',
- };
- const verification = {
- callback: requestUrl,
- topic: topicUrl,
- };
- const expectedUrlObj = new URL(topic.publisherValidationUrl);
- const expected = {
- method: 'POST',
- url: topic.publisherValidationUrl,
- data: {
- callback: requestUrl,
- topic: topicUrl,
- },
- headers: {
- 'Content-Type': 'application/json',
- },
- params: expectedUrlObj.searchParams,
- responseType: 'text',
- };
- const result = Communication._publisherValidationAxiosConfig(topic, verification);
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
- });
- it('_topicFetchAxiosConfig', function () {
- const topic = {
- url: topicUrl,
- contentType: 'text/plain',
- };
- const expectedUrlObj = new URL(topicUrl);
- const expected = {
- method: 'GET',
- url: topicUrl,
- params: expectedUrlObj.searchParams,
- headers: {
- Accept: 'text/plain, */*;q=0.9',
- },
- responseType: 'text',
- };
- const result = Communication._topicFetchAxiosConfig(topic);
- delete result.transformResponse;
- assert.deepStrictEqual(result, expected);
});
- }); // Axios Configurations
+ });
describe('verificationProcess', function () {
const challenge = 'a_challenge';
sinon.stub(Communication, 'generateChallenge').resolves(challenge);
sinon.stub(communication, 'publisherValidate').resolves(true);
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
},
- data: challenge,
+ body: challenge,
});
communication.db.verificationGetById.resolves(verification);
await communication.verificationProcess(dbCtx, callback, topicId, requestId);
assert(communication.db.verificationRelease.called);
- assert(!communication.axios.called);
+ assert(!communication.got.called);
});
it('denies subscription to deleted topic', async function () {
});
it('handles request error', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').throws(new Error());
+ communication.got.restore();
+ sinon.stub(communication, 'got').rejects(new Error());
await communication.verificationProcess(dbCtx, callback, topicId, requestId);
});
it('handles 500 response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 500,
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 500,
});
await communication.verificationProcess(dbCtx, callback, topicId, requestId);
});
it('handles non-200 response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 400,
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 400,
});
await communication.verificationProcess(dbCtx, callback, topicId, requestId);
communication.db.verificationGetById.restore();
verification.mode = 'unsubscribe';
sinon.stub(communication.db, 'verificationGetById').resolves(verification);
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
},
- data: 'not the challenge',
+ body: 'not the challenge',
});
await communication.verificationProcess(dbCtx, callback, topicId, requestId);
httpRemoteAddr: '127.0.0.0',
};
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'application/json',
},
});
it('succeeds with rejection', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 400,
- statusText: 'Bad Request',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 400,
+ statusMessage: 'Bad Request',
headers: {
'content-type': 'application/json',
},
});
it('defers on request server error', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 502,
- statusText: 'Bad Gateway',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 502,
+ statusMessage: 'Bad Gateway',
headers: {
'content-type': 'text/plain',
},
});
it('handles request error', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').throws(new Error());
+ communication.got.restore();
+ sinon.stub(communication, 'got').rejects(new Error());
const result = await communication.publisherValidate(dbCtx, topic, verification);
requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
link: '<https://example.com/hub/>; rel="hub"',
'last-modified': 'Thu, 18 Nov 2021 20:34:35 GMT',
'etag': '"9c104-1673e-5d1161636d742"',
},
- data: 'Jackdaws love my big sphinx of quartz.',
+ body: 'Jackdaws love my big sphinx of quartz.',
});
communication.db.topicGetById.resolves(topic);
await communication.topicFetchProcess(dbCtx, topicId, requestId);
- assert(!communication.axios.called);
+ assert(!communication.got.called);
});
it('handles request error', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').throws(new Error());
+ communication.got.restore();
+ sinon.stub(communication, 'got').rejects(new Error());
await communication.topicFetchProcess(dbCtx, topicId, requestId);
});
it('handles 500 response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 500,
- statusText: 'Internal Server Error',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 500,
+ statusMessage: 'Internal Server Error',
headers: {
'content-type': 'text/plain',
},
});
it('handles bad response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 404,
- statusText: 'Not Found',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 404,
+ statusMessage: 'Not Found',
headers: {
'content-type': 'text/plain',
},
topic.httpLastModified = 'Thu, 18 Nov 2021 20:34:35 GMT';
topic.httpEtag = '"9c104-1673e-5d1161636d742"';
communication.db.topicGetById.resolves(topic);
- communication.axios.resolves({
- status: 304,
+ communication.got.resolves({
+ statusCode: 304,
});
await communication.topicFetchProcess(dbCtx, topicId, requestId);
});
it('updates content with lax link enforcement', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
link: '<https://example.com/other/hub/>; rel="hub"',
},
- data: 'Jackdaws love my big sphinx of quartz.',
+ body: 'Jackdaws love my big sphinx of quartz.',
});
communication.options.communication.strictTopicHubLink = false;
});
it('deletes topic when hub relation unsatisfied', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
link: '<https://example.com/other/hub/>; rel="hub"',
},
- data: 'Jackdaws love my big sphinx of quartz.',
+ body: 'Jackdaws love my big sphinx of quartz.',
});
await communication.topicFetchProcess(dbCtx, topicId, requestId);
signatureAlgorithm: 'sha512',
};
- sinon.stub(communication, 'axios').resolves({
- status: 200,
- statusText: 'OK',
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 200,
+ statusMessage: 'OK',
headers: {
'content-type': 'text/plain',
},
- data: 'Jackdaws love my big sphinx of quartz.',
+ body: 'Jackdaws love my big sphinx of quartz.',
});
communication.db.topicGetContentById.resolves(topic);
});
it('handles request error', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').throws();
+ communication.got.restore();
+ sinon.stub(communication, 'got').throws();
await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
});
it('handles 5xx response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 500,
- statusText: 'Internal Server Error',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 500,
+ statusMessage: 'Internal Server Error',
headers: {
'content-type': 'text/plain',
},
});
it('handles 4xx response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 404,
- statusText: 'Not Found',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 404,
+ statusMessage: 'Not Found',
headers: {
'content-type': 'text/plain',
},
});
it('handles 410 response', async function () {
- communication.axios.restore();
- sinon.stub(communication, 'axios').resolves({
- status: 410,
- statusText: 'Gone',
+ communication.got.restore();
+ sinon.stub(communication, 'got').resolves({
+ statusCode: 410,
+ statusMessage: 'Gone',
headers: {
'content-type': 'text/plain',
},