From: Justin Wind Date: Sun, 24 Sep 2023 21:05:35 +0000 (-0700) Subject: rudimentary support for totp 2fa X-Git-Tag: v1.4.0~6 X-Git-Url: https://git.squeep.com/?a=commitdiff_plain;h=2ca511865b0caf3108819cfd6ee775124ea70dff;p=squeep-authentication-module rudimentary support for totp 2fa --- diff --git a/lib/authenticator.js b/lib/authenticator.js index ba5d921..da6b718 100644 --- a/lib/authenticator.js +++ b/lib/authenticator.js @@ -4,11 +4,18 @@ const common = require('./common'); const Enum = require('./enum'); const Errors = require('./errors'); const { MysteryBox } = require('@squeep/mystery-box'); +const { TOTP } = require('@squeep/totp'); const { name: packageName } = require('../package'); const _fileScope = common.fileScope(__filename); class Authenticator { + /** + * @typedef {Object} AuthInfo + * @property {String} identifier + * @property {String} credentials + * @property {String=} otpKey + */ /** * @param {Console} logger * @param {*} db @@ -55,15 +62,19 @@ class Authenticator { this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name))); this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32; + + this.TOTP = TOTP; // For ease of test stubbing. } /** * Check local auth entries. * Sets ctx.authenticatedId if valid. + * Sets ctx.otpNeeded if account has otpKey. * @param {String} identifier * @param {String} credential * @param {Object} ctx + * @returns {Promise} */ async isValidIdentifierCredential(identifier, credential, ctx) { const _scope = _fileScope('isValidIdentifierCredential'); @@ -79,16 +90,14 @@ class Authenticator { const authData = await this.db.authenticationGet(dbCtx, identifier); if (!authData) { this.logger.debug(_scope, 'failed, invalid identifier', { ctx, identifier }); + } else if (authData.credential.startsWith('$argon2') + && this.authnEnabled.includes('argon2')) { + isValid = await this.authn.argon2.verify(authData.credential, credential); + } else if (authData.credential.startsWith('$PAM$') + && this.authnEnabled.includes('pam')) { + isValid = this._isValidPAMIdentifier(identifier, credential); } else { - if (authData.credential.startsWith('$argon2') - && this.authnEnabled.includes('argon2')) { - isValid = await this.authn.argon2.verify(authData.credential, credential); - } else if (authData.credential.startsWith('$PAM$') - && this.authnEnabled.includes('pam')) { - isValid = this._isValidPAMIdentifier(identifier, credential); - } else { - this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { identifier, ctx }); - } + this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { identifier, ctx }); } if (this.authnEnabled.includes('DEBUG_ANY')) { @@ -97,6 +106,7 @@ class Authenticator { if (isValid) { ctx.authenticationId = identifier; + ctx.otpNeeded = !!authData?.otpKey; await this.db.authenticationSuccess(dbCtx, identifier); } }); // dbCtx @@ -109,7 +119,7 @@ class Authenticator { * Check system PAM. * @param {String} identifier * @param {String} credential - * @returns {Boolean} + * @returns {Promise} */ async _isValidPAMIdentifier(identifier, credential) { const _scope = _fileScope('_isValidPAMIdentifier'); @@ -130,11 +140,38 @@ class Authenticator { } + /** + * + * @param {Object} state + * @param {String} state.key + * @param {Number} state.attempt + * @param {Number} state.epochMs + * @param {String} otp + * @returns {String} Enum.OTPResult + */ + checkOTP(state, otp) { + const totp = new this.TOTP({ + ...this.options?.authenticator?.otpOptions, + ...state, + }); + const isValid = totp.validate(otp); + if (isValid) { + return Enum.OTPResult.Valid; + } + const otpAttemptsRemain = state.attempt <= (this.options?.authenticator?.otpAttemptsAllowed ?? 3); + const otpWithinWindow = state.epochMs + (this.options?.authenticator?.otpWindowMs ?? 600000) > Date.now(); + if (otpAttemptsRemain && otpWithinWindow) { + return Enum.OTPResult.InvalidSoftFail; + } + return Enum.OTPResult.InvalidHardFail; + } + + /** * Check for valid Basic auth, updates ctx with identifier if valid. * @param {String} credentials * @param {Object} ctx - * @returns {Boolean} + * @returns {Promise} */ async isValidBasic(credentials, ctx) { const _scope = _fileScope('isValidBasic'); @@ -150,6 +187,7 @@ class Authenticator { * Determine which Authorization header is available, and if it is valid. * @param {String} authorizationHeader * @param {Object} ctx + * @returns {Promise} */ async isValidAuthorization(authorizationHeader, ctx) { const _scope = _fileScope('isValidAuthorization'); @@ -378,12 +416,10 @@ class Authenticator { this.logger.debug(_scope, 'valid authorization', { ctx, sessionAlsoValid }); return true; } - } else { - if (sessionAlsoValid - && await this.sessionCheck(req, res, ctx, undefined, false, false)) { - this.logger.debug(_scope, 'valid session', { ctx, sessionAlsoValid }); - return true; - } + } else if (sessionAlsoValid + && await this.sessionCheck(req, res, ctx, undefined, false, false)) { + this.logger.debug(_scope, 'valid session', { ctx, sessionAlsoValid }); + return true; } this.logger.debug(_scope, 'invalid authorization', { ctx, sessionAlsoValid }); diff --git a/lib/enum.js b/lib/enum.js index 8f2471a..66e2856 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -12,6 +12,11 @@ const Enum = mergeDeep(DingusEnum, { WWWAuthenticate: 'WWW-Authenticate', }, SessionCookie: 'squeepSession', + OTPResult: { + Valid: 'Valid', + InvalidSoftFail: 'InvalidSoftFail', + InvalidHardFail: 'InvalidHardFail', + }, }); module.exports = freezeDeep(Enum); diff --git a/lib/session-manager.js b/lib/session-manager.js index 43990cc..48ec11a 100644 --- a/lib/session-manager.js +++ b/lib/session-manager.js @@ -58,7 +58,7 @@ class SessionManager { if (this.options.authenticator.secureAuthOnly) { cookieParts.push('Secure'); } - if (typeof(maxAge) === 'number') { + if (typeof maxAge === 'number') { cookieParts.push(`Max-Age=${maxAge}`); } if (path) { @@ -79,7 +79,21 @@ class SessionManager { const _scope = _fileScope('getAdminLogin'); this.logger.debug(_scope, 'called', { ctx }); - res.end(Template.LoginHTML(ctx, this.options)); + // If there is already a valid session established, and if a + // redirect is present, follow it. Why? Maybe someone had + // multiple tabs open, their session expired, and their browser + // restarted, reloading all of them, and they have relogged in + // on one and just want to hit reload on the others to go back + // to where they were. + const redirect = ctx.queryParams['r']; + if (ctx.authenticationId && redirect) { + res.statusCode = 302; + res.setHeader(Enum.Header.Location, redirect); + res.end(); + } else { + res.end(Template.LoginHTML(ctx, this.options)); + } + this.logger.info(_scope, 'finished', { ctx }) } @@ -95,113 +109,98 @@ class SessionManager { ctx.errors = []; - const redirect = ctx.queryParams['r'] || './'; - - // Only attempt user login if no IndieAuth profile is set - if (!this.options.authenticator.authnEnabled.includes('indieAuth') || !ctx.parsedBody['me']) { - - const identifier = ctx.parsedBody['identifier']; - const credential = ctx.parsedBody['credential']; // N.B. Logger must specifically mask this field from ctx. - - const isValidLocalIdentifier = await this.authenticator.isValidIdentifierCredential(identifier, credential, ctx); - if (!isValidLocalIdentifier) { - ctx.errors.push('Invalid username or password'); - } - - if (ctx.errors.length) { - res.end(Template.LoginHTML(ctx, this.options)); - return; - } + // Check if this was an OTP entry attempt. + if (await this._otpSubmission(res, ctx)) { + // OTP path was taken, either successful entry and session creation, or re-prompting for otp. + return; + } - // Valid auth, persist the authenticated session - ctx.session = { - authenticatedIdentifier: ctx.authenticationId, - }; - await this._sessionCookieSet(res, ctx.session, this.cookieLifespan); - res.statusCode = 302; - res.setHeader(Enum.Header.Location, redirect); - res.end(); - this.logger.info(_scope, 'finished local', { ctx }); + if (await this._localUserAuth(res, ctx)) { + // Local auth path was taken. return; } // Otherwise, carry on with IndieAuth handshake. + + // Is profile a reasonable url? let me, meAutoScheme, session, authorizationEndpoint; try { me = new URL(ctx.parsedBody['me']); meAutoScheme = !!ctx.parsedBody['me_auto_scheme']; - } catch (e) { this.logger.debug(_scope, 'failed to parse supplied profile url', { ctx }); ctx.errors.push(`Unable to understand '${ctx.parsedBody['me']}' as a profile URL.`); } - if (this.options.authenticator.authnEnabled.includes('indieAuth') - && me) { - let profile; + if (ctx.errors.length) { + res.end(Template.LoginHTML(ctx, this.options)); + return; + } + + let profile; + profile = await this.indieAuthCommunication.fetchProfile(me); + if ((!profile?.metadata) + && meAutoScheme) { + this.logger.debug(_scope, 'trying http fallback', { ctx }); + me.protocol = 'http'; profile = await this.indieAuthCommunication.fetchProfile(me); - if ((!profile?.metadata) - && meAutoScheme) { - this.logger.debug(_scope, 'trying http fallback', { ctx }); - me.protocol = 'http'; - profile = await this.indieAuthCommunication.fetchProfile(me); + } + if (!profile?.metadata) { + this.logger.debug(_scope, 'failed to find any profile information at url', { ctx }); + ctx.errors.push(`No profile information was found at '${me}'.`); + } else { + // fetch and parse me for 'authorization_endpoint' relation links + try { + authorizationEndpoint = new URL(profile.metadata.authorizationEndpoint); + } catch (e) { + ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.metadata.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`); } - if (!profile?.metadata) { - this.logger.debug(_scope, 'failed to find any profile information at url', { ctx }); - ctx.errors.push(`No profile information was found at '${me}'.`); - } else { - // fetch and parse me for 'authorization_endpoint' relation links - try { - authorizationEndpoint = new URL(profile.metadata.authorizationEndpoint); - } catch (e) { - ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.metadata.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`); - } - if (profile.metadata.issuer) { - // Validate issuer - try { - const issuer = new URL(profile.metadata.issuer); - if (issuer.hash - || issuer.search - || issuer.protocol.toLowerCase() !== 'https:') { // stupid URL trailing colon thing - this.logger.debug(_scope, 'supplied issuer url invalid', { ctx }); - ctx.errors.push('Authorization server provided an invalid issuer field.'); - } - } catch (e) { - this.logger.debug(_scope, 'failed to parse supplied issuer url', { ctx }); - ctx.errors.push('Authorization server provided an unparsable issuer field.'); + if (profile.metadata.issuer) { + // Validate issuer + try { + const issuer = new URL(profile.metadata.issuer); + if (issuer.hash + || issuer.search + || issuer.protocol.toLowerCase() !== 'https:') { // stupid URL trailing colon thing + this.logger.debug(_scope, 'supplied issuer url invalid', { ctx }); + ctx.errors.push('Authorization server provided an invalid issuer field.'); } - } else { - this.logger.debug(_scope, 'no issuer in metadata, assuming legacy mode', { ctx }); - // Strict 20220212 compliance would error here. - // ctx.errors.push('Authorization server did not provide issuer field, as required by current specification.'); + } catch (e) { + this.logger.debug(_scope, 'failed to parse supplied issuer url', { ctx }); + ctx.errors.push('Authorization server provided an unparsable issuer field.'); } + } else { + this.logger.debug(_scope, 'no issuer in metadata, assuming legacy mode', { ctx }); + // Strict 20220212 compliance would error here. + // ctx.errors.push('Authorization server did not provide issuer field, as required by current specification.'); } + } - if (authorizationEndpoint) { - const pkce = await IndieAuthCommunication.generatePKCE(); - - const state = randomUUID(); - session = { - authorizationEndpoint: authorizationEndpoint.href, - state, - codeVerifier: pkce.codeVerifier, - me, - redirect, - issuer: profile.metadata.issuer, - }; - - // Update auth endpoint parameters - Object.entries({ - 'response_type': 'code', - 'client_id': this.options.dingus.selfBaseUrl, - 'redirect_uri': `${this.options.dingus.selfBaseUrl}admin/_ia`, - 'state': session.state, - 'code_challenge': pkce.codeChallenge, - 'code_challenge_method': pkce.codeChallengeMethod, - 'me': me, - }).forEach(([name, value]) => authorizationEndpoint.searchParams.set(name, value)); - } + if (authorizationEndpoint) { + const pkce = await IndieAuthCommunication.generatePKCE(); + + const state = randomUUID(); + const redirect = ctx.queryParams['r'] || './'; + session = { + authorizationEndpoint: authorizationEndpoint.href, + state, + codeVerifier: pkce.codeVerifier, + me, + redirect, + issuer: profile.metadata.issuer, + }; + + // Update auth endpoint parameters + Object.entries({ + 'response_type': 'code', + 'client_id': this.options.dingus.selfBaseUrl, + 'redirect_uri': `${this.options.dingus.selfBaseUrl}admin/_ia`, + 'state': session.state, + 'code_challenge': pkce.codeChallenge, + 'code_challenge_method': pkce.codeChallengeMethod, + 'me': me, + }).forEach(([name, value]) => authorizationEndpoint.searchParams.set(name, value)); } if (ctx.errors.length) { @@ -218,6 +217,126 @@ class SessionManager { } + /** + * Check if processing an OTP entry attempt. If not, resume login flow. + * If so, validate otp and establish session, else reprompt for OTP, or + * return to login entry after too many failures. + * @param {*} res + * @param {*} ctx + * @returns {Boolean} true if otp was handled, otherwise false indicates further login processing needed + */ + async _otpSubmission(res, ctx) { + const _scope = _fileScope('_otpSubmission'); + + // Are we processing an OTP entry attempt? + const { otp, state: stateBox } = ctx.parsedBody; + let state; + try { + if (stateBox) { + state = await this.mysteryBox.unpack(stateBox); + } + } catch (e) { + this.logger.debug(_scope, 'failed to unpack otp state', { error: e, ctx }); + // Ignore and continue back to main login + return false; + } + if (otp && state) { + const OTPResult = await this.authenticator.checkOTP(state, otp); + switch (OTPResult) { + case Enum.OTPResult.Valid: + // successful otp entry + // Valid auth, persist the authenticated session + ctx.session = { + authenticatedIdentifier: state.authenticationId, + }; + await this._sessionCookieSet(res, ctx.session, this.cookieLifespan); + res.statusCode = 302; + res.setHeader(Enum.Header.Location, state.redirect); + res.end(); + this.logger.info(_scope, 'finished otp', { ctx }); + return true; + + case Enum.OTPResult.InvalidSoftFail: + // retry otp entry + ctx.otpNeeded = true; + ctx.errors.push('Invalid OTP token.'); + ctx.otpState = await this.mysteryBox.pack({ + ...state, + attempt: state.attempt + 1, + }); + res.end(Template.OTPHTML(ctx, this.options)); + this.logger.info(_scope, 'finished otp, invalid, request again', { ctx }); + return true; + + case Enum.OTPResult.InvalidHardFail: + // return to initial login + this.logger.debug(_scope, 'too many otp failures', { ctx, state }); + ctx.errors.push('Invalid OTP token, and too many failures. Try again.'); + return false; + + default: + throw new RangeError('Unexpected OTPResult'); + } + } + // not in otp flow + return false; + } + + /** + * + * @param {http.ServerResponse} res + * @param {Object} ctx + */ + async _localUserAuth(res, ctx) { + const _scope = _fileScope('_localUserAuth'); + + // If Indiauth enabled and profile was submitted, defer to that. + if (this.options.authenticator.authnEnabled.includes('indieAuth') + && ctx.parsedBody['me']) { + return false; + } + + const redirect = ctx.queryParams['r'] || './'; + const identifier = ctx.parsedBody['identifier']; + const credential = ctx.parsedBody['credential']; // N.B. Logger must specifically mask this field from ctx. + + // N.B. validity check also sets autenticationId and otpNeeded on ctx + const isValidLocalIdentifier = await this.authenticator.isValidIdentifierCredential(identifier, credential, ctx); + if (!isValidLocalIdentifier) { + ctx.errors.push('Invalid username or password'); + } + + if (ctx.errors.length) { + res.end(Template.LoginHTML(ctx, this.options)); + return true; + } + + // If OTP exists for valid identifier, follow that flow. + if (ctx.otpNeeded) { + ctx.otpState = await this.mysteryBox.pack({ + authenticatedIdentifier: ctx.authenticationId, + epochMs: Date.now(), + attempt: 0, + redirect, + }); + res.end(Template.OTPHTML(ctx, this.options)); + this.logger.info(_scope, 'finished local, otp required', { ctx }); + return true; + } + + // Valid auth, persist the authenticated session + ctx.session = { + authenticatedIdentifier: ctx.authenticationId, + }; + await this._sessionCookieSet(res, ctx.session, this.cookieLifespan); + res.statusCode = 302; + res.setHeader(Enum.Header.Location, redirect); + res.end(); + this.logger.info(_scope, 'finished local, no otp', { ctx }); + return true; + } + + /** * GET request to remove current credentials. * @param {http.ServerResponse} res @@ -254,7 +373,7 @@ class SessionManager { // Unpack cookie to restore session data - const [ cookieName, cookieValue ] = common.splitFirst((ctx.cookie || ''), '=', ''); + const [cookieName, cookieValue] = common.splitFirst((ctx.cookie || ''), '=', ''); if (cookieName !== Enum.SessionCookie) { this.logger.debug(_scope, 'no cookie', { ctx }); ctx.errors.push('missing required cookie'); diff --git a/lib/template/ia-html.js b/lib/template/ia-html.js index 00e2089..5de476a 100644 --- a/lib/template/ia-html.js +++ b/lib/template/ia-html.js @@ -2,6 +2,10 @@ const { TemplateHelper: th } = require('@squeep/html-template-helper'); +/** + * + * @returns {String} + */ function mainContent() { return [ `
@@ -31,7 +35,7 @@ module.exports = (ctx, options) => { ], }; // Ensure there is always an error to report, even if we do not have one, as we ended up here somehow. - if (!ctx.errors || !ctx.errors.length) { + if (!ctx?.errors?.length) { ctx.errors = [ 'Unknown Error — we are not sure what just happened', ]; diff --git a/lib/template/index.js b/lib/template/index.js index 69c70b9..c77123c 100644 --- a/lib/template/index.js +++ b/lib/template/index.js @@ -2,8 +2,10 @@ const IAHTML = require('./ia-html'); const LoginHTML = require('./login-html'); +const OTPHTML = require('./otp-html'); module.exports = { IAHTML, LoginHTML, + OTPHTML, }; \ No newline at end of file diff --git a/lib/template/otp-html.js b/lib/template/otp-html.js new file mode 100644 index 0000000..cf7cef6 --- /dev/null +++ b/lib/template/otp-html.js @@ -0,0 +1,54 @@ +'use strict'; + +const { TemplateHelper: th } = require('@squeep/html-template-helper'); + +/** + * Login form, continued. + */ + +function otpSection(ctx, options) { + const otpBlurb = (options.otpBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n'); + return `\t\t\t
+\t\t\t\t
+\t\t\t\t\t
+\t\t\t\t\t\tTwo-Factor Authentication +\t\t\t\t\t\t +\t\t\t\t\t\t +\t\t\t\t\t\t
+\t\t\t\t\t\t +${otpBlurb} +\t\t\t\t\t
+\t\t\t\t\t +\t\t\t\t
+\t\t\t { + const htmlOptions = { + pageTitle: options.manager.pageTitle, + logoUrl: options.manager.logoUrl, + footerEntries: options.manager.footerEntries, + otpBlurb: options.authenticator?.otpBlurb, + }; + const mainContent = [ + ...(options.authenticator?.loginBlurb || []), + otpSection(ctx, htmlOptions), + ]; + return th.htmlPage(2, ctx, htmlOptions, mainContent); +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f7b64c0..5e8cb2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,11 @@ "@squeep/api-dingus": "v2.0.0", "@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/mystery-box": "^2.0.1", + "@squeep/totp": "git+https://git.squeep.com/squeep-totp#v1.1.0" }, "devDependencies": { - "eslint": "^8.48.0", + "eslint": "^8.50.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-security": "^1.7.1", @@ -24,13 +25,13 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "sinon": "^15.2.0" + "sinon": "^16.0.0" }, "engines": { "node": "^14 >=14.18 || >=15.7" }, "optionalDependencies": { - "argon2": "^0.31.0", + "argon2": "^0.31.1", "node-linux-pam": "^0.2.1" } }, @@ -57,12 +58,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.10", + "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" }, "engines": { @@ -141,30 +142,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", - "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-compilation-targets": "^7.22.10", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helpers": "^7.22.11", - "@babel/parser": "^7.22.11", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -189,12 +190,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.22.15", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -204,13 +205,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -229,9 +230,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -263,28 +264,28 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "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==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -327,44 +328,44 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", - "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -444,9 +445,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", - "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -456,33 +457,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", - "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -500,13 +501,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", "to-fast-properties": "^2.0.0" }, "engines": { @@ -529,9 +530,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -561,18 +562,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -937,6 +938,18 @@ "node": "^14 >=14.18.0 || >=15.7.0" } }, + "node_modules/@squeep/totp": { + "version": "1.1.0", + "resolved": "git+https://git.squeep.com/squeep-totp#381355dd8d70451179cfbde204177ed89675c3a3", + "license": "ISC", + "dependencies": { + "base32.js": "^0.1.0", + "qrcode-svg": "^1.1.0" + }, + "engines": { + "node": ">14" + } + }, "node_modules/@squeep/web-linking": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@squeep/web-linking/-/web-linking-1.0.8.tgz", @@ -957,9 +970,9 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" }, "node_modules/abbrev": { "version": "1.1.1", @@ -1113,9 +1126,9 @@ } }, "node_modules/argon2": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.0.tgz", - "integrity": "sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.1.tgz", + "integrity": "sha512-ik2xnJrLXazya7m4Nz1XfBSRjXj8Koq8qF9PsQC8059p20ifWc9zx/hgU3ItZh/3TnwXkv0RbhvjodPkmFf0bg==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -1139,6 +1152,14 @@ "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/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1292,9 +1313,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001524", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", - "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "version": "1.0.30001534", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz", + "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==", "dev": true, "funding": [ { @@ -1646,9 +1667,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.503", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz", - "integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==", + "version": "1.4.523", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", + "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==", "dev": true }, "node_modules/emoji-regex": { @@ -1685,16 +1706,16 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", @@ -2057,9 +2078,9 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/foreground-child": { @@ -2250,9 +2271,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2963,14 +2984,14 @@ } }, "node_modules/microformats-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.4.1.tgz", - "integrity": "sha512-BSg9Y/Aik8hvvme/fkxnXMRvTKuVwOeTapeZdaPQ+92DEubyM31iMtwbgFZ1383om643UvfYY5G23E9s1FY2KQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.5.2.tgz", + "integrity": "sha512-EcHm8zxEm3CggOLgILfxCo2wDiJEOnACzpV/FXWGLaRk24ECei+JkoWNdKdo2vzo/Pww9EvrQNeQsdv4JuHy7Q==", "dependencies": { "parse5": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/mime-db": { @@ -4167,6 +4188,14 @@ "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", @@ -4291,9 +4320,9 @@ "devOptional": true }, "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -4474,9 +4503,9 @@ "devOptional": true }, "node_modules/sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.0.0.tgz", + "integrity": "sha512-B8AaZZm9CT5pqe4l4uWJztfD/mOTa7dL8Qo0W4+s+t74xECOgSZDDQCBjNgIK3+n4kyxQrSTv2V5ul8K25qkiQ==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", @@ -4629,9 +4658,9 @@ } }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "optional": true, "dependencies": { "chownr": "^2.0.0", @@ -4820,9 +4849,13 @@ "devOptional": true }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -5026,12 +5059,12 @@ } }, "@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.22.10", + "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" }, "dependencies": { @@ -5094,27 +5127,27 @@ } }, "@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "dev": true }, "@babel/core": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", - "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-compilation-targets": "^7.22.10", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helpers": "^7.22.11", - "@babel/parser": "^7.22.11", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -5131,25 +5164,25 @@ } }, "@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", "dev": true, "requires": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.22.15", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" } }, "@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "requires": { "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -5164,9 +5197,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { @@ -5189,25 +5222,25 @@ } }, "@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "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==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" } }, "@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" } }, "@babel/helper-simple-access": { @@ -5235,35 +5268,35 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "dev": true }, "@babel/helpers": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", - "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -5327,36 +5360,36 @@ } }, "@babel/parser": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", - "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", - "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -5370,13 +5403,13 @@ } }, "@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", "to-fast-properties": "^2.0.0" } }, @@ -5390,9 +5423,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true }, "@eslint/eslintrc": { @@ -5413,15 +5446,15 @@ } }, "@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -5707,6 +5740,14 @@ "resolved": "https://registry.npmjs.org/@squeep/mystery-box/-/mystery-box-2.0.1.tgz", "integrity": "sha512-is8Uqvcm27pm49ZaQL/KOizo89SiTuTZElQoyuPX34TvcnsOTN2eN2YxdVSwKusZwYi+mmE8brrAtBcM/Ttpwg==" }, + "@squeep/totp": { + "version": "git+https://git.squeep.com/squeep-totp#381355dd8d70451179cfbde204177ed89675c3a3", + "from": "@squeep/totp@git+https://git.squeep.com/squeep-totp#v1.1.0", + "requires": { + "base32.js": "^0.1.0", + "qrcode-svg": "^1.1.0" + } + }, "@squeep/web-linking": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@squeep/web-linking/-/web-linking-1.0.8.tgz", @@ -5721,9 +5762,9 @@ } }, "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" }, "abbrev": { "version": "1.1.1", @@ -5838,9 +5879,9 @@ } }, "argon2": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.0.tgz", - "integrity": "sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.1.tgz", + "integrity": "sha512-ik2xnJrLXazya7m4Nz1XfBSRjXj8Koq8qF9PsQC8059p20ifWc9zx/hgU3ItZh/3TnwXkv0RbhvjodPkmFf0bg==", "optional": true, "requires": { "@mapbox/node-pre-gyp": "^1.0.11", @@ -5860,6 +5901,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true }, + "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==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5972,9 +6018,9 @@ "devOptional": true }, "caniuse-lite": { - "version": "1.0.30001524", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", - "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "version": "1.0.30001534", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz", + "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==", "dev": true }, "chalk": { @@ -6233,9 +6279,9 @@ } }, "electron-to-chromium": { - "version": "1.4.503", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz", - "integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==", + "version": "1.4.523", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", + "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==", "dev": true }, "emoji-regex": { @@ -6263,16 +6309,16 @@ "dev": true }, "eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", @@ -6532,9 +6578,9 @@ } }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "foreground-child": { @@ -6667,9 +6713,9 @@ } }, "globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -7203,9 +7249,9 @@ } }, "microformats-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.4.1.tgz", - "integrity": "sha512-BSg9Y/Aik8hvvme/fkxnXMRvTKuVwOeTapeZdaPQ+92DEubyM31iMtwbgFZ1383om643UvfYY5G23E9s1FY2KQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.5.2.tgz", + "integrity": "sha512-EcHm8zxEm3CggOLgILfxCo2wDiJEOnACzpV/FXWGLaRk24ECei+JkoWNdKdo2vzo/Pww9EvrQNeQsdv4JuHy7Q==", "requires": { "parse5": "^6.0.0" } @@ -8154,6 +8200,11 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "qrcode-svg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz", + "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8234,9 +8285,9 @@ "devOptional": true }, "resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", "dev": true, "requires": { "is-core-module": "^2.13.0", @@ -8365,9 +8416,9 @@ "devOptional": true }, "sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.0.0.tgz", + "integrity": "sha512-B8AaZZm9CT5pqe4l4uWJztfD/mOTa7dL8Qo0W4+s+t74xECOgSZDDQCBjNgIK3+n4kyxQrSTv2V5ul8K25qkiQ==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.0", @@ -8484,9 +8535,9 @@ "dev": true }, "tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "optional": true, "requires": { "chownr": "^2.0.0", @@ -8629,9 +8680,9 @@ "devOptional": true }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "webidl-conversions": { "version": "3.0.1", diff --git a/package.json b/package.json index a1a55c4..a1b37a5 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,15 @@ "@squeep/api-dingus": "v2.0.0", "@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/mystery-box": "^2.0.1", + "@squeep/totp": "git+https://git.squeep.com/squeep-totp#v1.1.0" }, "optionalDependencies": { - "argon2": "^0.31.0", + "argon2": "^0.31.1", "node-linux-pam": "^0.2.1" }, "devDependencies": { - "eslint": "^8.48.0", + "eslint": "^8.50.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-security": "^1.7.1", @@ -49,6 +50,6 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "sinon": "^15.2.0" + "sinon": "^16.0.0" } } diff --git a/test/lib/authenticator.js b/test/lib/authenticator.js index 6cac48f..7576168 100644 --- a/test/lib/authenticator.js +++ b/test/lib/authenticator.js @@ -6,7 +6,6 @@ const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-requi const Authenticator = require('../../lib/authenticator'); const stubLogger = require('../stub-logger'); const stubDb = require('../stub-db'); -const Errors = require('../../lib/errors'); const Enum = require('../../lib/enum'); const Config = require('../stub-config'); @@ -267,6 +266,40 @@ describe('Authenticator', function () { }); }); // isValidCookieAuth + describe('checkOTP', function () { + let state, otp; + this.beforeEach(function () { + sinon.stub(authenticator.TOTP.prototype, 'validate').returns(true); + state = { + key: Buffer.from('12345678901234567890'), + attempt: 0, + epochMs: Date.now(), + }; + otp = '000000'; + }); + it('covers valid OTP entry', function () { + const result = authenticator.checkOTP(state, otp); + assert.strictEqual(result, Enum.OTPResult.Valid); + }); + it('covers invalid OTP entry', function () { + authenticator.TOTP.prototype.validate.returns(false); + const result = authenticator.checkOTP(state, otp); + assert.strictEqual(result, Enum.OTPResult.InvalidSoftFail); + }); + it('covers invalid OTP entry, too many failures', function () { + state.attempt = 10; + authenticator.TOTP.prototype.validate.returns(false); + const result = authenticator.checkOTP(state, otp); + assert.strictEqual(result, Enum.OTPResult.InvalidHardFail); + }); + it('covers invalid OTP entry', function () { + state.epochMs = 0; + authenticator.TOTP.prototype.validate.returns(false); + const result = authenticator.checkOTP(state, otp); + assert.strictEqual(result, Enum.OTPResult.InvalidHardFail); + }); + }); // checkOTP + describe('sessionCheck', function () { let cookie, req, res, loginPath, required, profilesAllowed; beforeEach(function () { diff --git a/test/lib/session-manager.js b/test/lib/session-manager.js index a3efeaf..79abbf0 100644 --- a/test/lib/session-manager.js +++ b/test/lib/session-manager.js @@ -7,6 +7,7 @@ const assert = require('assert'); const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require const SessionManager = require('../../lib/session-manager'); +const Enum = require('../../lib/enum'); const Config = require('../stub-config'); const stubLogger = require('../stub-logger'); @@ -25,9 +26,11 @@ describe('SessionManager', function () { params: {}, queryParams: {}, parsedBody: {}, + errors: [], }; stubAuthenticator = { isValidIdentifierCredential: sinon.stub(), + checkOTP: sinon.stub(), }; manager = new SessionManager(stubLogger, stubAuthenticator, options); sinon.stub(manager.indieAuthCommunication); @@ -61,12 +64,27 @@ describe('SessionManager', function () { }); // _sessionCookieSet describe('getAdminLogin', function () { - it('covers', async function () { + it('covers no session', async function () { + await manager.getAdminLogin(res, ctx); + }); + it('covers established session', async function () { + ctx.authenticationId = 'identifier'; + ctx.queryParams['r'] = '/admin'; await manager.getAdminLogin(res, ctx); + assert.strictEqual(res.statusCode, 302); + assert(res.setHeader.called); }); }); // getAdminLogin describe('postAdminLogin', function () { + beforeEach(function () { + sinon.stub(manager, '_otpSubmission').resolves(false); + }); + it('covers otp submission', async function () { + manager._otpSubmission.resolves(true); + await manager.postAdminLogin(res, ctx); + assert(res.end.notCalled); + }); it('covers valid local', async function () { ctx.parsedBody.identifier = 'user'; ctx.parsedBody.credential = 'password'; @@ -115,15 +133,15 @@ describe('SessionManager', function () { }); it('covers profile scheme fallback', async function () { ctx.parsedBody.me = 'https://example.com/profile'; - ctx.parsedBody.me_auto_scheme = '1'; + ctx.parsedBody['me_auto_scheme'] = '1'; manager.indieAuthCommunication.fetchProfile .onCall(0).resolves() .onCall(1).resolves({ - metadata: { - issuer: 'https://example.com/', - authorizationEndpoint: 'https://example.com/auth', - }, - }); + metadata: { + issuer: 'https://example.com/', + authorizationEndpoint: 'https://example.com/auth', + }, + }); await manager.postAdminLogin(res, ctx); assert.strictEqual(res.statusCode, 302); @@ -165,6 +183,103 @@ describe('SessionManager', function () { }); // living-standard-20220212 }); // postAdminLogin + describe('_otpSubmission', function () { + beforeEach(function () { + sinon.useFakeTimers(new Date()); + sinon.stub(manager.mysteryBox, 'unpack').resolves({ + authenticatedIdentifier: 'identifier', + attempt: 0, + epochMs: Date.now(), + }); + manager.authenticator.checkOTP.resolves(Enum.OTPResult.Valid); + ctx.parsedBody.state = 'state_data'; + ctx.parsedBody.otp = '123456'; + }); + it('returns false if no otp state', async function () { + delete ctx.parsedBody.state; + const result = await manager._otpSubmission(res, ctx); + assert(manager.mysteryBox.unpack.notCalled); + assert.strictEqual(result, false); + }); + it('returns false when presented with invalid otp state', async function () { + manager.mysteryBox.unpack.rejects(); + const result = await manager._otpSubmission(res, ctx); + assert(manager.mysteryBox.unpack.called); + assert.strictEqual(result, false); + }); + it('returns true when submitted otp is invalid, but allowed to retry', async function () { + manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidSoftFail); + const result = await manager._otpSubmission(res, ctx); + assert(manager.mysteryBox.unpack.called); + assert.strictEqual(result, true); + assert(res.end.called); + }); + it('returns false when submitted otp is invalid and too many attempts', async function () { + manager.mysteryBox.unpack.resolves({ + authenticatedIdentifier: 'identifier', + attempt: 10, + epochMs: Date.now(), + }); + manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail); + const result = await manager._otpSubmission(res, ctx); + assert(manager.mysteryBox.unpack.called); + assert.strictEqual(result, false); + }); + it('returns false when submitted otp is invalid and too much time has passed', async function () { + manager.mysteryBox.unpack.resolves({ + authenticatedIdentifier: 'identifier', + attempt: 0, + epochMs: Date.now() - 99999999, + }); + manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail); + const result = await manager._otpSubmission(res, ctx); + assert(manager.mysteryBox.unpack.called); + assert.strictEqual(result, false); + }); + it('returns true when submitted otp is valid', async function () { + const result = await manager._otpSubmission(res, ctx); + assert(res.end.called); + assert.strictEqual(result, true); + }); + it('covers unexpected otp response', async function () { + manager.authenticator.checkOTP.resolves('wrong'); + assert.rejects(() => manager._otpSubmission(res, ctx), RangeError); + }); + }); // _otpSubmission + + describe('_localUserAuth', function () { + beforeEach(function () { + ctx.parsedBody.identifier = 'identifier'; + ctx.parsedBody.credential = 'credential'; + manager.authenticator.isValidIdentifierCredential.resolves(true); + sinon.stub(manager.mysteryBox, 'pack').resolves('box'); + }); + it('returns false if indieauth available', async function () { + ctx.parsedBody.me = 'https://example.com/'; + const result = await manager._localUserAuth(res, ctx); + assert.strictEqual(result, false); + }); + it('returns true if identifier is invalid', async function () { + manager.authenticator.isValidIdentifierCredential.resolves(false); + const result = await manager._localUserAuth(res, ctx); + assert.strictEqual(result, true); + assert(manager.authenticator.isValidIdentifierCredential.called); + assert(res.end.called); + }); + it('returns true if valid identifier', async function () { + const result = await manager._localUserAuth(res, ctx); + assert.strictEqual(result, true); + assert(res.end.called); + }); + it('returns true if valid identifier requires otp entry', async function () { + ctx.otpNeeded = true; + const result = await manager._localUserAuth(res, ctx); + assert.strictEqual(result, true); + assert(manager.mysteryBox.pack.called); + assert(res.end.called); + }); + }); // _localUserAuth + describe('getAdminLogout', function () { it('covers', async function () { await manager.getAdminLogout(res, ctx); diff --git a/test/lib/template/otp-html.js b/test/lib/template/otp-html.js new file mode 100644 index 0000000..5aa3d8b --- /dev/null +++ b/test/lib/template/otp-html.js @@ -0,0 +1,38 @@ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const { OTPHTML } = require('../../../lib/template'); +const lintHtml = require('../../lint-html'); + +describe('Template OTPHTML', function () { + let ctx, options; + beforeEach(function () { + ctx = {}; + options = { + authenticator: { + otpBlurb: ['otp info'], + }, + manager: { + pageTitle: 'page', + }, + dingus: { + selfBaseUrl: 'https://example.com/', + }, + }; + }); + + it('renders', function () { + ctx.errors = ['an error', 'another error']; + const result = OTPHTML(ctx, options); + lintHtml(result); + assert(result); + }); + + it('covers empty error', function () { + const result = OTPHTML(ctx, options); + lintHtml(result); + assert(result); + }); + +}); \ No newline at end of file