Releases and notable changes to this project are documented here.
+## [unreleased] - TBD
+
+### Added
+
+- Account credential management interface.
+
+### Fixed
+
+- Dependency updates.
+
## [v1.3.11] - 2023-03-22
### Fixed
const defaultEnvironment = 'development';
const testEnvironment = 'test';
+/**
+ * Merge environment-specific config objects on top of defaults.
+ * @param {string=} environment from NODE_ENV
+ * @returns {object} config
+ */
function Config(environment) {
environment = environment || defaultEnvironment;
const defaultConfig = require('./default');
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+<!-- Generated by graphviz version 2.50.0 (20211204.2007)
-->
<!-- Title: WebsubHubERD Pages: 1 -->
<svg width="922pt" height="1084pt"
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">Postgres</text>
-<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.4</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.1.0</text>
<!-- topic -->
<g id="node1" class="node">
<title>topic</title>
<!-- authentication -->
<g id="node8" class="node">
<title>authentication</title>
-<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
-<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
-<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="lightblue" stroke="transparent" points="67,-811.13 67,-834.13 213,-834.13 213,-811.13 67,-811.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-812.13 68,-833.13 212,-833.13 212,-812.13 68,-812.13"/>
+<text text-anchor="start" x="71" y="-818.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-790.13 67,-811.13 213,-811.13 213,-790.13 67,-790.13"/>
+<text text-anchor="start" x="112.5" y="-796.93" font-family="Times,serif" font-size="14.00">created</text>
<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
-<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<text text-anchor="start" x="73.5" y="-775.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
-<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<text text-anchor="start" x="106.5" y="-754.93" font-family="Times,serif" font-size="14.00">identifier</text>
<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
-<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<text text-anchor="start" x="103.5" y="-733.93" font-family="Times,serif" font-size="14.00">credential</text>
<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
-<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+<text text-anchor="start" x="112.5" y="-712.93" font-family="Times,serif" font-size="14.00">otp_key</text>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+<!-- Generated by graphviz version 2.50.0 (20211204.2007)
-->
<!-- Title: WebsubHubERD Pages: 1 -->
<svg width="922pt" height="1084pt"
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">SQLite</text>
-<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.4</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.1.0</text>
<!-- topic -->
<g id="node1" class="node">
<title>topic</title>
<!-- authentication -->
<g id="node8" class="node">
<title>authentication</title>
-<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
-<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
-<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="lightblue" stroke="transparent" points="67,-811.13 67,-834.13 213,-834.13 213,-811.13 67,-811.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-812.13 68,-833.13 212,-833.13 212,-812.13 68,-812.13"/>
+<text text-anchor="start" x="71" y="-818.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-790.13 67,-811.13 213,-811.13 213,-790.13 67,-790.13"/>
+<text text-anchor="start" x="112.5" y="-796.93" font-family="Times,serif" font-size="14.00">created</text>
<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
-<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<text text-anchor="start" x="73.5" y="-775.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
-<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<text text-anchor="start" x="106.5" y="-754.93" font-family="Times,serif" font-size="14.00">identifier</text>
<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
-<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<text text-anchor="start" x="103.5" y="-733.93" font-family="Times,serif" font-size="14.00">credential</text>
<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
-<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+<text text-anchor="start" x="112.5" y="-712.93" font-family="Times,serif" font-size="14.00">otp_key</text>
</g>
</g>
</svg>
"version": "1.3.11",
"license": "ISC",
"dependencies": {
- "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9",
- "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.1",
- "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0",
+ "@squeep/api-dingus": "^2",
+ "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.5.0",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
"@squeep/logger-json-console": "^3",
"@squeep/roman": "^1",
"@squeep/web-linking": "^1",
}
},
"node_modules/@mapbox/node-pre-gyp": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
- "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+ "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,
"dependencies": {
- "detect-libc": "^2.0.0",
+ "detect-libc": "^1.0.3",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
- "node-fetch": "^2.6.7",
+ "node-fetch": "^2.6.1",
"nopt": "^5.0.0",
- "npmlog": "^5.0.1",
+ "npmlog": "^4.1.2",
"rimraf": "^3.0.2",
- "semver": "^7.3.5",
- "tar": "^6.1.11"
+ "semver": "^7.3.4",
+ "tar": "^6.1.0"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
"dev": true
},
"node_modules/@squeep/api-dingus": {
- "version": "1.2.9",
- "resolved": "git+https://git.squeep.com/squeep-api-dingus/#3b15b5ff792fc5d61be8337989058c297460cd99",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.1.2.tgz",
+ "integrity": "sha512-d4jj0TL0qDz/ADWMqwPkmJ/eFy2fhzFTP2iBTcAzIG/ZH0y0Xtic8XdZct5kiihAVQC6ut3SCptkxXKeA7K/mA==",
"dependencies": {
+ "@squeep/log-helper": "^1",
"mime-db": "^1.52.0",
- "uuid": "^9.0.0"
+ "uuid": "^9.0.1"
},
"engines": {
- "node": ">=14"
+ "node": ">=14.13.1"
}
},
"node_modules/@squeep/authentication-module": {
- "version": "1.3.1",
- "resolved": "git+https://git.squeep.com/squeep-authentication-module/#a821a0f79c42f8ea55aec503c992650113d82028",
+ "version": "1.5.0",
+ "resolved": "git+https://git.squeep.com/squeep-authentication-module/#5ea2ffe571a74618eef073c58c5fef06e1cf06a7",
"dependencies": {
- "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9",
- "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0",
- "@squeep/indieauth-helper": "^1.3.0",
- "@squeep/mystery-box": "^2.0.1"
+ "@squeep/api-dingus": "^2",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
+ "@squeep/indieauth-helper": "^1",
+ "@squeep/mystery-box": "^2",
+ "@squeep/totp": "^1",
+ "uuid": "^9"
},
"engines": {
- "node": "^14 >=14.18 || >=15.7"
+ "node": ">=18"
},
"optionalDependencies": {
- "argon2": "^0.30.3",
+ "argon2": "^0.40.1",
"node-linux-pam": "^0.2.1"
}
},
}
},
"node_modules/@squeep/html-template-helper": {
- "version": "1.4.0",
- "resolved": "git+https://git.squeep.com/squeep-html-template-helper#100046316a87631fb8814f80b35647709e6c7319",
+ "version": "1.6.1",
+ "resolved": "git+https://git.squeep.com/squeep-html-template-helper#93d1b030d6b3c6ea93c36a46f4940181a1acaca0",
"dependencies": {
- "@squeep/lazy-property": "^1.1.2"
+ "@squeep/lazy-property": "^1"
},
"engines": {
- "node": ">=14"
+ "node": ">=14.13.1"
}
},
"node_modules/@squeep/indieauth-helper": {
"resolved": "https://registry.npmjs.org/@squeep/roman/-/roman-1.0.2.tgz",
"integrity": "sha512-VFNjvSTR96r1Ii7zTxweUimiyoxs9cRo6TXXwjR/dX+riOL+YERvhf2w5M+mYYC3QhKP+j9aUVFrTWgjJV3MIw=="
},
+ "node_modules/@squeep/totp": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@squeep/totp/-/totp-1.1.5.tgz",
+ "integrity": "sha512-keqggH2NrHs8hqzyov31zIA4XTLUxwXBn+VfUFlCdzZY2omoWbgm4742Ht8j3W48FLtIX1q4Zrm1ncObi9RfMA==",
+ "dependencies": {
+ "base32.js": "^0.1.0",
+ "qrcode-svg": "^1.1.0"
+ },
+ "engines": {
+ "node": ">14"
+ }
+ },
"node_modules/@squeep/web-linking": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@squeep/web-linking/-/web-linking-1.0.9.tgz",
"dev": true
},
"node_modules/@typescript-eslint/types": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz",
- "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==",
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz",
+ "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==",
"dev": true,
"engines": {
"node": "^18.18.0 || >=20.0.0"
}
},
"node_modules/aproba": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
- "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"node_modules/archy": {
}
},
"node_modules/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==",
+ "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==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
"delegates": "^1.0.0",
- "readable-stream": "^3.6.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/are-we-there-yet/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "optional": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
+ "readable-stream": "^2.0.6"
}
},
"node_modules/argon2": {
- "version": "0.30.3",
- "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.30.3.tgz",
- "integrity": "sha512-DoH/kv8c9127ueJSBxAVJXinW9+EuPA3EMUxoV2sAY1qDE5H9BjTyVF/aD2XyHqbqUWabgBkIfcP3ZZuGhbJdg==",
+ "version": "0.40.3",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.3.tgz",
+ "integrity": "sha512-FrSmz4VeM91jwFvvjsQv9GYp6o/kARWoYKjbjDB2U5io1H3e5X67PYGclFDeQff6UXIhUd4aHR3mxCdBbMMuQw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
- "@mapbox/node-pre-gyp": "^1.0.10",
"@phc/format": "^1.0.0",
- "node-addon-api": "^5.0.0"
+ "node-addon-api": "^8.0.0",
+ "node-gyp-build": "^4.8.0"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=16.17.0"
}
},
"node_modules/argparse": {
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
},
+ "node_modules/base32.js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz",
+ "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001634",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001634.tgz",
- "integrity": "sha512-fbBYXQ9q3+yp1q1gBk86tOFs4pyn/yxFm5ZNP18OXJDfA3txImOY9PhfxVggZ4vRHDqoU8NrKU81eN0OtzOgRA==",
+ "version": "1.0.30001636",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
+ "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
"dev": true,
"funding": [
{
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"devOptional": true
},
- "node_modules/color-support": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
- "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
- "optional": true,
- "bin": {
- "color-support": "bin.js"
- }
- },
"node_modules/commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"optional": true
},
"node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=0.10"
}
},
"node_modules/diff": {
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.803",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz",
- "integrity": "sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==",
+ "version": "1.4.805",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.805.tgz",
+ "integrity": "sha512-8W4UJwX/w9T0QSzINJckTKG6CYpAUTqsaWcWIsdud3I1FYJcMgW9QqT1/4CBff/pP/TihWh13OmiyY8neto6vw==",
"dev": true
},
"node_modules/emoji-regex": {
}
},
"node_modules/espree": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
- "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
+ "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
"dev": true,
"dependencies": {
- "acorn": "^8.11.3",
+ "acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
}
},
"node_modules/gauge": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
- "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
- "aproba": "^1.0.3 || ^2.0.0",
- "color-support": "^1.1.2",
+ "aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.1",
- "object-assign": "^4.1.1",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1",
- "wide-align": "^1.1.2"
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "node_modules/gauge/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gauge/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "optional": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=0.10.0"
}
},
"node_modules/gensync": {
}
},
"node_modules/globals": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.4.0.tgz",
- "integrity": "sha512-unnwvMZpv0eDUyjNyh9DH/yxUaRYrEjW/qK4QcdrHg3oO11igUQrCSgODHEqxlKg8v2CD2Sd7UkqqEBoz5U7TQ==",
+ "version": "15.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz",
+ "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==",
"dev": true,
"engines": {
"node": ">=18"
}
},
"node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "devOptional": true,
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
+ "optional": true,
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=0.10.0"
}
},
"node_modules/is-glob": {
}
},
"node_modules/node-addon-api": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
- "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
- "optional": true
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz",
+ "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==",
+ "optional": true,
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
},
"node_modules/node-fetch": {
"version": "2.7.0",
}
}
},
+ "node_modules/node-gyp-build": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
+ "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
+ "optional": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-linux-pam": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-linux-pam/-/node-linux-pam-0.2.1.tgz",
"node": ">=8.6.0"
}
},
- "node_modules/node-linux-pam/node_modules/@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,
- "dependencies": {
- "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"
- },
- "bin": {
- "node-pre-gyp": "bin/node-pre-gyp"
- }
- },
- "node_modules/node-linux-pam/node_modules/ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
- "optional": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/node-linux-pam/node_modules/aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "optional": true
- },
- "node_modules/node-linux-pam/node_modules/are-we-there-yet": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
- "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
- "deprecated": "This package is no longer supported.",
- "optional": true,
- "dependencies": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
"node_modules/node-linux-pam/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"wrap-ansi": "^6.2.0"
}
},
- "node_modules/node-linux-pam/node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/node-linux-pam/node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "optional": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/node-linux-pam/node_modules/detect-libc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
- "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
- "optional": true,
- "bin": {
- "detect-libc": "bin/detect-libc.js"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/node-linux-pam/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"node": ">=8"
}
},
- "node_modules/node-linux-pam/node_modules/gauge": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
- "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
- "deprecated": "This package is no longer supported.",
- "optional": true,
- "dependencies": {
- "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"
- }
- },
- "node_modules/node-linux-pam/node_modules/gauge/node_modules/string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
- "optional": true,
- "dependencies": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/node-linux-pam/node_modules/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": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
+ "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,
- "dependencies": {
- "number-is-nan": "^1.0.0"
- },
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
"node_modules/node-linux-pam/node_modules/locate-path": {
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==",
"optional": true
},
- "node_modules/node-linux-pam/node_modules/npmlog": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
- "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "deprecated": "This package is no longer supported.",
- "optional": true,
- "dependencies": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
- }
- },
"node_modules/node-linux-pam/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"node": ">=8"
}
},
- "node_modules/node-linux-pam/node_modules/strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "node_modules/node-linux-pam/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"optional": true,
"dependencies": {
- "ansi-regex": "^2.0.0"
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
},
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
"node_modules/node-linux-pam/node_modules/wrap-ansi": {
"node": ">=8"
}
},
- "node_modules/node-linux-pam/node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/node-linux-pam/node_modules/wrap-ansi/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "optional": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/node-linux-pam/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
}
},
"node_modules/npmlog": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
- "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
- "are-we-there-yet": "^2.0.0",
- "console-control-strings": "^1.1.0",
- "gauge": "^3.0.0",
- "set-blocking": "^2.0.0"
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
}
},
"node_modules/number-is-nan": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/nyc/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/nyc/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"node": ">=8"
}
},
+ "node_modules/nyc/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/nyc/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"node": ">=10"
}
},
+ "node_modules/prebuild-install/node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"node": ">=6"
}
},
+ "node_modules/qrcode-svg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
+ "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==",
+ "bin": {
+ "qrcode-svg": "bin/qrcode-svg.js"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"optional": true
},
"node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "devOptional": true,
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
+ "optional": true,
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "optional": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
"node_modules/strip-ansi": {
}
},
"node_modules/type-fest": {
- "version": "4.20.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz",
- "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==",
+ "version": "4.20.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz",
+ "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==",
"engines": {
"node": ">=16"
},
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"author": "Justin Wind <jwind-websubhub@squeep.com>",
"license": "ISC",
"pre-commit": [
- "audit",
"eslint",
"coverage",
- "coverage-check"
+ "coverage-check",
+ "audit"
],
"dependencies": {
- "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9",
- "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.1",
- "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0",
+ "@squeep/api-dingus": "^2",
+ "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.5.0",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
"@squeep/logger-json-console": "^3",
"@squeep/roman": "^1",
"@squeep/web-linking": "^1",
return str.toString().slice(0, len) + `... (${str.toString().length} bytes)`;
};
+const nop = () => undefined;
+
module.exports = {
...common,
arrayChunk,
ensureArray,
freezeDeep,
logTruncate,
+ nop,
randomBytesAsync,
stackSafePush,
topicLeaseDefaults,
const _fileScope = common.fileScope(__filename);
class Database {
- constructor(logger = common.nullLogger, options = {}) {
+ constructor(logger, options = {}) {
this.logger = logger;
- common.ensureLoggerLevels(this.logger);
// Store the merged config and default values for lease values.
// N.B. breaking hierarchy of config options here
* @param {*} dbCtx db context
* @param {string} identifier authentication identifier
* @param {string} credential authentication credential
+ * @param {string=} otpKey authentication otp key
*/
- async authenticationUpsert(dbCtx, identifier, credential) {
+ async authenticationUpsert(dbCtx, identifier, credential, otpKey) {
this._notImplemented('authenticationUpsert', arguments);
}
+ /**
+ * Update an authentication entity's otp key.
+ * @param {*} dbCtx db context
+ * @param {string} identifier authentication identifier
+ * @param {string=} otpKey authentication otp key
+ */
+ async authenticationUpdateOTPKey(dbCtx, identifier, otpKey) {
+ this._notImplemented('authenticationUpdateKey', arguments);
+ }
+
+
+ /**
+ * Update an authentication entity's credential.
+ * @param {*} dbCtx db context
+ * @param {string} identifier authentication identifier
+ * @param {string} credential authentication credential
+ */
+ async authenticationUpdateCredential(dbCtx, identifier, credential) {
+ this._notImplemented('authenticationUpdateKey', arguments);
+ }
+
+
/**
* All subscriptions to a topic.
* @param {*} dbCtx db context
},
max: {
major: 1,
- minor: 0,
- patch: 4,
+ minor: 1,
+ patch: 0,
},
};
}
- async authenticationUpsert(dbCtx, identifier, credential) {
+ async authenticationUpsert(dbCtx, identifier, credential, otpKey) {
const _scope = _fileScope('authenticationUpsert');
const scrubbedCredential = '*'.repeat((credential || '').length);
- this.logger.debug(_scope, 'called', { identifier, scrubbedCredential });
+ const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null;
+ this.logger.debug(_scope, 'called', { identifier, scrubbedCredential, scrubbedOTPKey });
let result;
try {
- result = await dbCtx.result(this.statement.authenticationUpsert, { identifier, credential });
+ result = await dbCtx.result(this.statement.authenticationUpsert, { identifier, credential, otpKey });
if (result.rowCount != 1) {
throw new DBErrors.UnexpectedResult('did not upsert authentication');
}
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential, scrubbedOTPKey });
+ throw e;
+ }
+ }
+
+
+ async authenticationUpdateCredential(dbCtx, identifier, credential) {
+ const _scope = _fileScope('authenticationUpdateCredential');
+ const scrubbedCredential = '*'.repeat((credential || '').length);
+ this.logger.debug(_scope, 'called', { identifier, scrubbedCredential });
+
+ let result;
+ try {
+ result = await dbCtx.result(this.statement.authenticationUpdateCredential, { identifier, credential });
+ if (result.rowCount != 1) {
+ throw new DBErrors.UnexpectedResult('did not update authentication credential');
+ }
} catch (e) {
this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential });
throw e;
}
+ async authenticationUpdateOTPKey(dbCtx, identifier, otpKey) {
+ const _scope = _fileScope('authenticationUpdateOTPKey');
+ const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null;
+ this.logger.debug(_scope, 'called', { identifier, scrubbedOTPKey });
+
+ let result;
+ try {
+ result = await dbCtx.result(this.statement.authenticationUpdateOtpKey, { identifier, otpKey });
+ if (result.rowCount != 1) {
+ throw new DBErrors.UnexpectedResult('did not update authentication otp key');
+ }
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedOTPKey });
+ throw e;
+ }
+ }
+
+
async subscriptionsByTopicId(dbCtx, topicId) {
const _scope = _fileScope('subscriptionsByTopicId');
this.logger.debug(_scope, 'called', { topicId });
--- /dev/null
+UPDATE authentication
+ SET credential = $(credential)
+ WHERE identifier = $(identifier)
--- /dev/null
+UPDATE authentication
+ SET otp_key = $(otpKey)
+ WHERE identifier = $(identifier)
--
INSERT INTO authentication
- (identifier, credential)
+ (identifier, credential, otp_key)
VALUES
- ($(identifier), $(credential))
+ ($(identifier), $(credential), $(otpKey))
ON CONFLICT (identifier) DO UPDATE
SET
identifier = $(identifier),
- credential = $(credential)
+ credential = $(credential),
+ otp_key = $(otpKey)
--- /dev/null
+BEGIN;
+
+ ALTER TABLE authentication ADD COLUMN otp_key TEXT;
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0);
+
+COMMIT;
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.1.0",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ <tr><td port="">http_etag</td></tr>
+ <tr><td port="">http_last_modified</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ <tr><td port="">otp_key</td></tr>
+ </table>
+ >];
+
+}
--- /dev/null
+BEGIN;
+ ALTER TABLE authentication DROP COLUMN otp_key;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0;
+COMMIT;
},
max: {
major: 1,
- minor: 0,
- patch: 4,
+ minor: 1,
+ patch: 0,
},
};
this.db = new SQLite(dbFilename, sqliteOptions);
this.schemaVersionsSupported = schemaVersionsSupported;
this.changesSinceLastOptimize = BigInt(0);
- this.optimizeAfterChanges = options.db.connectionString.optimizeAfterChanges;
+ this.optimizeAfterChanges = options.db.optimizeAfterChanges;
this.db.pragma('foreign_keys = on'); // Enforce consistency.
this.db.pragma('journal_mode = WAL'); // Be faster, expect local filesystem.
this.db.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs.
}
- authenticationUpsert(dbCtx, identifier, credential) {
+ authenticationUpsert(dbCtx, identifier, credential, otpKey) {
const _scope = _fileScope('authenticationUpsert');
const scrubbedCredential = '*'.repeat((credential || '').length);
- this.logger.debug(_scope, 'called', { identifier, scrubbedCredential });
+ const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null;
+ this.logger.debug(_scope, 'called', { identifier, scrubbedCredential, scrubbedOTPKey });
let result;
try {
- result = this.statement.authenticationUpsert.run({ identifier, credential });
+ result = this.statement.authenticationUpsert.run({ identifier, credential, otpKey });
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not upsert authentication');
}
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential, scrubbedOTPKey });
+ throw e;
+ }
+ }
+
+
+ authenticationUpdateOTPKey(dbCtx, identifier, otpKey) {
+ const _scope = _fileScope('authenticationUpdateOTPKey');
+ const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null;
+ this.logger.debug(_scope, 'called', { identifier, scrubbedOTPKey });
+
+ let result;
+ try {
+ result = this.statement.authenticationUpdateOtpKey.run({ identifier, otpKey });
+ if (result.changes != 1) {
+ throw new DBErrors.UnexpectedResult('did not update authentication otp key');
+ }
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedOTPKey });
+ throw e;
+ }
+ }
+
+
+ authenticationUpdateCredential(dbCtx, identifier, credential) {
+ const _scope = _fileScope('authenticationUpdateCredential');
+ const scrubbedCredential = '*'.repeat((credential || '').length);
+ this.logger.debug(_scope, 'called', { identifier, scrubbedCredential });
+
+ let result;
+ try {
+ result = this.statement.authenticationUpdateCredential.run({ identifier, credential });
+ if (result.changes != 1) {
+ throw new DBErrors.UnexpectedResult('did not update authentication credential');
+ }
} catch (e) {
this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential });
throw e;
}
-module.exports = DatabaseSQLite;
\ No newline at end of file
+module.exports = DatabaseSQLite;
--- /dev/null
+UPDATE authentication
+ SET credential = :credential
+ WHERE identifier = :identifier
--- /dev/null
+UPDATE authentication
+ SET otp_key = :otpKey
+ WHERE identifier = :identifier
--
INSERT INTO authentication
- (identifier, credential)
+ (identifier, credential, otp_key)
VALUES
- (:identifier, :credential)
+ (:identifier, :credential, :otpKey)
ON CONFLICT (identifier) DO UPDATE
SET
identifier = :identifier,
- credential = :credential
+ credential = :credential,
+ otp_key = :otpKey
--- /dev/null
+BEGIN;
+
+ ALTER TABLE authentication ADD COLUMN otp_key TEXT CHECK (typeof(otp_key) IN ('text', 'null'));
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0);
+
+COMMIT;
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nSQLite\nSchema 1.1.0",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ <tr><td port="">http_etag</td></tr>
+ <tr><td port="">http_last_modified</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ <tr><td port="">otp_key</td></tr>
+ </table>
+ >];
+
+}
--- /dev/null
+BEGIN;
+ ALTER TABLE authentication DROP COLUMN otp_key;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0;
+COMMIT;
const Enum = require('./enum');
const Manager = require('./manager');
const { Authenticator, SessionManager } = require('@squeep/authentication-module');
+const { initContext, navLinks } = require('./template/template-helper');
const path = require('path');
const _fileScope = common.fileScope(__filename);
this.on('POST', '/', this.handlerPostRoot.bind(this));
// Information page about service
- this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this));
+ this.on(['GET'], '/', this.handlerGetRoot.bind(this));
// Give load-balancers something to check
- this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck.bind(this));
+ this.on(['GET'], '/healthcheck', this.handlerGetHealthcheck.bind(this));
// Public information about topics
this.on('GET', '/info', this.handlerGetInfo.bind(this));
// These routes are intended for accessing static content during development.
// In production, a proxy server would likely handle these first.
- this.on(['GET', 'HEAD'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
- this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
- this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile.bind(this));
- this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
- this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
+ this.on(['GET'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
+ this.on(['GET'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
+ this.on(['GET'], '/static/:file', this.handlerGetStaticFile.bind(this));
+ this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
+ this.on(['GET'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
// Private informational endpoints
- this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
- this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
- this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
- this.on(['GET', 'HEAD'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this));
+ this.on(['GET'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
+ this.on(['GET'], '/admin/', this.handlerGetAdminOverview.bind(this));
+ this.on(['GET'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
+ this.on(['GET'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this));
// Private data-editing endpoints
this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
this.on('POST', '/admin/process', this.handlerPostAdminProcess.bind(this));
// Admin login
- this.on(['GET', 'HEAD'], '/admin/login', this.handlerGetAdminLogin.bind(this));
+ this.on(['GET'], '/admin/login', this.handlerGetAdminLogin.bind(this));
this.on(['POST'], '/admin/login', this.handlerPostAdminLogin.bind(this));
this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout.bind(this));
this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA.bind(this));
+ this.on(['GET'], '/admin/settings', this.handlerGetAdminSettings.bind(this));
+ this.on(['POST'], '/admin/settings', this.handlerPostAdminSettings.bind(this));
}
*/
async preHandler(req, res, ctx) {
await super.preHandler(req, res, ctx);
+ ctx.url = req.url; // Persisted for logout redirect
+
const logObject = this.asyncLocalStorage.getStore();
// FIXME: for some reason, returning from the super.preHandler sometimes loses async context?
// Workaround until cause and solution are found.
const _scope = _fileScope('handlerPostRoot');
this.logger.debug(_scope, 'called', { req, ctx });
+ initContext(ctx);
+
this.setResponseType(this.responseTypes, req, res, ctx);
await this.ingestBody(req, res, ctx);
];
this.logger.debug(_scope, 'called', { req, ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ initContext(ctx);
this.setResponseType(responseTypes, req, res, ctx);
async handlerGetHealthcheck(req, res, ctx) {
const _scope = _fileScope('handlerGetHealthcheck');
this.logger.debug(_scope, 'called', { req, ctx });
-
- Dingus.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
- Dingus.setHeadHandler(req, res, ctx);
-
this.setResponseType(responseTypes, req, res, ctx);
await this.manager.getInfo(res, ctx);
const responseTypes = [Enum.ContentType.ImageSVG];
- Dingus.setHeadHandler(req, res, ctx);
-
this.setResponseType(responseTypes, req, res, ctx);
await this.manager.getHistorySVG(res, ctx);
const _scope = _fileScope('handlerGetAdminOverview');
this.logger.debug(_scope, 'called', { req, ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ initContext(ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetAdminTopicDetails');
this.logger.debug(_scope, 'called', { req, ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ initContext(ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
ctx.method = req.method;
await this.manager.updateSubscription(res, ctx);
}
-
+
/**
* @param {http.ClientRequest} req request
const _scope = _fileScope('handlerGetAdminLogin');
this.logger.debug(_scope, 'called', { req, ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ initContext(ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const _scope = _fileScope('handlerPostAdminLogin');
this.logger.debug(_scope, 'called', { req, ctx });
+ initContext(ctx);
+
this.setResponseType(this.responseTypes, req, res, ctx);
await this.authenticator.sessionOptionalLocal(req, res, ctx);
}
+ /**
+ * Delegate account settings to authentication module.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ */
+ async handlerGetAdminSettings(req, res, ctx) {
+ const _scope = _fileScope('handlerGetAdminSettings');
+ this.logger.debug(_scope, 'called', { req, ctx });
+
+ initContext(ctx);
+
+ this.setResponseType(this.responseTypes, req, res, ctx);
+
+ if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) {
+ await this.sessionManager.getAdminSettings(res, ctx, navLinks);
+ }
+ }
+
+
+ /**
+ * Delegate account settings to authentication module.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ */
+ async handlerPostAdminSettings(req, res, ctx) {
+ const _scope = _fileScope('handlerPostAdminSettings');
+ this.logger.debug(_scope, 'called', { req, ctx });
+
+ initContext(ctx);
+
+ this.setResponseType(this.responseTypes, req, res, ctx);
+
+ if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) {
+ await this.maybeIngestBody(req, res, ctx);
+ await this.sessionManager.postAdminSettings(res, ctx, navLinks);
+ }
+ }
+
+
/**
* Delegate login to authentication module.
* @param {http.ClientRequest} req request
const _scope = _fileScope('handlerGetAdminLogout');
this.logger.debug(_scope, 'called', { req, ctx });
+ initContext(ctx);
+
this.setResponseType(this.responseTypes, req, res, ctx);
await this.authenticator.sessionOptionalLocal(req, res, ctx);
const _scope = _fileScope('handlerGetAdminIA');
this.logger.debug(_scope, 'called', { req, ctx });
- this.setResponseType(this.responseTypes, req, res, ctx);
+ initContext(ctx);
- // Special case here, to see cookie before session established
- ctx.cookie = req.getHeader(Enum.Header.Cookie);
+ this.setResponseType(this.responseTypes, req, res, ctx);
await this.sessionManager.getAdminIA(res, ctx);
}
'use strict';
const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
/**
* Show a summary of all topics.
* @returns {string} html
*/
module.exports = (ctx, options) => {
+ const pagePathLevel = 1;
const pageTitle = `${options.manager.pageTitle} - Topics`;
const logoUrl = options.manager.logoUrl;
const footerEntries = options.manager.footerEntries;
}
const htmlOptions = {
+ pageIdentifier: 'admin',
pageTitle,
logoUrl,
footerEntries,
};
+ th.navLinks(pagePathLevel, ctx, htmlOptions);
+ sessionNavLinks(pagePathLevel, ctx, htmlOptions);
const content = [
` <section class="topics">
</section>`,
];
- return th.htmlPage(1, ctx, htmlOptions, content);
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, content);
};
\ No newline at end of file
'use strict';
const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
/**
* Show a topic with all of its subscribers.
* @returns {string} html
*/
module.exports = (ctx, options) => {
+ const pagePathLevel = 2;
const pageTitle = `${options.manager.pageTitle} - Topic Details`;
const logoUrl = options.manager.logoUrl;
const navLinks = [
}
const htmlOptions = {
+ pageIdentifier: 'admin',
pageTitle,
logoUrl,
navLinks,
footerEntries,
};
+ th.navLinks(pagePathLevel, ctx, htmlOptions);
+ sessionNavLinks(pagePathLevel, ctx, htmlOptions);
const content = [
` <section class="topics">
</section>`,
];
- return th.htmlPage(2, ctx, htmlOptions, content);
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, content);
};
\ No newline at end of file
'use strict';
const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
/**
*
* @returns {string} html
*/
module.exports = (ctx, options) => {
+ const pagePathLevel = 0;
const pageTitle = options.manager.pageTitle;
const isPublicHub = options.manager.publicHub;
const contactHTML = options.adminContactHTML;
const footerEntries = options.manager.footerEntries;
const hubURL = options.dingus.selfBaseUrl || '<s>https://hub.example.com/</s>';
- const navLinks = [{
- href: 'admin/',
- text: 'Admin',
- }];
const htmlOptions = {
+ pageIdentifier: 'root',
pageTitle,
logoUrl: options.manager.logoUrl,
footerEntries,
- navLinks,
+ navLinks: [],
};
+ th.navLinks(pagePathLevel, ctx, htmlOptions);
+ sessionNavLinks(pagePathLevel, ctx, htmlOptions);
const content = [
aboutSection(),
usageSection(isPublicHub, hubURL),
contactSection(contactHTML),
hAppSection(pageTitle, options.manager.logoUrl),
];
- return th.htmlPage(0, ctx, htmlOptions, content);
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, content);
};
\ No newline at end of file
}[c]));
}
+
+/**
+ * Add common site links to navigation header.
+ * @param {number} pagePathLevel depth from root
+ * @param {object} ctx context
+ * @param {object} options options
+ */
+function navLinks(pagePathLevel, ctx, options) {
+ if (!options.navLinks) {
+ options.navLinks = [];
+ }
+ const rootPath = '../'.repeat(pagePathLevel);
+
+ if (options.pageIdentifier !== 'admin') {
+ options.navLinks.push({
+ text: 'Admin',
+ href: `${rootPath}admin/`,
+ });
+ }
+}
+
+
module.exports = Object.assign(Object.create(TemplateHelper), {
+ navLinks,
xmlEscape,
renderTopicRowHeader,
renderTopicRow,
renderSubscriptionRowHeader,
renderSubscriptionRow,
-});
\ No newline at end of file
+});
-* {}
html {
height: 100vh;
}
.errors ul {
border: 1px solid red;
}
+.otp-key-qr svg {
+ height: 30vh;
+}
table {
border: 0;
width: 100%;
const assert = require('node:assert');
const sinon = require('sinon');
-const common = require('../../../src/common');
const DB = require('../../../src/db');
const DBErrors = require('../../../src/db/errors');
const DatabasePostgres = require('../../../src/db/postgres');
const DatabaseSQLite = require('../../../src/db/sqlite');
+const stubLogger = require('../../stub-logger');
describe('DatabaseFactory', function () {
let logger, options;
beforeEach(function () {
- logger = common.nullLogger;
+ logger = stubLogger,
options = {
db: {
connectionString: '',
});
describe('Authentication', function () {
- let identifier, credential;
+ let identifier, credential, otpKey;
beforeEach(function () {
identifier = 'username';
credential = 'myEncryptedPassword';
+ otpKey = '1234567890123456789012';
});
- step('create auth entry', async function() {
+ step('create auth entry', async function () {
await db.context(async (dbCtx) => {
await db.authenticationUpsert(dbCtx, identifier, credential);
});
});
- step('get auth entry', async function() {
+ step('get auth entry', async function () {
await db.context(async (dbCtx) => {
const authInfo = await db.authenticationGet(dbCtx, identifier);
assert.strictEqual(authInfo.credential, credential);
});
});
- step('valid auth event', async function() {
+ step('valid auth event', async function () {
await db.context(async (dbCtx) => {
await db.authenticationSuccess(dbCtx, identifier);
const authInfo = await db.authenticationGet(dbCtx, identifier);
assert.notStrictEqual(authInfo.lastAuthentication, undefined);
});
});
- step('update auth entry', async function() {
+ step('update auth entry', async function () {
await db.context(async (dbCtx) => {
credential = 'myNewPassword';
- await db.authenticationUpsert(dbCtx, identifier, credential);
+ await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
+ const authInfo = await db.authenticationGet(dbCtx, identifier);
+ assert.strictEqual(authInfo.credential, credential);
+ assert.strictEqual(authInfo.otpKey, otpKey);
+ });
+ });
+ step('update auth otp key', async function () {
+ await db.context(async (dbCtx) => {
+ const removedOTPKey = null;
+ await db.authenticationUpdateOTPKey(dbCtx, identifier, removedOTPKey);
+ const authInfo = await db.authenticationGet(dbCtx, identifier);
+ assert.strictEqual(authInfo.otpKey, removedOTPKey);
+ });
+ });
+ step('update credential', async function () {
+ await db.context(async (dbCtx) => {
+ credential = '$plain$anotherCredential';
+ await db.authenticationUpdateCredential(dbCtx, identifier, credential);
const authInfo = await db.authenticationGet(dbCtx, identifier);
assert.strictEqual(authInfo.credential, credential);
});
}); // authenticationGet
describe('authenticationUpsert', function () {
- let identifier, credential;
+ let identifier, credential, otpKey;
beforeEach(function () {
identifier = 'username';
credential = '$z$foo';
+ otpKey = '12345678901234567890123456789012';
});
it('success', async function () {
const dbResult = {
duration: 22,
};
sinon.stub(db.db, 'result').resolves(dbResult);
- await db.authenticationUpsert(dbCtx, identifier, credential);
+ await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
});
it('failure', async function() {
credential = undefined;
};
sinon.stub(db.db, 'result').resolves(dbResult);
try {
- await db.authenticationUpsert(dbCtx, identifier, credential);
+ await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
});
}); // authenticationUpsert
+ describe('authenticationUpdateCredential', function () {
+ let identifier, credential;
+ beforeEach(function () {
+ identifier = 'username';
+ });
+ it('success', async function () {
+ const dbResult = {
+ rowCount: 1,
+ rows: undefined,
+ duration: 22,
+ };
+ sinon.stub(db.db, 'result').resolves(dbResult);
+ await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+ });
+ it('failure', async function() {
+ credential = undefined;
+ const dbResult = {
+ rowCount: 0,
+ rows: undefined,
+ duration: 22,
+ };
+ sinon.stub(db.db, 'result').resolves(dbResult);
+ try {
+ await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ }); // authenticationUpdateCredential
+
+ describe('authenticationUpdateOTPKey', function () {
+ let identifier, otpKey;
+ beforeEach(function () {
+ identifier = 'username';
+ otpKey = '12345678901234567890123456789012';
+ });
+ it('success', async function () {
+ const dbResult = {
+ rowCount: 1,
+ rows: undefined,
+ duration: 22,
+ };
+ sinon.stub(db.db, 'result').resolves(dbResult);
+ await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+ });
+ it('failure', async function() {
+ const dbResult = {
+ rowCount: 0,
+ rows: undefined,
+ duration: 22,
+ };
+ sinon.stub(db.db, 'result').resolves(dbResult);
+ try {
+ await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ }); // authenticationUpdateOTPKey
+
describe('subscriptionsByTopicId', function () {
it('success', async function () {
const expected = [];
describe('topicGetByUrl', function () {
it('success', async function() {
- const expected = [];
+ const expected = { id: topicId };
sinon.stub(db.db, 'oneOrNone').resolves(expected);
const result = await db.topicGetByUrl(dbCtx, topicUrl);
assert.deepStrictEqual(result, expected);
});
+ it('success, no default', async function() {
+ const expected = { id: topicId };
+ sinon.stub(db.db, 'oneOrNone').resolves(expected);
+ const result = await db.topicGetByUrl(dbCtx, topicUrl, false);
+ assert.deepStrictEqual(result, expected);
+ });
it('failure', async function () {
const expected = new Error();
sinon.stub(db.db, 'oneOrNone').throws(expected);
sinon.restore();
});
+ it('covers options', function () {
+ const xoptions = new Config('test');
+ delete xoptions.db.connectionString;
+ db = new DB(stubLogger, xoptions);
+ });
+
// Ensure all interface methods are implemented
describe('Implementation', function () {
it('implements interface', async function () {
}); // authenticationGet
describe('authenticationUpsert', function () {
- let identifier, credential;
+ let identifier, credential, otpKey;
beforeEach(function () {
identifier = 'username';
credential = '$z$foo';
+ otpKey = '12345678901234567890123456789012';
});
it('success', async function() {
const dbResult = {
lastInsertRowid: undefined,
};
sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
- await db.authenticationUpsert(dbCtx, identifier, credential);
+ await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
});
it('failure', async function () {
const dbResult = {
};
sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
try {
- await db.authenticationUpsert(dbCtx, identifier, credential);
+ await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ }); // authenticationUpsert
+
+ describe('authenticationUpdateCredential', function () {
+ let identifier, credential;
+ beforeEach(function () {
+ identifier = 'username';
+ credential = '$z$foo';
+ });
+ it('success', async function() {
+ const dbResult = {
+ changes: 1,
+ lastInsertRowid: undefined,
+ };
+ sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
+ await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+ });
+ it('failure', async function () {
+ const dbResult = {
+ changes: 0,
+ lastInsertRowid: undefined,
+ };
+ sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
+ try {
+ await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ }); // authenticationUpdateCredential
+
+ describe('authenticationUpdateOTPKey', function () {
+ let identifier, otpKey;
+ beforeEach(function () {
+ identifier = 'username';
+ otpKey = '12345678901234567890123456789012';
+ });
+ it('success', async function() {
+ const dbResult = {
+ changes: 1,
+ lastInsertRowid: undefined,
+ };
+ sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
+ await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+ });
+ it('failure', async function () {
+ const dbResult = {
+ changes: 0,
+ lastInsertRowid: undefined,
+ };
+ sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
+ try {
+ await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
const result = await db.topicGetByUrl(dbCtx, topicUrl);
assert.deepStrictEqual(result, expected);
});
+ it('success, no defaults', async function() {
+ const expected = [];
+ sinon.stub(db.statement.topicGetByUrl, 'get').returns(expected);
+ const result = await db.topicGetByUrl(dbCtx, topicUrl, false);
+ assert.deepStrictEqual(result, expected);
+ });
it('failure', async function () {
const expected = new Error();
sinon.stub(db.statement.topicGetByUrl, 'get').throws(expected);
});
}); // handlerGetAdminLogin
+ describe('handlerGetAdminSettings', function () {
+ it('covers logged in', async function () {
+ service.authenticator.sessionRequiredLocal.resolves(true);
+ await service.handlerGetAdminSettings(req, res, ctx);
+ assert(service.sessionManager.getAdminSettings.called);
+ });
+ it('covers not logged in', async function () {
+ service.authenticator.sessionRequiredLocal.resolves(false);
+ await service.handlerGetAdminSettings(req, res, ctx);
+ assert(service.sessionManager.getAdminSettings.notCalled);
+ });
+ }); // handlerGetAdminSettings
+
+ describe('handlerPostAdminSettings', function () {
+ it('covers logged in', async function () {
+ service.authenticator.sessionRequiredLocal.resolves(true);
+ sinon.stub(service, 'bodyData').resolves();
+ await service.handlerPostAdminSettings(req, res, ctx);
+ assert(service.sessionManager.postAdminSettings.called);
+ });
+ it('covers logged outo', async function () {
+ service.authenticator.sessionRequiredLocal.resolves(false);
+ sinon.stub(service, 'bodyData').resolves();
+ await service.handlerPostAdminSettings(req, res, ctx);
+ assert(service.sessionManager.postAdminSettings.notCalled);
+ });
+ }); // handlerPostAdminSettings
+
describe('handlerPostAdminLogin', function () {
it('covers', async function () {
sinon.stub(service, 'bodyData').resolves();
'authenticationSuccess',
'authenticationGet',
'authenticationUpsert',
+ 'authenticationUpdateCredential',
+ 'authenticationUpdateOTPKey',
'healthCheck',
'initialize',
'subscriptionsByTopicId',