// Provide default values for all configuration.
-const packageName = require('../package.json').name;
+const { name: packageName, version: packageVersion } = require('../package.json');
const common = require('../src/common');
+const Enum = require('../src/enum');
const defaultOptions = {
// Uniquely identify this instance, used to tag work-in-progress.
manager: {
pageTitle: packageName, // title on html pages
+ logoUrl: '/static/logo.svg', // image to go with title
footerEntries: [ // common footers on all html pages
'<a href="https://git.squeep.com/?p=websub-hub;a=tree">Development Repository</a> / <a href="https://github.com/thylacine/websub-hub/">GitHub mirror</a>',
'<span class="copyright">©<time datetime="2021">ⅯⅯⅩⅩⅠ</time></span>',
claimTimeoutSeconds: 600, // how long until an in-progress task is deemed abandoned
},
- // Outgoing request UA header. Comments are defaults in code.
+ // Outgoing request UA header.
+ // These values are the same as the defaults in the code, but we are setting
+ // them here so they also apply to UA of other modules, e.g. @squeep/indieauth-helper
userAgent: {
- // product: packageName,
- // version: packageVersion,
- // implementation: Enum.Specification,
+ product: packageName,
+ version: packageVersion,
+ implementation: Enum.Specification,
},
authenticator: {
basicRealm: packageName, // Realm prompt for login on administration pages
secureAuthOnly: true, // Require secure transport for authentication.
- authnEnabled: ['argon2', 'pam'],
+ authnEnabled: ['indieAuth', 'argon2', 'pam'],
forbiddenPAMIdentifiers: ['root'],
},
};
-module.exports = defaultOptions;
\ No newline at end of file
+module.exports = defaultOptions;
"dev": true
},
"@mapbox/node-pre-gyp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
- "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz",
+ "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==",
"optional": true,
"requires": {
"detect-libc": "^1.0.3",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.5",
"nopt": "^5.0.0",
- "npmlog": "^4.1.2",
+ "npmlog": "^5.0.1",
"rimraf": "^3.0.2",
- "semver": "^7.3.4",
- "tar": "^6.1.0"
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
}
},
"@phc/format": {
"dev": true
},
"@squeep/api-dingus": {
- "version": "git+https://git.squeep.com/squeep-api-dingus/#47f38ca4c67e902ccef0b7114a0d144f476258bd",
- "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.2",
+ "version": "git+https://git.squeep.com/squeep-api-dingus/#a1b8b75e64c862276563e874c11a0228ac681946",
+ "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
"requires": {
- "mime-db": "^1.50.0",
+ "mime-db": "^1.51.0",
"uuid": "^8.3.2"
},
"dependencies": {
}
}
},
+ "@squeep/authentication-module": {
+ "version": "git+https://git.squeep.com/squeep-authentication-module/#ecf9c52f00dae3b9eee8316a1cffbc496f0fbd67",
+ "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.0",
+ "requires": {
+ "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
+ "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
+ "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.2",
+ "argon2": "^0.28.3",
+ "node-linux-pam": "^0.2.1"
+ }
+ },
+ "@squeep/html-template-helper": {
+ "version": "git+https://git.squeep.com/squeep-html-template-helper#d3f76b9e76b8f133e8158c1087bb01b32c38d9bb",
+ "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1"
+ },
"@squeep/indieauth-helper": {
- "version": "git+https://git.squeep.com/squeep-indieauth-helper/#ea59a4bf7a596b502aacf06baee26518d86fd312",
- "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.0",
+ "version": "git+https://git.squeep.com/squeep-indieauth-helper/#f8f7fc8165426471f3d290f10cda2ea2cdb7f643",
+ "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
"requires": {
- "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.0",
- "axios": "^0.23.0",
+ "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2",
+ "axios": "^0.24.0",
"iconv": "^3.0.1",
"microformats-parser": "^1.4.0"
- },
- "dependencies": {
- "@squeep/web-linking": {
- "version": "git+https://git.squeep.com/squeep-web-linking/#3436c07c25324507228f3d538d345ea35751c623",
- "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.0"
- },
- "axios": {
- "version": "0.23.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
- "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
- "requires": {
- "follow-redirects": "^1.14.4"
- }
- }
}
},
"@squeep/mystery-box": {
- "version": "git+https://git.squeep.com/squeep-mystery-box/#8df7723f7bbd9ad239bd5a3f66d6e9a8cd3c8100",
- "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.1"
+ "version": "git+https://git.squeep.com/squeep-mystery-box/#c6559eefb2fa776d6e4eedb876470790a53339c1",
+ "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.2"
},
"@squeep/web-linking": {
- "version": "git+https://git.squeep.com/squeep-web-linking/#0728e2b9347826ed0be7f9a7939bf5d61e181204",
- "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.1"
+ "version": "git+https://git.squeep.com/squeep-web-linking/#e0d9ba95109a2bbb61dbdce7a582f5579aa77c5c",
+ "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2"
},
"@ungap/promise-all-settled": {
"version": "1.1.2",
}
},
"aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "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==",
"optional": true
},
"archy": {
"dev": true
},
"are-we-there-yet": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"optional": true,
"requires": {
"delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
- "argon2": {
- "version": "0.28.3",
- "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
- "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
- "optional": true,
- "requires": {
- "@mapbox/node-pre-gyp": "^1.0.7",
- "@phc/format": "^1.0.0",
- "node-addon-api": "^4.2.0",
- "opencollective-postinstall": "^2.0.3"
+ "readable-stream": "^3.6.0"
},
"dependencies": {
- "@mapbox/node-pre-gyp": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.7.tgz",
- "integrity": "sha512-PplSvl4pJ5N3BkVjAdDzpPhVUPdC73JgttkR+LnBx2OORC1GCQsBjUeEuipf9uOaAM1SbxcdZFfR3KDTKm2S0A==",
- "optional": true,
- "requires": {
- "detect-libc": "^1.0.3",
- "https-proxy-agent": "^5.0.0",
- "make-dir": "^3.1.0",
- "node-fetch": "^2.6.5",
- "nopt": "^5.0.0",
- "npmlog": "^6.0.0",
- "rimraf": "^3.0.2",
- "semver": "^7.3.5",
- "tar": "^6.1.11"
- }
- },
- "are-we-there-yet": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
- "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
- "optional": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^3.6.0"
- }
- },
- "gauge": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz",
- "integrity": "sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==",
- "optional": true,
- "requires": {
- "ansi-regex": "^5.0.1",
- "aproba": "^1.0.3 || ^2.0.0",
- "color-support": "^1.1.2",
- "console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.1",
- "signal-exit": "^3.0.0",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1",
- "wide-align": "^1.1.2"
- }
- },
- "node-fetch": {
- "version": "2.6.6",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
- "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
- "optional": true,
- "requires": {
- "whatwg-url": "^5.0.0"
- }
- },
- "npmlog": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.0.tgz",
- "integrity": "sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==",
- "optional": true,
- "requires": {
- "are-we-there-yet": "^2.0.0",
- "console-control-strings": "^1.1.0",
- "gauge": "^4.0.0",
- "set-blocking": "^2.0.0"
- }
- },
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
- },
- "string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "optional": true,
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- }
- },
- "strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "optional": true,
- "requires": {
- "ansi-regex": "^5.0.1"
- }
}
}
},
+ "argon2": {
+ "version": "0.28.3",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
+ "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
+ "optional": true,
+ "requires": {
+ "@mapbox/node-pre-gyp": "^1.0.7",
+ "@phc/format": "^1.0.0",
+ "node-addon-api": "^4.2.0",
+ "opencollective-postinstall": "^2.0.3"
+ }
+ },
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"optional": true
},
"better-sqlite3": {
- "version": "7.4.5",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.5.tgz",
- "integrity": "sha512-mybC3dgrtJeHkIRGP36tST7wjBlIMgTRAXhhO4bMpPZ17EG23FZxZeFcwKWy6o8mV1SKQFnQNyeAZlQpGrgheQ==",
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.6.tgz",
+ "integrity": "sha512-LB/UxnMhcJY12bRCDXl2jTk0lsbXHCHOLn3cPjGhy3GCcVPGq45sCGJPUdfBZnfXGN14tYTJyq0ztUI3lGng8A==",
"optional": true,
"requires": {
"bindings": "^1.5.0",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
+ "camel-case": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+ "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+ "dev": true,
+ "requires": {
+ "no-case": "^2.2.0",
+ "upper-case": "^1.1.1"
+ }
+ },
"camelcase": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
}
},
"chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"optional": true
},
+ "clean-css": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz",
+ "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
+ "commander": {
+ "version": "2.17.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+ "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+ "dev": true
+ },
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"dev": true
},
"gauge": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
- "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"optional": true,
"requires": {
- "aproba": "^1.0.3",
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.0",
- "object-assign": "^4.1.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1",
- "wide-align": "^1.1.0"
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
},
"dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "optional": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "optional": true,
- "requires": {
- "number-is-nan": "^1.0.0"
- }
- },
"string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"optional": true,
"requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
}
},
"strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"optional": true,
"requires": {
- "ansi-regex": "^2.0.0"
+ "ansi-regex": "^5.0.1"
}
}
}
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
+ "html-minifier": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz",
+ "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==",
+ "dev": true,
+ "requires": {
+ "camel-case": "3.0.x",
+ "clean-css": "4.2.x",
+ "commander": "2.17.x",
+ "he": "1.2.x",
+ "param-case": "2.1.x",
+ "relateurl": "0.2.x",
+ "uglify-js": "3.4.x"
+ }
+ },
+ "html-minifier-lint": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-lint/-/html-minifier-lint-2.0.0.tgz",
+ "integrity": "sha1-i0vuyTHaiNsEyHc+5WgYjfO111g=",
+ "dev": true,
+ "requires": {
+ "html-minifier": "3.x"
+ }
+ },
"htmlparser2": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz",
"is-unicode-supported": "^0.1.0"
}
},
+ "lower-case": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+ "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+ "dev": true
+ },
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
}
},
"mime-db": {
- "version": "1.50.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
- "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A=="
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
},
"mimic-response": {
"version": "3.1.0",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
- "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
+ "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
}
},
+ "no-case": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+ "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+ "dev": true,
+ "requires": {
+ "lower-case": "^1.1.1"
+ }
+ },
"node-abi": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.5.0.tgz",
"optional": true
},
"node-fetch": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
- "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
- "optional": true
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
+ "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
+ "optional": true,
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
},
"node-linux-pam": {
"version": "0.2.1",
"yargs": "15.4.1"
},
"dependencies": {
+ "@mapbox/node-pre-gyp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
+ "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.3",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.1",
+ "nopt": "^5.0.0",
+ "npmlog": "^4.1.2",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.4",
+ "tar": "^6.1.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "optional": true
+ },
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"color-convert": "^2.0.1"
}
},
+ "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",
+ "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "optional": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "optional": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
}
},
"color-convert": {
"path-exists": "^4.0.0"
}
},
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==",
"optional": true
},
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"p-limit": "^2.2.0"
}
},
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "optional": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "optional": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
}
},
"y18n": {
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "optional": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "optional": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
}
},
"yargs-parser": {
"dev": true
},
"npmlog": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
- "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"optional": true,
"requires": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
}
},
"number-is-nan": {
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"optional": true
},
+ "param-case": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+ "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+ "dev": true,
+ "requires": {
+ "no-case": "^2.2.0"
+ }
+ },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "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",
+ "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
}
},
"prelude-ls": {
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"dev": true
},
+ "relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+ "dev": true
+ },
"release-zalgo": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
- },
- "dependencies": {
- "chownr": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
- "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
- "optional": true
- }
}
},
"tar-fs": {
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
+ },
+ "dependencies": {
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "optional": true
+ }
}
},
"tar-stream": {
"is-typedarray": "^1.0.0"
}
},
+ "uglify-js": {
+ "version": "3.4.10",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
+ "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==",
+ "dev": true,
+ "requires": {
+ "commander": "~2.19.0",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
+ "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "upper-case": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+ "dev": true
+ },
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"optional": true,
"requires": {
- "string-width": "^1.0.2 || 2"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "optional": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "optional": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "optional": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- }
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "optional": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
+ "string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"word-wrap": {
"coverage-check"
],
"dependencies": {
- "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.2",
- "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.0",
- "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.1",
- "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.1",
+ "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
+ "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.0",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
+ "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
+ "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2",
"axios": "^0.24.0",
"feedparser": "^2.2.10",
"htmlparser2": "^7.2.0",
"iconv": "^3.0.1"
},
"optionalDependencies": {
- "argon2": "^0.28.3",
- "better-sqlite3": "^7.4.5",
- "node-linux-pam": "^0.2.1",
+ "better-sqlite3": "^7.4.6",
"pg-promise": "^10.11.1"
},
"devDependencies": {
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-sonarjs": "^0.11.0",
+ "html-minifier-lint": "^2.0.0",
"mocha": "^9.1.3",
"mocha-steps": "^1.3.0",
"nyc": "^15.1.0",
+++ /dev/null
-'use strict';
-
-const common = require('./common');
-const Enum = require('./enum');
-const Errors = require('./errors');
-const { MysteryBox } = require('@squeep/mystery-box');
-
-const _fileScope = common.fileScope(__filename);
-
-class Authenticator {
- /**
- * @param {Console} logger
- * @param {*} db
- * @param {Object} options
- * @param {Object} options.authenticator
- * @param {String} options.authenticator.basicRealm
- * @param {Boolean} options.authenticator.secureAuthOnly
- * @param {String[]} options.authenticator.forbiddenPAMIdentifiers
- * @param {String[]} options.authenticator.authnEnabled
- */
- constructor(logger, db, options) {
- this.logger = logger;
- this.db = db;
- this.basicRealm = options.authenticator.basicRealm;
- this.secureAuthOnly = options.authenticator.secureAuthOnly;
-
- this.authn = {
- DEBUG_ANY: {},
- };
- try {
- this.authn.argon2 = require('argon2');
- } catch (e) { /**/ }
- try {
- this.authn.pam = require('node-linux-pam');
- this.forbiddenPAMIdentifiers = options.authenticator.forbiddenPAMIdentifiers;
- } catch (e) { /**/ }
-
- this.authnEnabled = Object.keys(this.authn).filter((auth) => options.authenticator.authnEnabled.includes(auth));
- this.logger.debug(_fileScope('constructor'), 'available mechanisms', { authn: this.authnEnabled });
-
- if (this.authnEnabled.length === 0) {
- throw new Error('no authentication mechanisms available');
- }
-
- this.mysteryBox = new MysteryBox(logger, options);
- }
-
-
- /**
- * Check for valid Basic auth, updates ctx with identifier if valid.
- * @param {String} credentials
- * @param {Object} ctx
- * @returns {Boolean}
- */
- async isValidBasic(credentials, ctx) {
- const _scope = _fileScope('isValidBasic');
- this.logger.debug(_scope, 'called', { ctx });
-
- const [identifier, credential] = common.splitFirst(credentials, ':', '');
-
- return this.isValidIdentifierCredential(identifier, credential, ctx);
- }
-
-
- /**
- * Check local auth entries.
- * @param {String} identifier
- * @param {String} credential
- * @param {Object} ctx
- */
- async isValidIdentifierCredential(identifier, credential, ctx) {
- const _scope = _fileScope('isValidIdentifierCredential');
- this.logger.debug(_scope, 'called', { identifier, credential: '*'.repeat(credential.length), ctx });
-
- let isValid = false;
-
- await this.db.context(async (dbCtx) => {
- const authData = await this.db.authenticationGet(dbCtx, identifier);
- if (!authData) {
- this.logger.debug(_scope, 'failed, invalid identifier', { ctx, identifier });
- } else {
- if (authData.credential.startsWith('$argon2')
- && this.authnEnabled.includes('argon2')) {
- isValid = await this.authn.argon2.verify(authData.credential, credential);
- } else if (authData.credential.startsWith('$PAM$')
- && this.authnEnabled.includes('pam')) {
- isValid = this._isValidPAMIdentifier(identifier, credential);
- } else {
- this.logger.error(_scope, 'failed, unknown type of stored credential', { identifier, ctx });
- }
- }
-
- if (this.authnEnabled.includes('DEBUG_ANY')) {
- isValid = true;
- }
-
- if (isValid) {
- ctx.authenticationId = identifier;
- await this.db.authenticationSuccess(dbCtx, identifier);
- }
- }); // dbCtx
-
- return isValid;
- }
-
-
- /**
- * Check system PAM.
- * @param {String} identifier
- * @param {String} credential
- * @returns {Boolean}
- */
- async _isValidPAMIdentifier(identifier, credential) {
- const _scope = _fileScope('_isValidPAMIdentifier');
- let isValid = false;
- if (this.forbiddenPAMIdentifiers.includes(identifier)) {
- return false;
- }
- try {
- await this.authn.pam.pamAuthenticatePromise({ username: identifier, password: credential });
- isValid = true;
- } catch (e) {
- this.logger.debug(_scope, 'failed', { error: e });
- if (!(e instanceof this.authn.pam.PamError)) {
- throw e;
- }
- }
- return isValid;
- }
-
-
- /**
- * Determine which Authorization header is available, and if it is valid.
- * @param {String} authorizationHeader
- * @param {Object} ctx
- */
- async isValidAuthorization(authorizationHeader, ctx) {
- const _scope = _fileScope('isValidAuthorization');
- this.logger.debug(_scope, 'called', { authorizationHeader, ctx });
-
- const [authMethod, authString] = common.splitFirst(authorizationHeader, ' ', '').map((x) => x.trim());
- // eslint-disable-next-line sonarjs/no-small-switch
- switch (authMethod.toLowerCase()) {
- case 'basic': {
- const credentials = Buffer.from(authString, 'base64').toString('utf-8');
- return this.isValidBasic(credentials, ctx);
- }
-
- default:
- this.logger.debug(_scope, 'unknown authorization scheme', { ctx });
- return false;
- }
- }
-
-
- /**
- * Send a response requesting basic auth.
- * @param {http.ServerResponse} res
- */
- requestBasic(res) {
- res.setHeader(Enum.Header.WWWAuthenticate, `Basic realm="${this.basicRealm}", charset="UTF-8"`);
- throw new Errors.ResponseError(Enum.ErrorResponse.Unauthorized);
- }
-
-
- /**
- * Attempt to parse a session cookie, and determine if it
- * contains authenticated user.
- * Restores ctx.session from cookie data.
- * @param {Object} ctx
- * @param {String} cookieHeader
- * @returns {Boolean}
- */
- async isValidCookieAuth(ctx, cookieHeader) {
- const _scope = _fileScope('isValidCookieAuth');
- this.logger.debug(_scope, 'called', { ctx, cookieHeader });
-
- const [ cookieName, cookieValue ] = common.splitFirst(cookieHeader, '=', '');
- if (cookieName !== 'WSHas') {
- return false;
- }
- try {
- ctx.session = await this.mysteryBox.unpack(cookieValue);
- this.logger.debug(_scope, 'unpacked cookie', { ctx });
- return !!ctx.session.authenticatedProfile || !! ctx.session.authenticatedIdentifier;
- } catch (e) {
- this.logger.debug(_scope, 'could not unpack cookie', { error:e, ctx });
- return false;
- }
- }
-
-
- /**
- * Require that a request has valid auth over secure channel, requests if missing.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- * @param {String} loginPath
- */
- async required(req, res, ctx, loginPath) {
- const _scope = _fileScope('required');
- this.logger.debug(_scope, 'called', { ctx });
-
- if (this.secureAuthOnly && ctx.clientProtocol.toLowerCase() !== 'https') {
- this.logger.debug(_scope, 'rejecting insecure auth', ctx);
- throw new Errors.ResponseError(Enum.ErrorResponse.Forbidden, 'authentication required, but connection is insecure; cannot continue');
- }
-
- const sessionCookie = req.getHeader(Enum.Header.Cookie);
- if (sessionCookie && await this.isValidCookieAuth(ctx, sessionCookie)) {
- return true;
- }
-
- const authData = req.getHeader(Enum.Header.Authorization);
- if (authData) {
- if (await this.isValidAuthorization(authData, ctx)) {
- return true;
- }
- // If they came in trying header auth, let them try again.
- return this.requestBasic(res);
- }
-
- // Otherwise redirect to login.
- res.statusCode = 302;
- res.setHeader(Enum.Header.Location, loginPath);
- res.end();
-
- return false;
- }
-
-
- /**
- * Require that a request has valid local auth over secure channel, requests if missing.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- * @param {String} loginPath
- */
- async requiredLocal(req, res, ctx, loginPath) {
- const _scope = _fileScope('requiredLocal');
- this.logger.debug(_scope, 'called', { ctx });
-
- if (this.secureAuthOnly && ctx.clientProtocol.toLowerCase() !== 'https') {
- this.logger.debug(_scope, 'rejecting insecure auth', ctx);
- throw new Errors.ResponseError(Enum.ErrorResponse.Forbidden, 'authentication required, but connection is insecure; cannot continue');
- }
-
- // Only accept identifier sessions.
- const sessionCookie = req.getHeader(Enum.Header.Cookie);
- if (sessionCookie
- && await this.isValidCookieAuth(ctx, sessionCookie)
- && ctx.session.authenticatedIdentifier) {
- return true;
- }
-
- // Allow header auth
- const authData = req.getHeader(Enum.Header.Authorization);
- if (authData) {
- if (await this.isValidAuthorization(authData, ctx)) {
- return true;
- }
- // If they came in trying header auth, let them try again.
- return this.requestBasic(res);
- }
-
- // Otherwise redirect to login.
- res.statusCode = 302;
- res.setHeader(Enum.Header.Location, loginPath);
- res.end();
-
- return false;
- }
-
-}
-
-module.exports = Authenticator;
\ No newline at end of file
const common = require('./common');
const Enum = require('./enum');
const Manager = require('./manager');
-const SessionManager = require('./session-manager');
-const Authenticator = require('./authenticator');
+const { Authenticator, SessionManager } = require('@squeep/authentication-module');
const path = require('path');
const _fileScope = common.fileScope(__filename);
this.setResponseType(this.responseTypes, req, res, ctx);
- await this.authenticator.required(req, res, ctx, this.loginPath);
+ await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
await this.manager.getAdminOverview(res, ctx);
}
this.setResponseType(this.responseTypes, req, res, ctx);
- await this.authenticator.required(req, res, ctx, this.loginPath);
+ await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
await this.manager.getTopicDetails(res, ctx);
}
this.setResponseType(this.responseTypes, req, res, ctx);
- await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+ await this.authenticator.apiRequiredLocal(req, res, ctx);
await this.maybeIngestBody(req, res, ctx);
ctx.method = req.method;
this.setResponseType(this.responseTypes, req, res, ctx);
- await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+ await this.authenticator.apiRequiredLocal(req, res, ctx);
await this.maybeIngestBody(req, res, ctx);
ctx.method = req.method;
this.setResponseType(this.responseTypes, req, res, ctx);
- await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+ await this.authenticator.apiRequiredLocal(req, res, ctx);
await this.manager.processTasks(res, ctx);
}
/**
+ * Delegate login to authentication module.
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {Object} ctx
/**
+ * Delegate login to authentication module.
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {Object} ctx
this.setResponseType(this.responseTypes, req, res, ctx);
+ await this.authenticator.sessionOptionalLocal(req, res, ctx);
+
await this.maybeIngestBody(req, res, ctx);
await this.sessionManager.postAdminLogin(res, ctx);
/**
+ * Delegate login to authentication module.
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {Object} ctx
this.setResponseType(this.responseTypes, req, res, ctx);
+ await this.authenticator.sessionOptionalLocal(req, res, ctx);
+
await this.sessionManager.getAdminLogout(res, ctx);
}
/**
+ * Delegate login to authentication module.
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {Object} ctx
+++ /dev/null
-'use strict';
-
-/**
- * Here we process activities which support login sessions.
- */
-
-const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper');
-const { MysteryBox } = require('@squeep/mystery-box');
-const common = require('./common');
-const Enum = require('./enum');
-const Template = require('./template');
-
-const _fileScope = common.fileScope(__filename);
-
-class SessionManager {
- constructor(logger, authenticator, options) {
- this.logger = logger;
- this.authenticator = authenticator;
- this.options = options;
- this.indieAuthCommunication = new IndieAuthCommunication(logger, options);
- this.mysteryBox = new MysteryBox(logger, options);
-
- this.secureCookie = options.authenticator.secureAuthOnly ? ' Secure;' : '';
- this.cookieLifespan = 60 * 60 * 24 * 32;
- }
-
-
- /**
- * Set or update our session cookie.
- * @param {http.ServerResponse} res
- * @param {Object} session
- * @param {Number} maxAge
- */
- async _sessionCookieSet(res, session, maxAge) {
- const cookieName = 'WSHas';
- const secureSession = session && await this.mysteryBox.pack(session) || '';
- const cookie = [
- `${cookieName}=${secureSession}`,
- 'HttpOnly',
- this.secureCookie,
- `Max-Age: ${maxAge}`,
- ].join('; ');
- res.setHeader(Enum.Header.SetCookie, cookie);
- }
-
-
- /**
- * GET request for establishing admin session.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- */
- async getAdminLogin(res, ctx) {
- const _scope = _fileScope('getAdminLogin');
- this.logger.debug(_scope, 'called', { ctx });
-
- res.end(Template.adminLoginHTML(ctx, this.options));
- this.logger.info(_scope, 'finished', { ctx })
- }
-
-
- /**
- * POST request for taking form data to establish admin session.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- */
- async postAdminLogin(res, ctx) {
- const _scope = _fileScope('postAdminLogin');
- this.logger.debug(_scope, 'called', { ctx });
-
- ctx.errors = [];
-
- // Only attempt user login if no IndieAuth profile is set
- if (!ctx.parsedBody['me']) {
- this.logger.debug(_scope, 'no indieauth profile, trying identifier', { ctx });
-
- const identifier = ctx.parsedBody['identifier'];
- const credential = ctx.parsedBody['credential'];
-
- const isValidLocalIdentifier = await this.authenticator.isValidIdentifierCredential(identifier, credential, ctx);
- if (!isValidLocalIdentifier) {
- ctx.errors.push('Invalid username or password');
- }
-
- if (ctx.errors.length) {
- res.end(Template.adminLoginHTML(ctx, this.options));
- return;
- }
-
- // Valid auth, persist the authenticated session
- ctx.session = {
- authenticatedIdentifier: ctx.authenticationId,
- };
- await this._sessionCookieSet(res, ctx.session, this.cookieLifespan);
- res.statusCode = 302;
- res.setHeader(Enum.Header.Location, './');
- res.end();
- this.logger.info(_scope, 'finished local', { ctx });
- return;
- }
-
- let me, session, authorizationEndpoint;
- try {
- me = new URL(ctx.parsedBody['me']);
- } catch (e) {
- this.logger.debug(_scope, 'failed to parse supplied profile url', { ctx });
- ctx.errors.push(`Unable to understand '${ctx.parsedBody['me']}' as a profile URL.`);
- }
-
- if (me) {
- const profile = await this.indieAuthCommunication.fetchProfile(me);
- if (!profile || !profile.authorizationEndpoint) {
- this.logger.debug(_scope, 'failed to find any profile information at url', { ctx });
- ctx.errors.push(`No profile information was found at '${me}'.`);
- } else {
- // fetch and parse me for 'authorization_endpoint' relation links
- try {
- authorizationEndpoint = new URL(profile.authorizationEndpoint);
- } catch (e) {
- ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`);
- }
- }
-
- if (authorizationEndpoint) {
- const pkce = await IndieAuthCommunication.generatePKCE();
- session = {
- authorizationEndpoint: authorizationEndpoint.href,
- state: ctx.requestId,
- codeVerifier: pkce.codeVerifier,
- me,
- };
-
- Object.entries({
- 'response_type': 'code',
- 'client_id': this.options.dingus.selfBaseUrl,
- 'redirect_uri': `${this.options.dingus.selfBaseUrl}admin/_ia`,
- 'state': session.state,
- 'code_challenge': pkce.codeChallenge,
- 'code_challenge_method': pkce.codeChallengeMethod,
- 'me': me,
- }).forEach(([name, value]) => authorizationEndpoint.searchParams.set(name, value));
- }
- }
-
- if (ctx.errors.length) {
- res.end(Template.adminLoginHTML(ctx, this.options));
- return;
- }
-
- await this._sessionCookieSet(res, session, this.cookieLifespan);
- res.setHeader(Enum.Header.Location, authorizationEndpoint.href);
- res.statusCode = 302; // Found
- res.end();
-
- this.logger.info(_scope, 'finished indieauth', { ctx })
- }
-
-
- /**
- * GET request to remove current credentials.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- */
- async getAdminLogout(res, ctx) {
- const _scope = _fileScope('getAdminLogout');
- this.logger.debug(_scope, 'called', { ctx });
-
- this._sessionCookieSet(res, '', 0);
- res.statusCode = 302;
- res.setHeader(Enum.Header.Location, './');
- res.end();
-
- this.logger.info(_scope, 'finished', { ctx });
- }
-
-
- /**
- * GET request for returning IndieAuth redirect.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- */
- async getAdminIA(res, ctx) {
- const _scope = _fileScope('getAdminIA');
- this.logger.debug(_scope, 'called', { ctx });
-
- ctx.errors = [];
- ctx.session = {};
-
- // Unpack cookie to restore session data
-
- const [ cookieName, cookieValue ] = common.splitFirst((ctx.cookie || ''), '=', '');
- if (cookieName !== 'WSHas') {
- this.logger.debug(_scope, 'no cookie', { ctx });
- ctx.errors.push('missing required cookie');
- } else {
- try {
- ctx.session = await this.mysteryBox.unpack(cookieValue);
- this.logger.debug(_scope, 'restored session from cookie', { ctx });
- } catch (e) {
- this.logger.debug(_scope, 'could not unpack cookie');
- ctx.errors.push('invalid cookie');
- }
- }
-
- // Validate unpacked session values
-
- // Add any auth errors
- if (ctx.queryParams['error']) {
- ctx.errors.push(ctx.queryParams['error']);
- if (ctx.queryParams['error_description']) {
- ctx.errors.push(ctx.queryParams['error_description']);
- }
- }
-
- // check stuff
- if (ctx.queryParams['state'] !== ctx.session.state) {
- this.logger.debug(_scope, 'state mismatch', { ctx });
- ctx.errors.push('invalid state');
- }
-
- const code = ctx.queryParams['code'];
- if (!code) {
- this.logger.debug(_scope, 'missing code', { ctx });
- ctx.errors.push('invalid code');
- }
-
- let redeemProfileUrl;
- try {
- redeemProfileUrl = new URL(ctx.session.authorizationEndpoint);
- } catch (e) {
- this.logger.debug(_scope, 'failed to parse restored session authorization endpoint as url', { ctx });
- ctx.errors.push('invalid cookie');
- }
- let profile;
- if (redeemProfileUrl) {
- profile = await this.indieAuthCommunication.redeemProfileCode(redeemProfileUrl, code, ctx.session.codeVerifier, this.options.dingus.selfBaseUrl, `${this.options.dingus.selfBaseUrl}admin/_ia`);
- if (!profile) {
- this.logger.debug(_scope, 'no profile from code redemption', { ctx });
- ctx.errors.push('did not get a profile response from authorization endpoint code redemption');
- } else if (!profile.me) {
- this.logger.debug(_scope, 'no profile me identifier from code redemption', { ctx });
- ctx.errors.push('did not get \'me\' value from authorization endpoint code redemption');
- } else if (profile.me !== ctx.session.me) {
- this.logger.debug(_scope, 'mis-matched canonical me from redeemed profile', { ctx, profile });
- const newProfileUrl = new URL(profile.me);
- // Rediscover auth endpoint for the new returned profile.
- const newProfile = await this.indieAuthCommunication.fetchProfile(newProfileUrl);
- if (newProfile.authorizationEndpoint !== ctx.session.authorizationEndpoint) {
- this.logger.debug(_scope, 'mis-matched auth endpoints between provided me and canonical me', { ctx, profile, newProfile });
- ctx.errors.push('canonical profile url provided by authorization endpoint is not handled by that endpoint, cannot continue');
- } else {
- // The endpoints match, all is okay, update our records.
- ctx.session.me = profile.me;
- }
- }
- }
-
- if (ctx.errors.length) {
- await this._sessionCookieSet(res, '', 0);
- res.end(Template.adminIAHTML(ctx, this.options));
- return;
- }
-
- // set cookie as auth valid, redirect to admin
- ctx.session = {
- authenticatedProfile: ctx.session.me,
- };
-
- await this._sessionCookieSet(res, ctx.session, this.cookieLifespan);
- res.statusCode = 302;
- res.setHeader(Enum.Header.Location, './');
- res.end();
-
- this.logger.info(_scope, 'finished', { ctx })
- }
-
-
-}
-
-module.exports = SessionManager;
\ No newline at end of file
+++ /dev/null
-'use strict';
-
-const th = require('./template-helper');
-
-function errorsSection(ctx) {
- return (ctx.errors && ctx.errors.length) ? ` <section class="errors">
- <h2>Troubles</h2>
- <p>Problems were encountered while trying to authenticate your profile URL.</p>
- <ul>` +
- ctx.errors.map((error) => `<li>${error}</li>`).join('\n') + `
- </ul>
- </section>
- <div>
- <a href="./login">Try Again?</a>
- </div>`
- : '';
-}
-
-/**
- * Render any errors from attempting IndieAuth.
- * @param {Object} ctx
- * @param {String[]} ctx.errors
- * @param {Object} options
- * @param {Object} options.manager
- * @param {String} options.manager.pageTitle
- * @param {Object} options.dingus
- * @param {String} options.dingus.selfBaseUrl
- * @returns {String}
- */
-module.exports = (ctx, options) => {
- const pageTitle = options.manager.pageTitle;
- const footerEntries = options.manager.footerEntries;
- const headElements = [];
- const navLinks = [];
- const mainContent = [
- errorsSection(ctx),
- ];
- return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, mainContent, footerEntries);
-};
\ No newline at end of file
+++ /dev/null
-'use strict';
-
-const th = require('./template-helper');
-
-
-/**
- * Login form.
- */
-function indieAuthSection() {
- return ` <section class="indieauth">
- <h2>Login</h2>
- <form action="" method="POST">
- <fieldset>
- <legend>IndieAuth</legend>
- <label for="me">Profile URL:</label>
- <input id="me" name="me" type="url" size="40" placeholder="https://example.com/my_profile_url" value="" autofocus>
- <button>Login</button>
- <br>
- <div>
- Logging in with an <a class="external" href="https://indieweb.org/IndieAuth">IndieAuth</a> profile will allow you to view details of any topics on this hub which are related to that profile's domain.
- </div>
- </fieldset>
- </form>
- </section>`;
-}
-
-
-function userSection(ctx, options) {
- const secure = (ctx.clientProtocol || '').toLowerCase() === 'https';
- const showUserForm = secure || !options.authenticator.secureAuthOnly;
- return showUserForm ? ` <section class="user">
- <form action="" method="POST">
- <fieldset>
- <legend>User Account</legend>
- <label for="identifier">Username:</label>
- <input id="identifier" name="identifier" value="">
- <br>
- <label for="credential">Password:</label>
- <input id="credential" name="credential" type="password" value="">
- <br>
- <button>Login</button>
- <br>
- </fieldset>
- </form>
- </section>`
- : '';
-}
-
-
-function errorsSection(ctx) {
- return (ctx.errors && ctx.errors.length) ? ` <section class="errors">
- <h2>Troubles</h2>
- <p>Problems were encountered while trying to authenticate you.</p>
- <ul>` +
- ctx.errors.map((error) => `<li>${error}</li>`).join('\n') + `
- </ul>
- </section>`
- : '';
-}
-
-
-/**
- * Render login form for both local and profile authentication.
- * @param {Object} ctx
- * @param {Object} options
- * @param {Object} options.manager
- * @param {String} options.manager.pageTitle
- * @param {Object} options.dingus
- * @param {String} options.dingus.selfBaseUrl
- * @returns {String}
- */
-module.exports = (ctx, options) => {
- const pageTitle = options.manager.pageTitle;
- const footerEntries = options.manager.footerEntries;
- const headElements = [];
- const navLinks = [];
- const mainContent = [
- errorsSection(ctx),
- indieAuthSection(),
- userSection(ctx, options),
- ];
- return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, mainContent, footerEntries);
-};
\ No newline at end of file
*/
module.exports = (ctx, options) => {
const pageTitle = `${options.manager.pageTitle} - Topics`;
- const headElements = [];
- const navLinks = [];
+ const logoUrl = options.manager.logoUrl;
const footerEntries = options.manager.footerEntries;
if (!ctx.topics) {
ctx.topics = [];
}
- return th.htmlTemplate(ctx, 1, pageTitle, headElements, navLinks, [
+
+ const htmlOptions = {
+ pageTitle,
+ logoUrl,
+ footerEntries,
+ };
+
+ const content = [
+ '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
` <section class="topics">
<p>${ctx.topics.length ? ctx.topics.length : 'no'} topic${(ctx.topics.length === 1) ? '' : 's'}</p>
<table>
` </tbody>
</table>
</section>`,
- ], footerEntries);
+ ];
+
+ return th.htmlPage(1, ctx, htmlOptions, content);
};
\ No newline at end of file
*/
module.exports = (ctx, options) => {
const pageTitle = `${options.manager.pageTitle} - Topic Details`;
- const headElements = [];
+ const logoUrl = options.manager.logoUrl;
const navLinks = [
{
href: '..',
if (!ctx.subscriptions) {
ctx.subscriptions = [];
}
- return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, [
+
+ const htmlOptions = {
+ pageTitle,
+ logoUrl,
+ navLinks,
+ footerEntries,
+ };
+
+ const content = [
+ '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
` <section class="topics">
<table>
<thead>`,
` </tbody>
</table>
</section>`,
- ], footerEntries);
+ ];
+
+ return th.htmlPage(2, htmlOptions, content);
};
\ No newline at end of file
'use strict';
module.exports = {
- adminLoginHTML: require('./admin-login-html'),
- adminIAHTML: require('./admin-ia-html'),
adminOverviewHTML: require('./admin-overview-html'),
adminTopicDetailsHTML: require('./admin-topic-details-html'),
badgeSVG: require('./badge-svg'),
const th = require('./template-helper');
-function hAppSection(pageTitle) {
+function hAppSection(pageTitle, logoUrl) {
return ` <section class="h-app hidden">
<h2>h-app Information for IndieAuth Logins</h2>
- <img src="static/favicon.ico" class="u-logo">
+ <img src="${logoUrl}" class="u-logo">
<a href="" class="u-url p-name">${pageTitle}</a>
<p class="p-summary">
This is a WebSub Hub service, facilitating content distribution.
const contactHTML = options.adminContactHTML;
const footerEntries = options.manager.footerEntries;
const hubURL = options.dingus.selfBaseUrl || '<s>https://hub.example.com/</s>';
- const headElements = [];
const navLinks = [{
href: 'admin/',
text: 'Admin',
}];
- const mainContent = [
+ const htmlOptions = {
+ pageTitle,
+ logoUrl: options.manager.logoUrl,
+ footerEntries,
+ navLinks,
+ };
+ const content = [
+ '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
aboutSection(),
usageSection(isPublicHub, hubURL),
contactSection(contactHTML),
- hAppSection(pageTitle),
+ hAppSection(pageTitle, options.manager.logoUrl),
];
- return th.htmlTemplate(ctx, 0, pageTitle, headElements, navLinks, mainContent, footerEntries,
- );
+ return th.htmlPage(1, ctx, htmlOptions, content);
};
\ No newline at end of file
'use strict';
-/**
- * A bunch of shorthand to put together common parts of an HTML page.
- */
-
-/**
- * Some fields may have values outside normal dates, handle them here.
- * @param {Date} date
- * @param {String} otherwise
- */
-const dateOrNot = (date, otherwise) => {
- if (!date) {
- return otherwise;
- }
- if (typeof date === 'number') {
- date = new Date(date);
- }
- const dateMs = date.getTime();
- if (!Number.isFinite(dateMs)
- || dateMs == 0) {
- return otherwise;
- }
- return date.toString();
-};
-
-
-/**
- * Render a duration.
- * @param {Number} seconds
- * @returns {String}
- */
-const secondsToPeriod = (seconds) => {
- let value = seconds;
- const result = [];
-
- const nextResult = (factor, label) => {
- const r = factor ? value % factor : value;
- if (r) {
- result.push(`${r} ${label}${r != 1 ? 's' : ''}`);
- }
- value = factor ? Math.floor(value / factor) : value;
- }
-
- nextResult(60, 'second');
- nextResult(60, 'minute');
- nextResult(24, 'hour');
- nextResult(30, 'day');
- nextResult(undefined, 'month');
-
- result.reverse();
- return result.join(' ');
-};
+const { TemplateHelper } = require('@squeep/html-template-helper');
/**
return `<tr>
<th scope="row">${detailsLink ? '<a href="topic/' + topic.id + '">' : ''}${topic.url}${detailsLink ? '</a>' : ''}</th>
<td>${subscribers.length}</td>
- <td>${dateOrNot(topic.created, 'Unknown')}</td>
- <td>${secondsToPeriod(topic.leaseSecondsPreferred)}</td>
- <td>${secondsToPeriod(topic.leaseSecondsMin)}</td>
- <td>${secondsToPeriod(topic.leaseSecondsMax)}</td>
+ <td>${TemplateHelper.dateOrNot(topic.created, 'Unknown')}</td>
+ <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsPreferred)}</td>
+ <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsMin)}</td>
+ <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsMax)}</td>
<td>${topic.publisherValidationUrl ? topic.publisherValidationUrl : 'None'}</td>
<td>${topic.isActive}</td>
<td>${topic.isDeleted}</td>
- <td>${dateOrNot(topic.lastPublish, 'Never')}</td>
- <td>${dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
+ <td>${TemplateHelper.dateOrNot(topic.lastPublish, 'Never')}</td>
+ <td>${TemplateHelper.dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
<td>${topic.contentFetchAttemptsSinceSuccess}</td>
- <td>${dateOrNot(topic.contentUpdated, 'Never')}</td>
+ <td>${TemplateHelper.dateOrNot(topic.contentUpdated, 'Never')}</td>
<td>${topic.contentType}</td>
<td>${topic.id}</td>
</tr>`;
}
return `<tr>
<td scope="row">${subscription.callback}</td>
- <td>${dateOrNot(subscription.created, 'Unknown')}</td>
- <td>${dateOrNot(subscription.verified, 'Never')}</td>
- <td>${dateOrNot(subscription.expires, 'Never')}</td>
+ <td>${TemplateHelper.dateOrNot(subscription.created, 'Unknown')}</td>
+ <td>${TemplateHelper.dateOrNot(subscription.verified, 'Never')}</td>
+ <td>${TemplateHelper.dateOrNot(subscription.expires, 'Never')}</td>
<td>${!!subscription.secret}</td>
<td>${subscription.signatureAlgorithm}</td>
<td>${subscription.httpRemoteAddr}</td>
<td>${subscription.httpFrom}</td>
- <td>${dateOrNot(subscription.contentDelivered, 'Never')}</td>
+ <td>${TemplateHelper.dateOrNot(subscription.contentDelivered, 'Never')}</td>
<td>${subscription.deliveryAttemptsSinceSuccess}</td>
- <td>${dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
+ <td>${TemplateHelper.dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
<td>${subscription.id}</td>
</tr>`;
}
}
-/**
- * Render the preamble for an HTML page, up through body.
- * @param {Number} pagePathLevel number of paths below root this page is
- * @param {String} pageTitle
- * @param {String[]} headElements
- * @returns
- */
-function htmlHead(pagePathLevel, pageTitle, headElements = []) {
- const rootPathPfx = '../'.repeat(pagePathLevel);
- return `<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">` +
- headElements.map((e) => `${' '.repeat(2)}${e}`).join('\n') + `
- <title>${pageTitle}</title>
- <link rel="stylesheet" href="${rootPathPfx}static/theme.css">
- </head>
- <body>`;
-}
-
-
-/**
- * Closes remainder of HTML page body.
- * @returns {String}
- */
-function htmlTail() {
- return ` </body>
-</html>`;
-}
-
-
-/**
- * Render a navigation link for the header section.
- * @param {Object} nav
- * @param {String} nav.href
- * @param {String} nav.class
- * @param {String} nav.text
- * @returns {String}
- */
-function renderNavLink(nav) {
- return `<li>
- <a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>
-</li>`;
-}
-
-
-/**
- * Render the navigation header, and open the main section.
- * @param {String} pageTitle
- * @param {Object[]} navLinks
- * @returns {String}
- */
-function htmlHeader(pageTitle, navLinks = []) {
- return ` <header>
- <h1>${pageTitle}</h1>
- <nav>` +
- (navLinks.length ? `
- <ol>
- ${navLinks.map((l) => renderNavLink(l)).join('\n')}
- </ol>`
- : '') + `
- </nav>
- </header>
- <main>`;
-}
-
-
-/**
- * Close the main section and finish off with boilerplate.
- * @param {String[]} footerEntries
- * @returns {String}
- */
-function htmlFooter(footerEntries = []) {
- return ` </main>
- <footer>` +
- (footerEntries.length ? `
- <ol>` + footerEntries.map((f) => ` <li>${f}</li>`).join('\n') + `
- </ol>`
- : '') + `
- </footer>`;
-}
-
-
-/**
- * Render all parts of an HTML page. Adds user logout nav link automatically.
- * @param {Object} ctx
- * @param {Number} pagePathLevel
- * @param {String} pageTitle
- * @param {String[]} headElements
- * @param {Object[]} navLinks
- * @param {String[]} main
- * @param {String[]} footerEntries
- * @returns {String}
- */
-function htmlTemplate(ctx, pagePathLevel, pageTitle, headElements = [], navLinks = [], main = [], footerEntries = []) {
- const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
- if (user) {
- let logoutPath;
- if (pagePathLevel > 0) {
- logoutPath = `${'../'.repeat(pagePathLevel - 1)}`;
- } else {
- logoutPath = 'admin/';
- }
- navLinks.push({
- text: `Logout (${user})`,
- href: `${logoutPath}logout`,
- });
- }
- return [
- htmlHead(pagePathLevel, pageTitle, headElements),
- htmlHeader(pageTitle, navLinks),
- ...main,
- htmlFooter(footerEntries),
- htmlTail(),
- ].join('\n');
-}
-
-
-module.exports = {
- dateOrNot,
- secondsToPeriod,
- htmlHeader,
- htmlFooter,
- htmlHead,
- htmlTail,
- renderNavLink,
+module.exports = Object.assign(Object.create(TemplateHelper), {
renderTopicRowHeader,
renderTopicRow,
renderSubscriptionRowHeader,
renderSubscriptionRow,
- htmlTemplate,
-};
\ No newline at end of file
+});
\ No newline at end of file
section + section {
margin-top: 2em;
}
+.logo {
+ vertical-align: middle;
+ height: 2em;
+}
.about {}
.usage {}
.copyright {
--- /dev/null
+'use strict';
+
+const assert = require('assert');
+const stubLogger = require('./stub-logger');
+const { lint } = require('html-minifier-lint'); // eslint-disable-line node/no-unpublished-require
+
+function lintHtml(html) {
+ const result = lint(html);
+ stubLogger.debug('lintHtml', '', { result, html });
+ assert(!result);
+}
+
+module.exports = lintHtml;
+++ /dev/null
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
-const Authenticator = require('../../src/authenticator');
-const stubLogger = require('../stub-logger');
-const stubDb = require('../stub-db');
-const Errors = require('../../src/errors');
-const Enum = require('../../src/enum');
-const Config = require('../../config');
-
-const noExpectedException = 'did not receive expected exception';
-
-describe('Authenticator', function () {
- let authenticator, credential, ctx, identifier, password, options;
- function _authMechanismRequired(a, m) {
- if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
- this.skip();
- }
- };
-
- beforeEach(function () {
- options = Config('test');
- authenticator = new Authenticator(stubLogger, stubDb, options);
- identifier = 'username';
- credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
- ctx = {};
- password = 'badPassword';
- stubDb._reset();
- });
- afterEach(function () {
- sinon.restore();
- });
-
- it('covers no auth mechanisms', function () {
- options.authenticator.authnEnabled = [];
- try {
- authenticator = new Authenticator(stubLogger, stubDb, options);
- assert.fail(noExpectedException);
- } catch (e) {
- assert.strictEqual(e.message, 'no authentication mechanisms available');
- }
- });
-
- describe('isValidBasic', function () {
- it('succeeds', async function () {
- _authMechanismRequired(authenticator, 'argon2');
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential,
- });
- const authString = `${identifier}:${password}`;
- const result = await authenticator.isValidBasic(authString, ctx);
- assert.strictEqual(result, true);
- assert.strictEqual(ctx.authenticationId, identifier);
- });
- it('fails', async function () {
- _authMechanismRequired(authenticator, 'argon2');
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential,
- });
- const authString = `${identifier}:wrongPassword}`;
- const result = await authenticator.isValidBasic(authString, ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- it('covers no entry', async function() {
- authenticator.db.authenticationGet.resolves();
- const authString = `${identifier}:wrongPassword}`;
- const result = await authenticator.isValidBasic(authString, ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- it('covers unknown password hash', async function () {
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential: '$other$kind_of_credential',
- });
- const authString = `${identifier}:wrongPassword}`;
- const result = await authenticator.isValidBasic(authString, ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- }); // isValidBasic
-
- describe('isValidIdentifierCredential', function () {
- it('succeeds', async function () {
- _authMechanismRequired(authenticator, 'argon2');
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential,
- });
- const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
- assert.strictEqual(result, true);
- assert.strictEqual(ctx.authenticationId, identifier);
- });
- it('fails', async function () {
- _authMechanismRequired(authenticator, 'argon2');
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential,
- });
- const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- it('covers no entry', async function() {
- authenticator.db.authenticationGet.resolves();
- const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- it('covers unknown password hash', async function () {
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential: '$other$kind_of_credential',
- });
- const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
- assert.strictEqual(result, false);
- assert.strictEqual(ctx.authenticationId, undefined);
- });
- it('covers PAM', async function () {
- _authMechanismRequired(authenticator, 'pam');
- sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
- authenticator.db.authenticationGet.resolves({
- identifier,
- credential: '$PAM$',
- });
- const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
- assert.strictEqual(result, true);
- assert.strictEqual(ctx.authenticationId, identifier);
- });
- it('covers debug', async function () {
- authenticator.authnEnabled = ['DEBUG_ANY'];
- const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
- assert.strictEqual(result, true);
- assert.strictEqual(ctx.authenticationId, identifier);
- });
- }); // isValidIdentifierCredential
-
- describe('_isValidPAMIdentifier', function () {
- beforeEach(function () {
- _authMechanismRequired(authenticator, 'pam');
- sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
- });
- it('covers success', async function () {
- authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
- const result = await authenticator._isValidPAMIdentifier(identifier, credential);
- assert.strictEqual(result, true);
- });
- it('covers failure', async function () {
- _authMechanismRequired(authenticator, 'pam');
- authenticator.authn.pam.pamAuthenticatePromise.rejects(new authenticator.authn.pam.PamError());
- const result = await authenticator._isValidPAMIdentifier(identifier, credential);
- assert.strictEqual(result, false);
- });
- it('covers error', async function () {
- _authMechanismRequired(authenticator, 'pam');
- const expected = new Error('blah');
- authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
- try {
- await authenticator._isValidPAMIdentifier(identifier, credential);
- assert.fail(noExpectedException);
- } catch (e) {
- assert.deepStrictEqual(e, expected);
- }
- });
- it('covers forbidden', async function () {
- identifier = 'root';
- const result = await authenticator._isValidPAMIdentifier(identifier, credential);
- assert.strictEqual(result, false);
- });
- }); // _isValidPAMIdentifier
-
- describe('isValidAuthorization', function () {
- it('handles basic', async function () {
- const expected = true;
- const authorizationHeader = 'basic Zm9vOmJhcg==';
- sinon.stub(authenticator, 'isValidBasic').resolves(expected);
- const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
- assert.strictEqual(result, expected);
- });
- it('handles other', async function () {
- const expected = false;
- const authorizationHeader = 'bearer Zm9vOmJhcg==';
- const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
- assert.strictEqual(result, expected);
- });
- }); // isValidAuthorization
-
- describe('requestBasic', function () {
- it('covers', function () {
- try {
- const res = {
- setHeader: () => {},
- };
- authenticator.requestBasic(res);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof Errors.ResponseError);
- assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
- }
- });
- }); // requestBasic
-
- describe('isValidCookieAuth', function () {
- beforeEach(function () {
- sinon.stub(authenticator.mysteryBox, 'unpack');
- });
- it('covers identifier success', async function () {
- authenticator.mysteryBox.unpack.resolves({
- authenticatedIdentifier: 'identifier',
- });
- const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
- assert.strictEqual(result, true);
- });
- it('covers profile success', async function () {
- authenticator.mysteryBox.unpack.resolves({
- authenticatedProfile: 'profile',
- });
- const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
- assert.strictEqual(result, true);
- });
- it('covers missing cookie', async function () {
- const result = await authenticator.isValidCookieAuth(ctx, 'wrongCookie');
- assert.strictEqual(result, false);
- });
- it('covers bad cookie', async function () {
- authenticator.mysteryBox.unpack.rejects();
- const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
- assert.strictEqual(result, false);
- });
- }); // isValidCookieAuth
-
- describe('required', function () {
- let req, res;
- beforeEach(function () {
- ctx.clientProtocol = 'https';
- req = {
- getHeader: sinon.stub(),
- };
- res = {
- end: sinon.stub(),
- setHeader: sinon.stub(),
- }
- });
- it('succeeds', async function() {
- req.getHeader.returns('auth header');
- sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
- const result = await authenticator.required(req, res, ctx);
- assert.strictEqual(result, true);
- });
- it('covers valid cookie session', async function () {
- req.getHeader.returns('WSHas=sessionCookie');
- sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
- const result = await authenticator.required(req, res, ctx);
- assert.strictEqual(result, true);
- });
- it('rejects insecure connection', async function () {
- ctx.clientProtocol = 'http';
- try {
- await authenticator.required(req, res, ctx);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof Errors.ResponseError);
- assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
- }
- });
- it('rejects invalid auth', async function () {
- try {
- req.getHeader.returns('auth header');
- sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
- await authenticator.required(req, res, ctx);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof Errors.ResponseError);
- assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
- }
- });
- it('redirects without any auth', async function () {
- await authenticator.required(req, res, ctx);
- assert(res.end.called);
- assert(res.setHeader.called);
- });
- }); // required
-
- describe('requiredLocal', function () {
- let req, res;
- beforeEach(function () {
- ctx.clientProtocol = 'https';
- req = {
- getHeader: sinon.stub(),
- };
- res = {
- end: sinon.stub(),
- setHeader: sinon.stub(),
- }
- });
- it('succeeds', async function() {
- req.getHeader.returns('auth header');
- sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
- const result = await authenticator.requiredLocal(req, res, ctx);
- assert.strictEqual(result, true);
- });
- it('covers valid cookie session', async function () {
- req.getHeader.returns('WSHas=sessionCookie');
- sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
- ctx.session = {
- authenticatedIdentifier: identifier,
- };
- const result = await authenticator.requiredLocal(req, res, ctx);
- assert.strictEqual(result, true);
- });
- it('rejects insecure connection', async function () {
- ctx.clientProtocol = 'http';
- try {
- await authenticator.requiredLocal(req, res, ctx);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof Errors.ResponseError);
- assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
- }
- });
- it('rejects invalid auth', async function () {
- try {
- req.getHeader.returns('auth header');
- sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
- await authenticator.requiredLocal(req, res, ctx);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof Errors.ResponseError);
- assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
- }
- });
- it('redirects without any auth', async function () {
- await authenticator.requiredLocal(req, res, ctx);
- assert(res.end.called);
- assert(res.setHeader.called);
- });
- }); // requiredLocal
-
-}); // Authenticator
describe('handlerGetAdminOverview', function () {
it('covers', async function () {
await service.handlerGetAdminOverview(req, res, ctx);
- assert(service.authenticator.required.called);
+ assert(service.authenticator.sessionRequired.called);
assert(service.manager.getAdminOverview.called);
})
}); // handlerGetAdminOverview
describe('handlerGetAdminTopicDetails', function () {
it('covers', async function () {
await service.handlerGetAdminTopicDetails(req, res, ctx);
- assert(service.authenticator.required.called);
+ assert(service.authenticator.sessionRequired.called);
assert(service.manager.getTopicDetails.called);
})
}); // handlerGetAdminTopicDetails
it('covers', async function () {
service.serveFile.resolves();
await service.handlerPostAdminProcess(req, res, ctx);
- assert(service.authenticator.requiredLocal.called);
+ assert(service.authenticator.apiRequiredLocal.called);
assert(service.manager.processTasks.called);
});
}); // handlerPostAdminProcess
it('covers', async function () {
sinon.stub(service, 'bodyData').resolves();
await service.handlerUpdateTopic(req, res, ctx);
- assert(service.authenticator.requiredLocal.called);
+ assert(service.authenticator.apiRequiredLocal.called);
assert(service.manager.updateTopic.called);
});
}); // handlerUpdateTopic
it('covers', async function () {
sinon.stub(service, 'bodyData').resolves();
await service.handlerUpdateSubscription(req, res, ctx);
- assert(service.authenticator.requiredLocal.called);
+ assert(service.authenticator.apiRequiredLocal.called);
assert(service.manager.updateSubscription.called);
});
}); // handlerUpdateSubscription
+++ /dev/null
-/* eslint-env mocha */
-/* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
-
-'use strict';
-
-const assert = require('assert');
-const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
-
-const SessionManager = require('../../src/session-manager');
-const Config = require('../../config');
-const stubLogger = require('../stub-logger');
-
-describe('SessionManager', function () {
- let manager, options, stubAuthenticator;
- let res, ctx;
-
- beforeEach(function () {
- options = new Config('test');
- res = {
- end: sinon.stub(),
- setHeader: sinon.stub(),
- };
- ctx = {
- cookie: '',
- params: {},
- queryParams: {},
- parsedBody: {},
- };
- stubAuthenticator = {
- isValidIdentifierCredential: sinon.stub(),
- };
- manager = new SessionManager(stubLogger, stubAuthenticator, options);
- sinon.stub(manager.indieAuthCommunication);
- stubLogger._reset();
- });
- afterEach(function () {
- sinon.restore();
- });
-
- describe('_sessionCookieSet', function () {
- let session, maxAge;
- beforeEach(function () {
- session = {};
- maxAge = 86400;
- });
- it('covers', async function () {
- await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
- });
- it('covers reset', async function () {
- session = undefined;
- maxAge = 0;
- await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
- });
- }); // _sessionCookieSet
-
- describe('getAdminLogin', function () {
- it('covers', async function () {
- await manager.getAdminLogin(res, ctx);
- });
- }); // getAdminLogin
-
- describe('postAdminLogin', function () {
- it('covers valid local', async function () {
- ctx.parsedBody.identifier = 'user';
- ctx.parsedBody.credential = 'password';
- manager.authenticator.isValidIdentifierCredential.resolves(true);
- await manager.postAdminLogin(res, ctx);
- assert.strictEqual(res.statusCode, 302);
- });
- it('covers invalid local', async function () {
- ctx.parsedBody.identifier = 'user';
- ctx.parsedBody.credential = 'password';
- manager.authenticator.isValidIdentifierCredential.resolves(false);
- await manager.postAdminLogin(res, ctx);
- assert(!res.setHeader.called);
- });
- it('covers valid profile', async function () {
- ctx.parsedBody.me = 'https://example.com/profile';
- manager.indieAuthCommunication.fetchProfile.resolves({
- authorizationEndpoint: 'https://example.com/auth',
- });
- await manager.postAdminLogin(res, ctx);
- assert.strictEqual(res.statusCode, 302);
- });
- it('covers invalid profile', async function () {
- ctx.parsedBody.me = 'not a profile';
- manager.indieAuthCommunication.fetchProfile.resolves();
- await manager.postAdminLogin(res, ctx);
- assert(!res.setHeader.called);
- });
- it('covers invalid profile response', async function () {
- ctx.parsedBody.me = 'https://example.com/profile';
- manager.indieAuthCommunication.fetchProfile.resolves();
- await manager.postAdminLogin(res, ctx);
- assert(!res.setHeader.called);
- });
- it('covers invalid profile response endpoint', async function () {
- ctx.parsedBody.me = 'https://example.com/profile';
- manager.indieAuthCommunication.fetchProfile.resolves({
- authorizationEndpoint: 'not an auth endpoint',
- });
- await manager.postAdminLogin(res, ctx);
- assert(!res.setHeader.called);
- });
- }); // postAdminLogin
-
- describe('getAdminLogout', function () {
- it('covers', async function () {
- await manager.getAdminLogout(res, ctx);
- });
- }); // getAdminLogout
-
- describe('getAdminIA', function () {
- let state, me, authorizationEndpoint;
- beforeEach(function () {
- state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
- me = 'https://example.com/profile';
- authorizationEndpoint = 'https://example.com/auth'
- ctx.cookie = 'WSHas=sessionCookie';
- manager.indieAuthCommunication.redeemProfileCode.resolves({
- me,
- });
- manager.indieAuthCommunication.fetchProfile.resolves({
- authorizationEndpoint,
- });
- sinon.stub(manager.mysteryBox, 'unpack').resolves({
- authorizationEndpoint,
- state,
- me,
- });
- });
- it('covers valid', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
-
- await manager.getAdminIA(res, ctx);
-
- assert.strictEqual(res.statusCode, 302);
- });
- it('covers missing cookie', async function () {
- delete ctx.cookie;
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers invalid cookie', async function () {
- manager.mysteryBox.unpack.restore();
- sinon.stub(manager.mysteryBox, 'unpack').rejects();
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers mis-matched state', async function () {
- ctx.queryParams['state'] = 'incorrect-state';
- ctx.queryParams['code'] = 'codeCodeCode';
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('relays auth endpoint errors', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
- ctx.queryParams['error'] = 'error_code';
- ctx.queryParams['error_description'] = 'something went wrong';
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers invalid restored session', async function () {
- manager.mysteryBox.unpack.restore();
- sinon.stub(manager.mysteryBox, 'unpack').resolves({
- authorizationEndpoint: 'not a url',
- state,
- me,
- });
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers empty profile redemption response', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
- manager.indieAuthCommunication.redeemProfileCode.restore();
- sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves();
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers missing profile in redemption response', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
- manager.indieAuthCommunication.redeemProfileCode.restore();
- sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
- });
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- it('covers different canonical profile response', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
- manager.indieAuthCommunication.redeemProfileCode.restore();
- sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
- me: 'https://different.example.com/profile',
- });
-
- await manager.getAdminIA(res, ctx);
-
- assert.strictEqual(res.statusCode, 302);
- });
- it('covers different canonical profile response mis-matched endpoint', async function () {
- ctx.queryParams['state'] = state;
- ctx.queryParams['code'] = 'codeCodeCode';
- manager.indieAuthCommunication.redeemProfileCode.restore();
- sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
- me: 'https://different.example.com/profile',
- });
- manager.indieAuthCommunication.fetchProfile.restore();
- sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
- authorizationEndpoint: 'https://elsewhere.example.com/auth',
- });
-
- await manager.getAdminIA(res, ctx);
-
- assert(ctx.errors.length);
- });
- }); // getAdminIA
-
-}); // SessionManager
\ No newline at end of file
+++ /dev/null
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const template = require('../../../src/template/admin-ia-html');
-const Config = require('../../../config');
-const config = new Config('test');
-
-describe('Admin Login HTML Template', function () {
- let ctx;
-
- beforeEach(function () {
- ctx = {};
- });
-
- it('covers', function () {
- ctx.errors = ['bad'];
- const result = template(ctx, config);
- assert(result);
- });
- it('covers empty', function () {
- const result = template(ctx, config);
- assert(result);
- });
-});
+++ /dev/null
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const template = require('../../../src/template/admin-login-html');
-const Config = require('../../../config');
-const config = new Config('test');
-
-describe('Admin Login HTML Template', function () {
- let ctx;
-
- beforeEach(function () {
- ctx = {};
- });
-
- it('covers', function () {
- ctx.errors = ['bad'];
- ctx.clientProtocol = 'https';
- const result = template(ctx, config);
- assert(result);
- });
- it('covers empty', function () {
- const result = template(ctx, config);
- assert(result);
- });
-});
const assert = require('assert');
const template = require('../../../src/template/admin-overview-html');
const Config = require('../../../config');
-const config = new Config('test');
+const lintHtml = require('../../lint-html');
describe('Admin Overview HTML Template', function () {
- let ctx;
+ let ctx, config;
beforeEach(function () {
ctx = {};
+ config = new Config('test');
});
it('covers missing topics', function () {
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers single topic', function () {
ctx.topics = [{}];
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers plural topics', function () {
ctx.topics = [{}, {}, {}];
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
});
const assert = require('assert');
const template = require('../../../src/template/admin-topic-details-html');
const Config = require('../../../config');
-const config = new Config('test');
+const lintHtml = require('../../lint-html');
describe('Admin Topic Details HTML Template', function () {
- let ctx;
+ let ctx, config;
beforeEach(function () {
ctx = {
{},
],
};
+ config = new Config('test');
});
it('renders', function () {
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers null topic', function () {
ctx.topic = null;
ctx.subscriptions = null;
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers missing subscriptions', function () {
delete ctx.subscriptions;
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers plural subscriptions', function () {
ctx.subscriptions = [{}, {}, {}];
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
});
const assert = require('assert');
const template = require('../../../src/template/root-html');
const Config = require('../../../config');
+const lintHtml = require('../../lint-html');
describe('Root HTML Template', function () {
let ctx, config;
it('renders', function () {
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
it('covers options', function () {
delete config.dingus.selfBaseUrl;
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
config.adminContactHTML = '<div>support</div>';
config.manager.publicHub = false;
const result = template(ctx, config);
+ lintHtml(result);
assert(result);
});
const assert = require('assert');
const th = require('../../../src/template/template-helper');
-const Config = require('../../../config');
-const config = new Config('test');
describe('Template Helper', function () {
- let ctx;
-
- beforeEach(function () {
- ctx = {};
- });
-
- describe('dateOrNot', function () {
- let date, otherwise;
- beforeEach(function () {
- date = new Date();
- otherwise = 'otherwise';
- });
- it('covers', function () {
- const result = th.dateOrNot(date, otherwise);
- assert.strictEqual(result, date.toString());
- });
- it('covers no date', function () {
- date = undefined;
- const result = th.dateOrNot(date, otherwise);
- assert.strictEqual(result, otherwise);
- });
- it('covers ms', function () {
- const result = th.dateOrNot(date.getTime(), otherwise);
- assert.strictEqual(result, date.toString());
- });
- it('covers naught', function () {
- const result = th.dateOrNot(0, otherwise);
- assert.strictEqual(result, otherwise);
- });
- it('covers the infinite', function () {
- const result = th.dateOrNot(-Infinity, otherwise);
- assert.strictEqual(result, otherwise);
- });
- }); // dateOrNot
-
- describe('secondsToPeriod', function () {
- it('covers seconds', function () {
- const result = th.secondsToPeriod(45);
- assert.strictEqual(result, '45 seconds');
- });
- it('covers minutes', function () {
- const result = th.secondsToPeriod(105);
- assert.strictEqual(result, '1 minute 45 seconds');
- });
- it('covers hours', function () {
- const result = th.secondsToPeriod(3705);
- assert.strictEqual(result, '1 hour 1 minute 45 seconds');
- });
- it('covers days', function () {
- const result = th.secondsToPeriod(90105);
- assert.strictEqual(result, '1 day 1 hour 1 minute 45 seconds');
- });
- it('covers months', function () {
- const result = th.secondsToPeriod(5274105);
- assert.strictEqual(result, '2 months 1 day 1 hour 1 minute 45 seconds');
- });
- }); // secondsToPeriod
describe('renderTopicRow', function () {
let topic, subscribers;
});
}); // renderSubscriptionRowHeader
- describe('htmlHead', function () {
- let pagePathLevel, pageTitle, headElements;
- beforeEach(function () {
- pagePathLevel = 2;
- pageTitle = 'title';
- });
- it('covers', function () {
- const result = th.htmlHead(pagePathLevel, pageTitle, headElements);
- assert(result);
- });
- it('covers elements', function () {
- headElements = [ '<div>foop</div>', '<div>poof</div>' ];
- const result = th.htmlHead(pagePathLevel, pageTitle, headElements);
- assert(result);
- });
- }); // htmlHead
-
- describe('htmlTail', function () {
- it('covers', function () {
- const result = th.htmlTail();
- assert(result);
- });
- }); // htmlTail
-
- describe('renderNavLink', function () {
- let nav;
- beforeEach(function () {
- nav = {
- href: 'https://example.com/',
- text: 'example',
- };
- });
- it('covers no class', function () {
- const result = th.renderNavLink(nav);
- assert(result);
- });
- it('covers class', function () {
- nav.class = 'foo bar';
- const result = th.renderNavLink(nav);
- assert(result);
- });
- }); // renderNavLink
-
- describe('htmlHeader', function () {
- let pageTitle, navLinks;
- beforeEach(function () {
- pageTitle = 'title';
- navLinks = [];
- });
- it('covers no links', function () {
- const result = th.htmlHeader(pageTitle);
- assert(result);
- });
- it('covers links', function () {
- navLinks = [
- {
- href: 'https://exmaple.com/',
- text: 'example',
- },
- ];
- const result = th.htmlHeader(pageTitle, navLinks);
- assert(result);
- });
- }); // htmlHeader
-
- describe('htmlFooter', function () {
- it('covers', function () {
- const result = th.htmlFooter(['foo', 'bar']);
- assert(result);
- });
- it('covers default', function () {
- const result = th.htmlFooter();
- assert(result);
- });
- }); // htmlFooter
-
- describe('htmlTemplate', function () {
- let pagePathLevel, pageTitle, headElements, navLinks, main;
- beforeEach(function () {
- ctx = {};
- pagePathLevel = 1;
- pageTitle = 'title';
- headElements = [];
- navLinks = [];
- main = [];
- });
- it('covers', function () {
- const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle, headElements, navLinks, main);
- assert(result);
- });
- it('covers defaults', function () {
- const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
- assert(result);
- });
- it('covers user', function () {
- ctx.session = {
- authenticatedProfile: 'user',
- };
- const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
- assert(result);
- });
- it('covers user at root path', function () {
- ctx.session = {
- authenticatedIdentifier: 'user',
- };
- pagePathLevel = 0;
- const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
- assert(result);
- });
- }); // htmlTemplate
-
});