update dependencies, fixes to support new authentication features
authorJustin Wind <justin.wind+git@gmail.com>
Fri, 21 Jun 2024 00:47:28 +0000 (17:47 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Fri, 21 Jun 2024 00:47:28 +0000 (17:47 -0700)
34 files changed:
CHANGELOG.md
config/index.js
documentation/media/postgres-er.svg
documentation/media/sqlite-er.svg
package-lock.json
package.json
src/common.js
src/db/base.js
src/db/postgres/index.js
src/db/postgres/sql/authentication-update-credential.sql [new file with mode: 0644]
src/db/postgres/sql/authentication-update-otp-key.sql [new file with mode: 0644]
src/db/postgres/sql/authentication-upsert.sql
src/db/postgres/sql/schema/1.1.0/apply.sql [new file with mode: 0644]
src/db/postgres/sql/schema/1.1.0/er.dot [new file with mode: 0644]
src/db/postgres/sql/schema/1.1.0/revert.sql [new file with mode: 0644]
src/db/sqlite/index.js
src/db/sqlite/sql/authentication-update-credential.sql [new file with mode: 0644]
src/db/sqlite/sql/authentication-update-otp-key.sql [new file with mode: 0644]
src/db/sqlite/sql/authentication-upsert.sql
src/db/sqlite/sql/schema/1.1.0/apply.sql [new file with mode: 0644]
src/db/sqlite/sql/schema/1.1.0/er.dot [new file with mode: 0644]
src/db/sqlite/sql/schema/1.1.0/revert.sql [new file with mode: 0644]
src/service.js
src/template/admin-overview-html.js
src/template/admin-topic-details-html.js
src/template/root-html.js
src/template/template-helper.js
static/theme.css
test/src/db/factory.js
test/src/db/integration.js
test/src/db/postgres.js
test/src/db/sqlite.js
test/src/service.js
test/stub-db.js

index fa404224cddbb7882f3836e028389d9dac858474..375376fe596da7939de9e6c1dd7d75507c13b98d 100644 (file)
@@ -2,6 +2,16 @@
 
 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
index d06162efd0431d4a980990c9a753168eefb853f5..8f31211317d5689ae240a4a63a708f72b26ecd8c 100644 (file)
@@ -5,6 +5,11 @@ const common = require('../src/common');
 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');
index 86105934cff913de1e5e60130c6f2bf4935cf35d..26a00ff7428f8d8df62c2b85f206dfa92447cb92 100644 (file)
@@ -1,7 +1,7 @@
 <?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"
@@ -11,7 +11,7 @@
 <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&#45;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>
index 91752cb05f53004e6ff7ab63463d4a943b54a9f0..0d151f41186b93cc694552de79499581d421f299 100644 (file)
@@ -1,7 +1,7 @@
 <?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"
@@ -11,7 +11,7 @@
 <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&#45;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>
index a8b6d27e72690469072f7158282d825e15d3ea0d..b09d6a71a15fabe902de217383c54bd4cf990ef6 100644 (file)
@@ -9,9 +9,9 @@
       "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",
index d457c0988c566fae83aacc361e7f108500d9c24a..8da00340b0a5dd1576efb5de1d039d114637207d 100644 (file)
   "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",
index 7d0d3180bba4dec235c0bf49053b00f5b09dc1e6..0d6500c44eaaf7b201e96ecef698931761423005 100644 (file)
@@ -164,6 +164,8 @@ const logTruncate = (str, len) => {
   return str.toString().slice(0, len) + `... (${str.toString().length} bytes)`;
 };
 
+const nop = () => undefined;
+
 module.exports = {
   ...common,
   arrayChunk,
@@ -172,6 +174,7 @@ module.exports = {
   ensureArray,
   freezeDeep,
   logTruncate,
+  nop,
   randomBytesAsync,
   stackSafePush,
   topicLeaseDefaults,
index 2be2db0dea69d106f064c45e4639df95b72f6b74..8331fb2336973a82c9223d5d3afadf00dfd62710 100644 (file)
@@ -12,9 +12,8 @@ const svh = require('./schema-version-helper');
 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
@@ -327,12 +326,35 @@ class Database {
    * @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
index 9cacaeca26fce14a17df57c6d58f88d6388fa1e5..78a4dab6fb775bdf69ba8faaea25fc5e129d6914 100644 (file)
@@ -29,8 +29,8 @@ const schemaVersionsSupported = {
   },
   max: {
     major: 1,
-    minor: 0,
-    patch: 4,
+    minor: 1,
+    patch: 0,
   },
 };
 
@@ -369,17 +369,36 @@ class DatabasePostgres extends Database {
   }
 
 
-  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;
@@ -387,6 +406,24 @@ class DatabasePostgres extends Database {
   }
 
 
+  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 });
diff --git a/src/db/postgres/sql/authentication-update-credential.sql b/src/db/postgres/sql/authentication-update-credential.sql
new file mode 100644 (file)
index 0000000..8d48498
--- /dev/null
@@ -0,0 +1,3 @@
+UPDATE authentication
+       SET credential = $(credential)
+       WHERE identifier = $(identifier)
diff --git a/src/db/postgres/sql/authentication-update-otp-key.sql b/src/db/postgres/sql/authentication-update-otp-key.sql
new file mode 100644 (file)
index 0000000..b639c69
--- /dev/null
@@ -0,0 +1,3 @@
+UPDATE authentication
+       SET otp_key = $(otpKey)
+       WHERE identifier = $(identifier)
index e86d4fbc17674ed2ebcc8a5f2f583bd7268a4ed8..f37892fac77b505b42aae0513af4211725a73dd2 100644 (file)
@@ -1,9 +1,10 @@
 --
 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)
diff --git a/src/db/postgres/sql/schema/1.1.0/apply.sql b/src/db/postgres/sql/schema/1.1.0/apply.sql
new file mode 100644 (file)
index 0000000..64be4d4
--- /dev/null
@@ -0,0 +1,7 @@
+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
diff --git a/src/db/postgres/sql/schema/1.1.0/er.dot b/src/db/postgres/sql/schema/1.1.0/er.dot
new file mode 100644 (file)
index 0000000..8dcd3b0
--- /dev/null
@@ -0,0 +1,140 @@
+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>
+       >];
+
+}
diff --git a/src/db/postgres/sql/schema/1.1.0/revert.sql b/src/db/postgres/sql/schema/1.1.0/revert.sql
new file mode 100644 (file)
index 0000000..fbcc115
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN;
+       ALTER TABLE authentication DROP COLUMN otp_key;
+
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0;
+COMMIT;
index 03252110dfcf7ddd897134e76e83ad39010c6dc4..6100d491a909547912cc6bdea61419706b4962e3 100644 (file)
@@ -19,8 +19,8 @@ const schemaVersionsSupported = {
   },
   max: {
     major: 1,
-    minor: 0,
-    patch: 4,
+    minor: 1,
+    patch: 0,
   },
 };
 
@@ -48,7 +48,7 @@ class DatabaseSQLite extends Database {
     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.
@@ -290,17 +290,54 @@ class DatabaseSQLite extends Database {
   }
 
 
-  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;
@@ -1149,4 +1186,4 @@ class DatabaseSQLite extends Database {
 
 }
 
-module.exports = DatabaseSQLite;
\ No newline at end of file
+module.exports = DatabaseSQLite;
diff --git a/src/db/sqlite/sql/authentication-update-credential.sql b/src/db/sqlite/sql/authentication-update-credential.sql
new file mode 100644 (file)
index 0000000..021b9ec
--- /dev/null
@@ -0,0 +1,3 @@
+UPDATE authentication
+       SET credential = :credential
+       WHERE identifier = :identifier
diff --git a/src/db/sqlite/sql/authentication-update-otp-key.sql b/src/db/sqlite/sql/authentication-update-otp-key.sql
new file mode 100644 (file)
index 0000000..d52506b
--- /dev/null
@@ -0,0 +1,3 @@
+UPDATE authentication
+       SET otp_key = :otpKey
+       WHERE identifier = :identifier
index 8a141ee49bbc748d29af0316d7ea586e6c925fa9..b83071926d346834adf7b4749540489760c17c8a 100644 (file)
@@ -1,9 +1,10 @@
 --
 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
diff --git a/src/db/sqlite/sql/schema/1.1.0/apply.sql b/src/db/sqlite/sql/schema/1.1.0/apply.sql
new file mode 100644 (file)
index 0000000..fdfd499
--- /dev/null
@@ -0,0 +1,7 @@
+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;
diff --git a/src/db/sqlite/sql/schema/1.1.0/er.dot b/src/db/sqlite/sql/schema/1.1.0/er.dot
new file mode 100644 (file)
index 0000000..0436d73
--- /dev/null
@@ -0,0 +1,140 @@
+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>
+       >];
+
+}
diff --git a/src/db/sqlite/sql/schema/1.1.0/revert.sql b/src/db/sqlite/sql/schema/1.1.0/revert.sql
new file mode 100644 (file)
index 0000000..fbcc115
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN;
+       ALTER TABLE authentication DROP COLUMN otp_key;
+
+       DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0;
+COMMIT;
index 742bcda0f7ae46bfa46237cb28d6272f2327c95b..fbbb1c48238142b620ccc7d162ba29244ccaaa5c 100644 (file)
@@ -10,6 +10,7 @@ const common = require('./common');
 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);
@@ -31,10 +32,10 @@ class Service extends Dingus {
     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));
@@ -42,17 +43,17 @@ class Service extends Dingus {
 
     // 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));
@@ -62,10 +63,12 @@ class Service extends Dingus {
     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));
 
   }
 
@@ -81,6 +84,8 @@ class Service extends Dingus {
    */
   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.
@@ -102,6 +107,8 @@ class Service extends Dingus {
     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);
 
@@ -121,7 +128,7 @@ class Service extends Dingus {
     ];
     this.logger.debug(_scope, 'called', { req, ctx });
 
-    Dingus.setHeadHandler(req, res, ctx);
+    initContext(ctx);
 
     this.setResponseType(responseTypes, req, res, ctx);
 
@@ -139,8 +146,6 @@ class Service extends Dingus {
   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);
 
@@ -159,8 +164,6 @@ class Service extends Dingus {
 
     const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
 
-    Dingus.setHeadHandler(req, res, ctx);
-
     this.setResponseType(responseTypes, req, res, ctx);
 
     await this.manager.getInfo(res, ctx);
@@ -173,8 +176,6 @@ class Service extends Dingus {
 
     const responseTypes = [Enum.ContentType.ImageSVG];
 
-    Dingus.setHeadHandler(req, res, ctx);
-
     this.setResponseType(responseTypes, req, res, ctx);
 
     await this.manager.getHistorySVG(res, ctx);
@@ -190,7 +191,7 @@ class Service extends Dingus {
     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);
 
@@ -209,7 +210,7 @@ class Service extends Dingus {
     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);
 
@@ -269,7 +270,7 @@ class Service extends Dingus {
     ctx.method = req.method;
     await this.manager.updateSubscription(res, ctx);
   }
-  
+
 
   /**
    * @param {http.ClientRequest} req request
@@ -298,7 +299,7 @@ class Service extends Dingus {
     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);
 
@@ -316,6 +317,8 @@ class Service extends Dingus {
     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);
@@ -326,6 +329,47 @@ class Service extends Dingus {
   }
 
 
+  /**
+   * 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
@@ -336,6 +380,8 @@ class Service extends Dingus {
     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);
@@ -354,10 +400,9 @@ class Service extends Dingus {
     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);
   }
index fd0261f53c85618eb6310e1fdd53bf2115bacb27..948bdbb0083fa77917a7f5b96dc61eb94557be92 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
 
 /**
  * Show a summary of all topics.
@@ -12,6 +13,7 @@ const th = require('./template-helper');
  * @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;
@@ -20,10 +22,13 @@ module.exports = (ctx, options) => {
   }
 
   const htmlOptions = {
+    pageIdentifier: 'admin',
     pageTitle,
     logoUrl,
     footerEntries,
   };
+  th.navLinks(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
 
   const content = [
     `      <section class="topics">
@@ -39,5 +44,5 @@ module.exports = (ctx, options) => {
       </section>`,
   ];
 
-  return th.htmlPage(1, ctx, htmlOptions, content);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, content);
 };
\ No newline at end of file
index a70c510633b810c2a7508d6ce80841b57d22c988..b598313e1c098921719d711e660f85896506b792 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
 
 /**
  * Show a topic with all of its subscribers.
@@ -13,6 +14,7 @@ const th = require('./template-helper');
  * @returns {string} html
  */
 module.exports = (ctx, options) => {
+  const pagePathLevel = 2;
   const pageTitle = `${options.manager.pageTitle} - Topic Details`;
   const logoUrl = options.manager.logoUrl;
   const navLinks = [
@@ -27,11 +29,14 @@ module.exports = (ctx, options) => {
   }
 
   const htmlOptions = {
+    pageIdentifier: 'admin',
     pageTitle,
     logoUrl,
     navLinks,
     footerEntries,
   };
+  th.navLinks(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
 
   const content = [
     `      <section class="topics">
@@ -68,5 +73,5 @@ module.exports = (ctx, options) => {
       </section>`,
   ];
 
-  return th.htmlPage(2, ctx, htmlOptions, content);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, content);
 };
\ No newline at end of file
index d035bb8c342da6ca94b3f93fd4f25cfcb1398a4d..575d4bcbdf439c0287f1bc565e15fb4d10834ab0 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const th = require('./template-helper');
+const { sessionNavLinks } = require('@squeep/authentication-module');
 
 /**
  *
@@ -164,26 +165,26 @@ ${contactHTML}
  * @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
index f891e4fe65949633d7326d0dd644b9e85510fb52..5850fc1256604c955fdf973931b0e93ed97d2734 100644 (file)
@@ -134,10 +134,33 @@ function xmlEscape(string) {
   }[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
+});
index f6099e2a74e8bcacac6858ec77daf3bf82e413b5..3939c55a07fd43f0c923c4a1577cc7c84516f186 100644 (file)
@@ -1,4 +1,3 @@
-* {}
 html {
        height: 100vh;
 }
@@ -87,6 +86,9 @@ section + section {
 .errors ul {
        border: 1px solid red;
 }
+.otp-key-qr svg {
+       height: 30vh;
+}
 table {
        border: 0;
        width: 100%;
index ebb89ff45657077f5847ebec868938d754fb0349..d26d51a2dbb440f56db1940b06837b3f380838cb 100644 (file)
@@ -2,16 +2,16 @@
 
 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: '',
index 7eb7d91670295b0f4bafad18f15b2e8bb31e7f56..eed37e2c55f5070c4031f4c47d1a8491ace96588 100644 (file)
@@ -83,33 +83,51 @@ describe('Database Integration', function () {
       });
 
       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);
           });
index 1cdcfda44c94eee4d56a508b2cda717c6a323291..825a295fc93a2e7bdd515755b2ca6348f9b0ba1f 100644 (file)
@@ -486,10 +486,11 @@ describe('DatabasePostgres', 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 = {
@@ -498,7 +499,7 @@ describe('DatabasePostgres', function () {
         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;
@@ -509,7 +510,7 @@ describe('DatabasePostgres', function () {
       };
       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);
@@ -517,6 +518,68 @@ describe('DatabasePostgres', function () {
     });
   }); // 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 = [];
@@ -1189,11 +1252,17 @@ describe('DatabasePostgres', function () {
 
   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);
index 8d25fc8dda85c3427559ca6a10e8203d1a1c8caf..33f00282d9d80ff9b1e1d2278afe4e8cebdf4481 100644 (file)
@@ -43,6 +43,12 @@ describe('DatabaseSQLite', function () {
     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 () {
@@ -337,10 +343,11 @@ describe('DatabaseSQLite', 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 = {
@@ -348,7 +355,7 @@ describe('DatabaseSQLite', function () {
         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 = {
@@ -357,7 +364,65 @@ describe('DatabaseSQLite', function () {
       };
       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);
@@ -1042,6 +1107,12 @@ describe('DatabaseSQLite', function () {
       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);
index 8be37e2416b7310d3f3ccdcd1886728f913872e0..2cfe38d66ed4b55115ebb070dcc4301c1f7b9f1d 100644 (file)
@@ -188,6 +188,34 @@ describe('Service', function () {
     });
   }); // 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();
index e0c7ad0b5d8c5220d09e06537375a1dfce49a28d..129c9071f98178dd91fb110787959558715ecf68 100644 (file)
@@ -12,6 +12,8 @@ const stubFns = [
   'authenticationSuccess',
   'authenticationGet',
   'authenticationUpsert',
+  'authenticationUpdateCredential',
+  'authenticationUpdateOTPKey',
   'healthCheck',
   'initialize',
   'subscriptionsByTopicId',