support interaction between module and apps for updating templates before rendering
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 23 Mar 2024 20:54:17 +0000 (13:54 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sat, 23 Mar 2024 20:54:17 +0000 (13:54 -0700)
14 files changed:
CHANGELOG.md
README.md
index.js
lib/session-manager.js
lib/template/helpers.js [new file with mode: 0644]
lib/template/ia-html.js
lib/template/index.js
lib/template/login-html.js
lib/template/otp-html.js
lib/template/settings-html.js
package-lock.json
package.json
test/lib/template/helpers.js [new file with mode: 0644]
test/lib/template/login-html.js

index 0d7f4e74875046f5533d98aa8e6d61f52b9ba5bf..893b4c709774ff010bc4721a21d9d0283f03a8b7 100644 (file)
@@ -6,7 +6,9 @@ Releases and notable changes to this project are documented here.
 
 ### Added
 
-- TOTP 2FA support
+- TOTP 2FA support.
+- Add appCb parameter to session manager page methods to allow mogrifying template data before rendering, exempli gratia adding app navLinks to account management pages.
+- Add sessinNavLinks helper function for populating navLinks in app templates.
 - Settings page for updating credentials and OTP.
 - Helper function for securely reading credential from stdin.
 - Replaced debug authn with plaintext.
index c67283e42ee29773b134e27bd12b8b4a745fc5b5..6b4400ea56b2285729fd59e3c6fcf307d71a89d4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -34,9 +34,17 @@ Class providing service handler functions for rendering and processing session l
   for local users, or redirecting to IndieAuth server and persisting transient state
   in session cookie.
 - `getAdminIA` interprets the returning redirect from the IndieAuth server.
+- `getAdminSettings` renders the HTML account settings form
+- `postAdminSettings` ingests and acts on account updates
+
+### Helpers
+
+- `sessionNavLinks` call from app templates to populate navLinks for account settings and logout
 
 ### Other Notes
 
+For the moment, this imposes a web structure of /admin/* for authentication management paths.
+
 The logger used should be able to mask these context fields:
 
 - `ctx.parsedBody.credential`
index 53b799ea719bfa6b21fee757dfa6f3d44aab176f..5d9da0c4c3b931845ea1edca8fbfdccca3cef3da 100644 (file)
--- a/index.js
+++ b/index.js
@@ -3,9 +3,11 @@
 const Authenticator = require('./lib/authenticator');
 const SessionManager = require('./lib/session-manager');
 const stdioCredential = require('./lib/stdio-credential');
+const templateHelpers = require('./lib/template/helpers');
 
 module.exports = {
   Authenticator,
   SessionManager,
   stdioCredential,
+  ...templateHelpers,
 };
index d1045ff570674c608585752b5f7a9e900490c221..2d3d96cd10706e07512ee3d908a6d849adb829b5 100644 (file)
@@ -73,12 +73,17 @@ class SessionManager {
     await this._sessionCookieSet(res, undefined, 0, path);
   }
 
+  /**
+   * @typedef {(pagePathLevel: Number, ctx: Object, htmlOptions: Object) => void} AppTemplateCallback
+   */
+
   /**
    * GET request for establishing admin session.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
+   * @param {AppTemplateCallback} appCb
    */
-  async getAdminLogin(res, ctx) {
+  async getAdminLogin(res, ctx, appCb) {
     const _scope = _fileScope('getAdminLogin');
     this.logger.debug(_scope, 'called', { ctx });
 
@@ -94,7 +99,7 @@ class SessionManager {
       res.setHeader(Enum.Header.Location, redirect);
       res.end();
     } else {
-      res.end(Template.LoginHTML(ctx, this.options));
+      res.end(Template.LoginHTML(ctx, this.options, appCb));
     }
 
     this.logger.info(_scope, 'finished', { ctx });
@@ -105,20 +110,21 @@ class SessionManager {
    * POST request for taking form data to establish admin session.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
+   * @param {AppTemplateCallback} appCb
    */
-  async postAdminLogin(res, ctx) {
+  async postAdminLogin(res, ctx, appCb) {
     const _scope = _fileScope('postAdminLogin');
     this.logger.debug(_scope, 'called', { ctx });
 
     ctx.errors = [];
 
     // Check if this was an OTP entry attempt.
-    if (await this._otpSubmission(res, ctx)) {
+    if (await this._otpSubmission(res, ctx, appCb)) {
       // OTP path was taken, either successful entry and session creation, or re-prompting for otp.
       return;
     }
 
-    if (await this._localUserAuth(res, ctx)) {
+    if (await this._localUserAuth(res, ctx, appCb)) {
       // Local auth path was taken.
       return;
     }
@@ -136,7 +142,7 @@ class SessionManager {
     }
 
     if (ctx.errors.length) {
-      res.end(Template.LoginHTML(ctx, this.options));
+      res.end(Template.LoginHTML(ctx, this.options, appCb));
       return;
     }
 
@@ -207,7 +213,7 @@ class SessionManager {
     }
 
     if (ctx.errors.length) {
-      res.end(Template.LoginHTML(ctx, this.options));
+      res.end(Template.LoginHTML(ctx, this.options, appCb));
       return;
     }
 
@@ -261,7 +267,7 @@ class SessionManager {
    * @param {String} ctx.parsedBody.otp
    * @returns {Promise<Boolean>} true if otp was handled, otherwise false indicates further login processing needed
    */
-  async _otpSubmission(res, ctx) {
+  async _otpSubmission(res, ctx, appCb) {
     const _scope = _fileScope('_otpSubmission');
 
     const {
@@ -287,7 +293,7 @@ class SessionManager {
     if (!otp) {
       // Nothing submitted, but valid state, just present otp form again, do not count as attempt.
       ctx.otpState = stateBox;
-      res.end(Template.OTPHTML(ctx, this.options));
+      res.end(Template.OTPHTML(ctx, this.options, appCb));
       this.logger.info(_scope, 'finished otp, nothing entered, request again', { ctx });
       return true;
     }
@@ -313,7 +319,7 @@ class SessionManager {
           ...state,
           attempt: state.attempt + 1,
         });
-        res.end(Template.OTPHTML(ctx, this.options));
+        res.end(Template.OTPHTML(ctx, this.options, appCb));
         this.logger.info(_scope, 'finished otp, invalid, request again', { ctx });
         return true;
 
@@ -335,7 +341,7 @@ class SessionManager {
    * @param {Object} ctx
    * @returns {Promise<Boolean>} true if handled, false if flow should continue
    */
-  async _localUserAuth(res, ctx) {
+  async _localUserAuth(res, ctx, appCb) {
     const _scope = _fileScope('_localUserAuth');
 
     // If Indiauth enabled and profile was submitted, defer to that.
@@ -355,7 +361,7 @@ class SessionManager {
     }
 
     if (ctx.errors.length) {
-      res.end(Template.LoginHTML(ctx, this.options));
+      res.end(Template.LoginHTML(ctx, this.options, appCb));
       return true;
     }
 
@@ -368,7 +374,7 @@ class SessionManager {
         attempt: 0,
         redirect,
       });
-      res.end(Template.OTPHTML(ctx, this.options));
+      res.end(Template.OTPHTML(ctx, this.options, appCb));
       this.logger.info(_scope, 'finished local, otp required', { ctx });
       return true;
     }
@@ -412,8 +418,9 @@ class SessionManager {
    * This currently only redeems a scope-less profile.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
+   * @param {AppTemplateCallback} appCb
    */
-  async getAdminIA(res, ctx) {
+  async getAdminIA(res, ctx, appCb) {
     const _scope = _fileScope('getAdminIA');
     this.logger.debug(_scope, 'called', { ctx });
 
@@ -504,7 +511,7 @@ class SessionManager {
 
     if (ctx.errors.length) {
       await this._sessionCookieClear(res);
-      res.end(Template.IAHTML(ctx, this.options));
+      res.end(Template.IAHTML(ctx, this.options, appCb));
       return;
     }
 
@@ -528,8 +535,9 @@ class SessionManager {
    * Page for modifying credentials and OTP.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
+   * @param {AppTemplateCallback} appCb
    */
-  async getAdminSettings(res, ctx) {
+  async getAdminSettings(res, ctx, appCb) {
     const _scope = _fileScope('getAdminSettings');
     this.logger.debug(_scope, 'called', { ctx });
 
@@ -547,7 +555,7 @@ class SessionManager {
       ctx.errors.push('An error was encountered.  Sorry that is not very helpful.');
     }
 
-    res.end(Template.SettingsHTML(ctx, this.options));
+    res.end(Template.SettingsHTML(ctx, this.options, appCb));
     this.logger.info(_scope, 'finished', { ctx });
   }
 
@@ -556,8 +564,10 @@ class SessionManager {
    * Page for modifying credentials and OTP.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
+   * @param {Object[]=} appNavLinks
+   * @param {AppTemplateCallback} appCb
    */
-  async postAdminSettings(res, ctx) {
+  async postAdminSettings(res, ctx, appCb) {
     const _scope = _fileScope('postAdminSettings');
     this.logger.debug(_scope, 'called', { ctx });
 
@@ -597,7 +607,7 @@ class SessionManager {
       ctx.errors.push('An error was encountered.  Sorry that is not very helpful.');
     }
 
-    res.end(Template.SettingsHTML(ctx, this.options));
+    res.end(Template.SettingsHTML(ctx, this.options, appCb));
     this.logger.info(_scope, 'finished', { ctx });
   }
 
@@ -738,7 +748,6 @@ class SessionManager {
     }
   }
 
-
 }
 
 module.exports = SessionManager;
diff --git a/lib/template/helpers.js b/lib/template/helpers.js
new file mode 100644 (file)
index 0000000..67162f8
--- /dev/null
@@ -0,0 +1,46 @@
+'use strict';
+
+/**
+ * Populates nevLinks with (currently hardcoded) session-related links.
+ * @param {Number} pagePathLevel relative to base
+ * @param {Object} ctx
+ * @param {String=} ctx.url redirect on logout
+ * @param {Object=} ctx.session
+ * @param {String=} ctx.session.authenticatedIdentifier
+ * @param {String=} ctx.session.authenticatedProfile
+ * @param {Object} options
+ * @param {Object[]=} options.navLinks created if not present
+ * @param {String=} options.pageIdentifier
+ */
+function sessionNavLinks(pagePathLevel, ctx, options) {
+  if (!options.navLinks) {
+    options.navLinks = [];
+  }
+  const rootPath = '../'.repeat(pagePathLevel);
+  const redirectQuery = ctx?.url ? `?r=${encodeURIComponent(ctx.url)}` : rootPath;
+  const adminPath = rootPath + 'admin/';
+
+  const user = ctx?.session?.authenticatedProfile || ctx?.session?.authenticatedIdentifier;
+  if (user) {
+    if (options.pageIdentifier !== 'account') {
+      options.navLinks.push({
+        text: 'Account',
+        href: `${adminPath}settings`,
+      });
+    }
+    options.navLinks.push({
+      text: `Logout (${user})`,
+      href: `${adminPath}logout${redirectQuery}`,
+    });
+  } else {
+    options.navLinks.push({
+      text: 'Login',
+      href: `${adminPath}login${redirectQuery}`,
+    });
+  }
+}
+
+
+module.exports = {
+  sessionNavLinks,
+};
index 5de476ae04b880d2d94e6e3b1bd3db359aee2095..8a27479eb8426e4a3df658f9b71d382414754556 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const { TemplateHelper: th } = require('@squeep/html-template-helper');
+const { sessionNavLinks } = require('./helpers');
 
 /**
  *
@@ -23,10 +24,13 @@ function mainContent() {
  * @param {String} options.manager.pageTitle
  * @param {Object} options.dingus
  * @param {String} options.dingus.selfBaseUrl
+ * @param {(pagePathLevel, ctx, htmlOptions) => {void}} appCb
  * @returns {String}
  */
-module.exports = (ctx, options) => {
+module.exports = (ctx, options, appCb = () => {}) => {
+  const pagePathLevel = 1;
   const htmlOptions = {
+    pageIdentifier: 'indieAuthError',
     pageTitle: options.manager.pageTitle,
     logoUrl: options.manager.logoUrl,
     footerEntries: options.manager.footerEntries,
@@ -34,11 +38,14 @@ module.exports = (ctx, options) => {
       '<p>Problems were encountered while trying to authenticate your profile URL.</p>',
     ],
   };
+  appCb(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
+
   // Ensure there is always an error to report, even if we do not have one, as we ended up here somehow.
   if (!ctx?.errors?.length) {
     ctx.errors = [
       'Unknown Error &mdash; we are not sure what just happened',
     ];
   }
-  return th.htmlPage(2, ctx, htmlOptions, mainContent());
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent());
 };
\ No newline at end of file
index f645bee5df5106f492abfac8a8a8d7233f95d289..ce326db9fc5595fce536e80f05185bac7cd52234 100644 (file)
@@ -1,11 +1,13 @@
 'use strict';
 
+const Helpers = require('./helpers');
 const IAHTML = require('./ia-html');
 const LoginHTML = require('./login-html');
 const OTPHTML = require('./otp-html');
 const SettingsHTML = require('./settings-html');
 
 module.exports = {
+  Helpers,
   IAHTML,
   LoginHTML,
   OTPHTML,
index f59e002ca8111c85dd2b308a332280e948536f93..8ee7392ad9f4883150546a444f1f4c7c23a2c42f 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const { TemplateHelper: th } = require('@squeep/html-template-helper');
+const { sessionNavLinks } = require('./helpers');
 
 /**
  * Login form.
@@ -103,10 +104,13 @@ ${userBlurb}
  * @param {String=} options.manager.logoUrl
  * @param {Object} options.dingus
  * @param {String} options.dingus.selfBaseUrl
+ * @param {() => {}} appCb
  * @returns {String}
  */
-module.exports = (ctx, options) => {
+module.exports = (ctx, options, appCb = () => {}) => {
+  const pagePathLevel = 1;
   const htmlOptions = {
+    pageIdentifier: 'login',
     pageTitle: options.manager.pageTitle,
     logoUrl: options.manager.logoUrl,
     footerEntries: options.manager.footerEntries,
@@ -115,11 +119,13 @@ module.exports = (ctx, options) => {
     indieAuthBlurb: options.authenticator.indieAuthBlurb,
     userBlurb: options.authenticator.userBlurb,
   };
+  appCb(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
   const mainContent = [
     ...(options.authenticator.loginBlurb || []),
     indieAuthURLTrySecureFirstScript(ctx, htmlOptions),
     indieAuthSection(ctx, htmlOptions),
     userSection(ctx, htmlOptions),
   ];
-  return th.htmlPage(2, ctx, htmlOptions, mainContent);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent);
 };
\ No newline at end of file
index 066acce57ac308da62d4988c3cf3c4bc62f4e726..389f68670964b4a4b6fbb9daa45246fa4168d42a 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 const { TemplateHelper: th } = require('@squeep/html-template-helper');
+const { sessionNavLinks } = require('./helpers');
 
 /**
  * Login form, continued.
@@ -37,18 +38,23 @@ ${otpBlurb}
  * @param {String=} options.manager.logoUrl
  * @param {Object} options.dingus
  * @param {String} options.dingus.selfBaseUrl
+ * @param {() => {}} appCb
  * @returns {String}
  */
-module.exports = (ctx, options) => {
+module.exports = (ctx, options, appCb = () => {}) => {
+  const pagePathLevel = 1;
   const htmlOptions = {
+    pageIdentifier: 'otp',
     pageTitle: options.manager.pageTitle,
     logoUrl: options.manager.logoUrl,
     footerEntries: options.manager.footerEntries,
     otpBlurb: options.authenticator?.otpBlurb,
   };
+  appCb(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
   const mainContent = [
     ...(options.authenticator?.loginBlurb || []),
     otpSection(ctx, htmlOptions),
   ];
-  return th.htmlPage(2, ctx, htmlOptions, mainContent);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent);
 };
\ No newline at end of file
index c5bb6c082909bd92274ca3242a76c33b3764e574..42bfdcda4fedae3e6c12a25177cb87943f41bfbc 100644 (file)
@@ -3,6 +3,7 @@
 /* eslint-disable no-unused-vars */
 
 const { TemplateHelper: th } = require('@squeep/html-template-helper');
+const { sessionNavLinks } = require('./helpers');
 const { TOTP } = require('@squeep/totp');
 
 function updatePasswordSection(ctx, htmlOptions) {
@@ -93,16 +94,20 @@ function OTPSection(ctx, htmlOptions) {
 }
 
 
-module.exports = (ctx, options) => {
+module.exports = (ctx, options, appCb = () => {}) => {
+  const pagePathLevel = 1;
   const htmlOptions = {
+    pageIdentifier: 'account',
     pageTitle: options.manager.pageTitle,
     logoUrl: options.manager.logoUrl,
     footerEntries: options.manager.footerEntries,
   };
+  appCb(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
   const mainContent = [
     OTPSection(ctx, htmlOptions),
     updatePasswordSection(ctx, htmlOptions),
   ];
 
-  return th.htmlPage(1, ctx, htmlOptions, mainContent);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent);
 };
index abb3d3e130c07b87fed3554cbcd66f24215761c0..977ca4a27022e1487f3ab494cd91ec08e6f94d90 100644 (file)
@@ -10,7 +10,7 @@
       "license": "ISC",
       "dependencies": {
         "@squeep/api-dingus": "^2.1.0",
-        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.5.3",
+        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.0",
         "@squeep/indieauth-helper": "^1.4.1",
         "@squeep/mystery-box": "^2.0.2",
         "@squeep/totp": "^1.1.4"
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
       "dev": true,
       "dependencies": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
+        "@babel/highlight": "^7.24.2",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/code-frame/node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
-    },
-    "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/compat-data": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
-      "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz",
+      "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
-      "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz",
+      "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
       "dev": true,
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.2",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-compilation-targets": "^7.23.6",
         "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.24.0",
-        "@babel/parser": "^7.24.0",
+        "@babel/helpers": "^7.24.1",
+        "@babel/parser": "^7.24.1",
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.23.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
-      "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz",
+      "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.23.6",
-        "@jridgewell/gen-mapping": "^0.3.2",
-        "@jridgewell/trace-mapping": "^0.3.17",
+        "@babel/types": "^7.24.0",
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
       },
       "engines": {
       }
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
+      "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.22.15"
+        "@babel/types": "^7.24.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
-      "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz",
+      "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==",
       "dev": true,
       "dependencies": {
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0"
       },
       "engines": {
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
       "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
-      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
+      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
       "dev": true,
       "bin": {
         "parser": "bin/babel-parser.js"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
-      "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
+      "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
       "dev": true,
       "dependencies": {
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.1",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
         "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.24.0",
+        "@babel/parser": "^7.24.1",
         "@babel/types": "^7.24.0",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       }
     },
     "node_modules/@squeep/html-template-helper": {
-      "version": "1.5.3",
-      "resolved": "git+https://git.squeep.com/squeep-html-template-helper#084ad86d1dde896c0f49498f5573fcc6a60fddd8",
+      "version": "1.6.0",
+      "resolved": "git+https://git.squeep.com/squeep-html-template-helper#2d0ba72a2ea35f45c1ab1ac81fce3d0cbe7db419",
       "license": "ISC",
       "dependencies": {
         "@squeep/lazy-property": "^1.1.2"
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001597",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
-      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
+      "version": "1.0.30001600",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
+      "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
       "dev": true,
       "funding": [
         {
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.708",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.708.tgz",
-      "integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==",
+      "version": "1.4.715",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
+      "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==",
       "dev": true
     },
     "node_modules/emoji-regex": {
       }
     },
     "node_modules/eslint-compat-utils": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz",
-      "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz",
+      "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==",
       "dev": true,
+      "dependencies": {
+        "semver": "^7.5.4"
+      },
       "engines": {
         "node": ">=12"
       },
       }
     },
     "node_modules/eslint-plugin-es-x": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz",
-      "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.6.0.tgz",
+      "integrity": "sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.1.2",
         "@eslint-community/regexpp": "^4.6.0",
-        "eslint-compat-utils": "^0.1.2"
+        "eslint-compat-utils": "^0.5.0"
       },
       "engines": {
         "node": "^14.18.0 || >=16.0.0"
       "dev": true
     },
     "node_modules/html-validate": {
-      "version": "8.15.0",
-      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.15.0.tgz",
-      "integrity": "sha512-kqRgG8IDb6rMuQkMAsH7tmzkKTU7a67c0ZZDu4JlncIhImoPFra3H4CzdtIxF7hWaFTXR//QRGEwFiidjh0wfQ==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.17.1.tgz",
+      "integrity": "sha512-jBcyyC7/O+ag/gSNfPMtjJ4HrSvASsxLv9FRgpZmK1BGHTF8l7zBibmWFRRYS/s+QsdmBF6dG9JyM1/378/Izw==",
       "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.10.0",
       }
     },
     "node_modules/tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "optional": true,
       "dependencies": {
         "chownr": "^2.0.0",
       }
     },
     "@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
       "dev": true,
       "requires": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          }
-        },
-        "color-convert": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-          "dev": true
-        },
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        }
+        "@babel/highlight": "^7.24.2",
+        "picocolors": "^1.0.0"
       }
     },
     "@babel/compat-data": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
-      "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz",
+      "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==",
       "dev": true
     },
     "@babel/core": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
-      "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz",
+      "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
       "dev": true,
       "requires": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.2",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-compilation-targets": "^7.23.6",
         "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.24.0",
-        "@babel/parser": "^7.24.0",
+        "@babel/helpers": "^7.24.1",
+        "@babel/parser": "^7.24.1",
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
       }
     },
     "@babel/generator": {
-      "version": "7.23.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
-      "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz",
+      "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.23.6",
-        "@jridgewell/gen-mapping": "^0.3.2",
-        "@jridgewell/trace-mapping": "^0.3.17",
+        "@babel/types": "^7.24.0",
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
       }
     },
       }
     },
     "@babel/helper-module-imports": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
+      "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.22.15"
+        "@babel/types": "^7.24.0"
       }
     },
     "@babel/helper-module-transforms": {
       }
     },
     "@babel/helper-string-parser": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true
     },
     "@babel/helper-validator-identifier": {
       "dev": true
     },
     "@babel/helpers": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
-      "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz",
+      "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0"
       }
     },
     "@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
       "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "dependencies": {
         "ansi-styles": {
       }
     },
     "@babel/parser": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
-      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
+      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
       "dev": true
     },
     "@babel/template": {
       }
     },
     "@babel/traverse": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
-      "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
+      "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.1",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
         "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.24.0",
+        "@babel/parser": "^7.24.1",
         "@babel/types": "^7.24.0",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       }
     },
     "@squeep/html-template-helper": {
-      "version": "git+https://git.squeep.com/squeep-html-template-helper#084ad86d1dde896c0f49498f5573fcc6a60fddd8",
-      "from": "@squeep/html-template-helper@git+https://git.squeep.com/squeep-html-template-helper#v1.5.3",
+      "version": "git+https://git.squeep.com/squeep-html-template-helper#2d0ba72a2ea35f45c1ab1ac81fce3d0cbe7db419",
+      "from": "@squeep/html-template-helper@git+https://git.squeep.com/squeep-html-template-helper#v1.6.0",
       "requires": {
         "@squeep/lazy-property": "^1.1.2"
       }
       "devOptional": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001597",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
-      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
+      "version": "1.0.30001600",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
+      "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
       "dev": true
     },
     "chalk": {
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.4.708",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.708.tgz",
-      "integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==",
+      "version": "1.4.715",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
+      "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==",
       "dev": true
     },
     "emoji-regex": {
       }
     },
     "eslint-compat-utils": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz",
-      "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz",
+      "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==",
       "dev": true,
-      "requires": {}
+      "requires": {
+        "semver": "^7.5.4"
+      }
     },
     "eslint-plugin-es-x": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz",
-      "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.6.0.tgz",
+      "integrity": "sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==",
       "dev": true,
       "requires": {
         "@eslint-community/eslint-utils": "^4.1.2",
         "@eslint-community/regexpp": "^4.6.0",
-        "eslint-compat-utils": "^0.1.2"
+        "eslint-compat-utils": "^0.5.0"
       }
     },
     "eslint-plugin-n": {
       "dev": true
     },
     "html-validate": {
-      "version": "8.15.0",
-      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.15.0.tgz",
-      "integrity": "sha512-kqRgG8IDb6rMuQkMAsH7tmzkKTU7a67c0ZZDu4JlncIhImoPFra3H4CzdtIxF7hWaFTXR//QRGEwFiidjh0wfQ==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.17.1.tgz",
+      "integrity": "sha512-jBcyyC7/O+ag/gSNfPMtjJ4HrSvASsxLv9FRgpZmK1BGHTF8l7zBibmWFRRYS/s+QsdmBF6dG9JyM1/378/Izw==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.10.0",
       "dev": true
     },
     "tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "optional": true,
       "requires": {
         "chownr": "^2.0.0",
index 1a280a6bb1ab03c0ec897e1338d128902388e434..e0901c785b1f2371aca9aa748b03ddf8717b0164 100644 (file)
@@ -31,7 +31,7 @@
   ],
   "dependencies": {
     "@squeep/api-dingus": "^2.1.0",
-    "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.5.3",
+    "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.0",
     "@squeep/indieauth-helper": "^1.4.1",
     "@squeep/mystery-box": "^2.0.2",
     "@squeep/totp": "^1.1.4"
diff --git a/test/lib/template/helpers.js b/test/lib/template/helpers.js
new file mode 100644 (file)
index 0000000..686f72b
--- /dev/null
@@ -0,0 +1,54 @@
+'use strict';
+
+const assert = require('node:assert');
+const Helpers = require('../../../lib/template/helpers');
+
+describe('Template Helpers', function () {
+  describe('sessionNavLinks', function () {
+    let pagePathLevel, ctx, options;
+    beforeEach(function () {
+      pagePathLevel = 0;
+      ctx = {
+        session: {
+          authenticatedIdentifier: 'username',
+        }
+      };
+      options = {};
+    });
+    it('adds nav links', function () {
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 2);
+    });
+    it('extends nav links', function () {
+      options.navLinks = [ {} ];
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 3);
+    });
+    it('add login link if no user', function () {
+      delete ctx.session.authenticatedIdentifier;
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 1);
+    });
+    it('adds nav links for profile', function () {
+      delete ctx.session.authenticatedIdentifier;
+      ctx.session.authenticatedProfile = 'https://example.com/';
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 2);
+    });
+    it('covers logout redirect', function () {
+      ctx.url = '../relative';
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 2);
+    });
+    it('covers page depth', function () {
+      pagePathLevel = 2;
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 2);
+    });
+    it('elides account link on account page', function () {
+      options.pageIdentifier = 'account';
+      Helpers.sessionNavLinks(pagePathLevel, ctx, options);
+      assert.strictEqual(options.navLinks.length, 1);
+    });
+  }); // sessionNavLinks
+}); // Template Helpers
index d9ed065d86a451feee8f3f6cd8b86d203653066b..04e755584d6ee7c30584ea166b7f20963946b2dd 100644 (file)
@@ -31,6 +31,13 @@ describe('Template LoginHTML', function () {
     assert(result);
   });
 
+  it('covers local user', async function () {
+    options.authenticator.authnEnabled = ['argon2'];
+    const result = LoginHTML(ctx, options);
+    await lintHtml(result);
+    assert(result);
+  });
+
   it('renders errors and additional content', async function () {
     ctx.errors = ['an error', 'another error'];
     options.manager.logoUrl = 'https://example.com/logo.png';