From 1a23cb132d2b316b9b2de896133b8f6b4b72727c Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 12:11:06 -0500 Subject: [PATCH 01/44] add jsdoc --- src/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/index.ts b/src/index.ts index c32d663..363e71b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -393,6 +393,24 @@ export class TOTPStrategy extends Strategy { } } + /** + * Authenticates a user using TOTP. + * + * If the user is already authenticated, simply returns the user. + * + * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | + * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| + * | POST | ✓ | ✗ | - | - | Generate new TOTP, send to user, store email and TOTP in session. | + * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | + * | POST | ✗ | ✗ | ✓ | ✓ | Invalidate previous TOTP, generate new one if session has email and TOTP. | + * | POST | ≠ | - | ✓ | ✓ | Invalidate previous TOTP, generate new TOTP for new email. | + * | GET | - | - | - | - | If magic-link enabled and URL has TOTP, validate it. If valid, authenticate user. | + * + * @param {Request} request - The request object. + * @param {SessionStorage} sessionStorage - The session storage instance. + * @param {AuthenticateOptions} options - The authentication options. + * @returns {Promise} The authenticated user. + */ async authenticate( request: Request, sessionStorage: SessionStorage, From 84c17084ecc9d050bfb5a3a4580dcc892c7c71c8 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 19:05:17 -0500 Subject: [PATCH 02/44] generate/send totp --- src/constants.ts | 1 + src/index.ts | 530 ++++++++++++++++++++++++++------------------- src/utils.ts | 6 + test/index.spec.ts | 119 ++++++---- test/utils.ts | 2 +- 5 files changed, 394 insertions(+), 264 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 4a3731d..5ff0e00 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,7 @@ export const ERRORS = { REQUIRED_EMAIL: 'Email is required.', INVALID_EMAIL: 'Email is not valid.', INVALID_TOTP: 'Code is not valid.', + EXPIRED_TOTP: 'Code has expired.', INACTIVE_TOTP: 'Code is no longer active.', TOTP_NOT_FOUND: 'Database TOTP not found.', diff --git a/src/index.ts b/src/index.ts index 363e71b..b941c44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,14 +11,37 @@ import { signJWT, verifyJWT, ensureStringOrUndefined, + ensureObjectOrUndefined, } from './utils.js' import { STRATEGY_NAME, FORM_FIELDS, SESSION_KEYS, ERRORS } from './constants.js' +/** + * The TOTP data stored in the session. + */ +export interface TOTPData { + /** + * The encrypted TOTP. + */ + hash: string + + /** + * The expiration date for the TOTP secret. + * After this date, the TOTP will no longer be valid. + */ + expiresAt: Date + + /** + * The number of attempts the user tried to verify the TOTP. + * @default 0 + */ + attempts: number +} + /** * The TOTP data the application stores. * Used in CRUD functions provided by the application. */ -export interface TOTPData { +export interface TOTPDataDeprecated { /** * The encrypted TOTP. */ @@ -104,7 +127,7 @@ export interface MagicLinkGenerationOptions { * @param expiresAt The TOTP expiration date. */ export interface CreateTOTP { - (data: TOTPData, expiresAt: Date): Promise + (data: TOTPDataDeprecated, expiresAt: Date): Promise } /** @@ -112,7 +135,7 @@ export interface CreateTOTP { * @param hash The hash of the TOTP. */ export interface ReadTOTP { - (hash: string): Promise + (hash: string): Promise } /** @@ -123,7 +146,11 @@ export interface ReadTOTP { * @param expiresAt The TOTP expiration date. It is always the same as the expiration passed into createTOTP(). */ export interface UpdateTOTP { - (hash: string, data: Partial>, expiresAt: Date): Promise + ( + hash: string, + data: Partial>, + expiresAt: Date, + ): Promise } /** @@ -193,6 +220,11 @@ export interface CustomErrorsOptions { */ invalidTotp?: string + /** + * The expired TOTP error message. + */ + expiredTotp?: string + /** * The inactive TOTP error message. */ @@ -356,6 +388,7 @@ export class TOTPStrategy extends Strategy { requiredEmail: ERRORS.REQUIRED_EMAIL, invalidEmail: ERRORS.INVALID_EMAIL, invalidTotp: ERRORS.INVALID_TOTP, + expiredTotp: ERRORS.EXPIRED_TOTP, inactiveTotp: ERRORS.INACTIVE_TOTP, totpNotFound: ERRORS.TOTP_NOT_FOUND, } satisfies CustomErrorsOptions @@ -397,7 +430,7 @@ export class TOTPStrategy extends Strategy { * Authenticates a user using TOTP. * * If the user is already authenticated, simply returns the user. - * + * * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| * | POST | ✓ | ✗ | - | - | Generate new TOTP, send to user, store email and TOTP in session. | @@ -406,9 +439,16 @@ export class TOTPStrategy extends Strategy { * | POST | ≠ | - | ✓ | ✓ | Invalidate previous TOTP, generate new TOTP for new email. | * | GET | - | - | - | - | If magic-link enabled and URL has TOTP, validate it. If valid, authenticate user. | * + * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | + * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| + * | POST | ✓ | - | - | - | Generate new TOTP, send to user, store email and TOTP in session. | + * | POST | ✗ | ✗ | ✓ | - | Generate new TOTP for session email, send to user, store TOTP in session. | + * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | + * | GET | - | - | ✓ | ✓ | If magic-link enabled and URL has TOTP, validate against session. If valid, authenticate user. | + * * @param {Request} request - The request object. * @param {SessionStorage} sessionStorage - The session storage instance. - * @param {AuthenticateOptions} options - The authentication options. + * @param {AuthenticateOptions} options - The authentication options. successRedirect is required. * @returns {Promise} The authenticated user. */ async authenticate( @@ -419,225 +459,275 @@ export class TOTPStrategy extends Strategy { if (!this.secret) throw new Error(ERRORS.REQUIRED_ENV_SECRET) if (!options.successRedirect) throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) - const isPOST = request.method === 'POST' - const isGET = request.method === 'GET' - const session = await sessionStorage.getSession(request.headers.get('cookie')) + const user: User | null = session.get(options.sessionKey) ?? null + if (user) return this.success(user, request, sessionStorage, options) + + if (request.method === 'GET') throw new Error('GET not implemented.') + // let magicLinkTotp: string | undefined = undefined + const sessionEmail = ensureStringOrUndefined(session.get(this.sessionEmailKey)) - const sessionTotp = ensureStringOrUndefined(session.get(this.sessionTotpKey)) - const sessionTotpExpiresAt = ensureStringOrUndefined( - session.get(this.sessionTotpExpiresAtKey), - ) - - let user: User | null = session.get(options.sessionKey) ?? null - - let formData: FormData | undefined = undefined - let formDataEmail: string | undefined = undefined - let formDataTotp: string | undefined = undefined - let magicLinkTotp: string | undefined = undefined - - try { - if (!user) { - /** - * 1st Authentication Phase. - */ - if (isPOST) { - formData = await request.formData() - const form = Object.fromEntries(formData) - - formDataEmail = form[this.emailFieldKey] && String(form[this.emailFieldKey]) - formDataTotp = form[this.totpFieldKey] && String(form[this.totpFieldKey]) - - /** - * Re-send TOTP - User has requested a new TOTP. - * This will invalidate previous TOTP and assign session email to form email. - */ - if ( - !formDataEmail && - !formDataTotp && - sessionEmail && - sessionTotp && - sessionTotpExpiresAt - ) { - const expiresAt = new Date(sessionTotpExpiresAt) - await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - formDataEmail = sessionEmail - } - - /** - * Invalidate previous TOTP - User has submitted a new email address. - */ - if ( - formDataEmail && - sessionEmail && - formDataEmail !== sessionEmail && - sessionTotp && - sessionTotpExpiresAt - ) { - const expiresAt = new Date(sessionTotpExpiresAt) - await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - } - - /** - * First TOTP request. - */ - if (!formDataTotp) { - if (!formDataEmail) throw new Error(this.customErrors.requiredEmail) - await this.validateEmail(formDataEmail) - - // Generate, Sign and create Magic Link. - const { otp: _otp, ...totp } = generateTOTP({ - ...this.totpGeneration, - secret: generateSecret(), - }) - const signedTotp = await signJWT({ - payload: totp, - expiresIn: - this.totpGeneration.period ?? this._totpGenerationDefaults.period, - secretKey: this.secret, - }) - const magicLink = generateMagicLink({ - ...this.magicLinkGeneration, - param: this.totpFieldKey, - code: _otp, - request, - }) - - // Create TOTP in application storage. (Milliseconds since Unix epoch). - const expiresAtEpochMs = - Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 - const expiresAt = new Date(expiresAtEpochMs) - - await this.createTOTP( - { hash: signedTotp, active: true, attempts: 0 }, - expiresAt, - ) - - // Send TOTP. - await this.sendTOTP({ - email: formDataEmail, - code: _otp, - magicLink, - form: formData, - request, - }) - - session.set(this.sessionEmailKey, formDataEmail) - session.set(this.sessionTotpKey, signedTotp) - session.set(this.sessionTotpExpiresAtKey, expiresAt.toISOString()) - session.unset(options.sessionErrorKey) - - throw redirect(options.successRedirect, { - headers: { - 'set-cookie': await sessionStorage.commitSession(session, { - maxAge: this.maxAge, - }), - }, - }) - } - } - - /** - * 2nd Authentication Phase. - * Either via form submission or magic-link URL. - */ - if (isGET && this.magicLinkGeneration.enabled) { - const url = new URL(request.url) - - if (url.pathname !== this.magicLinkGeneration.callbackPath) { - throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) - } - - magicLinkTotp = url.searchParams.has(this.totpFieldKey) - ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') - : undefined - } - - if ((isPOST && formDataTotp) || (isGET && magicLinkTotp)) { - // Validation. - if (!sessionEmail || !sessionTotp || !sessionTotpExpiresAt) { - throw new Error(this.customErrors.inactiveTotp) - } - - const expiresAt = new Date(sessionTotpExpiresAt) - - if (isPOST && formDataTotp) { - await this._validateTOTP(sessionTotp, formDataTotp, expiresAt) - } - if (isGET && magicLinkTotp) { - await this._validateTOTP(sessionTotp, magicLinkTotp, expiresAt) - } - - // Invalidation. - await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - - // Allow developer to handle user validation. - user = await this.verify({ - email: sessionEmail, - form: formData, - magicLink: magicLinkTotp, - request, - }) - - session.set(options.sessionKey, user) - session.unset(this.sessionEmailKey) - session.unset(this.sessionTotpKey) - session.unset(this.sessionTotpExpiresAtKey) - session.unset(options.sessionErrorKey) - - throw redirect(options.successRedirect, { - headers: { - 'set-cookie': await sessionStorage.commitSession(session, { - maxAge: this.maxAge, - }), - }, - }) - } - } - } catch (error) { - // Allow Response to pass-through. - if (error instanceof Response && error.status === 302) throw error - if (error instanceof Error) { - if (error.message === ERRORS.INVALID_JWT) { - if (sessionTotp && sessionTotpExpiresAt) { - const dbTOTP = await this.readTOTP(sessionTotp) - if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) - - const expiresAt = new Date(sessionTotpExpiresAt) - await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - } - return await this.failure( - this.customErrors.inactiveTotp || ERRORS.INACTIVE_TOTP, - request, - sessionStorage, - options, - error, - ) - } - - return await this.failure(error.message, request, sessionStorage, options, error) - } - if (typeof error === 'string') { - return await this.failure( - error, - request, - sessionStorage, - options, - new Error(error), - ) - } - return await this.failure( - ERRORS.UNKNOWN_ERROR, + const sessionTotp = ensureObjectOrUndefined(session.get(this.sessionTotpKey)) + + const ensureNonEmptyStringOrNull = (value: unknown) => { + if (typeof value === 'string' && value.length > 0) return value + return null; + } + const formData = await request.formData() + const formDataEmail = ensureNonEmptyStringOrNull(formData.get(this.emailFieldKey)) + const formDataTotp = ensureNonEmptyStringOrNull(formData.get(this.totpFieldKey)) + const email = formDataEmail ?? (!formDataTotp ? sessionEmail : null) + console.log('authenticate:', {formDataEmail, formDataTotp, sessionEmail, sessionTotp, email}) + if (email) { + // Generate new TOTP. + await this.validateEmail(email) + + const { otp: code, ...totp } = generateTOTP({ + ...this.totpGeneration, + secret: generateSecret(), + }) + const hash = await signJWT({ + payload: totp, + expiresIn: this.totpGeneration.period ?? this._totpGenerationDefaults.period, + secretKey: this.secret, + }) + const magicLink = generateMagicLink({ + ...this.magicLinkGeneration, + code, + param: this.totpFieldKey, + request, + }) + await this.sendTOTP({ + email, + code, + magicLink, + form: formData, request, - sessionStorage, - options, - new Error(JSON.stringify(error, null, 2)), - ) + }) + + const expiresAtEpochMs = // (Milliseconds since Unix epoch). + Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 + const totpData: TOTPData = { + hash, + expiresAt: new Date(expiresAtEpochMs), + attempts: 0, + } + session.set(this.sessionEmailKey, email) + session.set(this.sessionTotpKey, totpData) + session.unset(options.sessionErrorKey) + throw redirect(options.successRedirect, { + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, + }) } - if (!user) throw new Error(ERRORS.USER_NOT_FOUND) - - return this.success(user, request, sessionStorage, options) + throw new Error('Not implemented.') + + // try { + // if (!user) { + // /** + // * 1st Authentication Phase. + // */ + // if (isPOST) { + // formData = await request.formData() + // const form = Object.fromEntries(formData) + + // formDataEmail = form[this.emailFieldKey] && String(form[this.emailFieldKey]) + // formDataTotp = form[this.totpFieldKey] && String(form[this.totpFieldKey]) + + // /** + // * Re-send TOTP - User has requested a new TOTP. + // * This will invalidate previous TOTP and assign session email to form email. + // */ + // if ( + // !formDataEmail && + // !formDataTotp && + // sessionEmail && + // sessionTotp && + // sessionTotpExpiresAt + // ) { + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // formDataEmail = sessionEmail + // } + + // /** + // * Invalidate previous TOTP - User has submitted a new email address. + // */ + // if ( + // formDataEmail && + // sessionEmail && + // formDataEmail !== sessionEmail && + // sessionTotp && + // sessionTotpExpiresAt + // ) { + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // } + + // /** + // * First TOTP request. + // */ + // if (!formDataTotp) { + // if (!formDataEmail) throw new Error(this.customErrors.requiredEmail) + // await this.validateEmail(formDataEmail) + + // // Generate, Sign and create Magic Link. + // const { otp: _otp, ...totp } = generateTOTP({ + // ...this.totpGeneration, + // secret: generateSecret(), + // }) + // const signedTotp = await signJWT({ + // payload: totp, + // expiresIn: + // this.totpGeneration.period ?? this._totpGenerationDefaults.period, + // secretKey: this.secret, + // }) + // const magicLink = generateMagicLink({ + // ...this.magicLinkGeneration, + // param: this.totpFieldKey, + // code: _otp, + // request, + // }) + + // // Create TOTP in application storage. (Milliseconds since Unix epoch). + // const expiresAtEpochMs = + // Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 + // const expiresAt = new Date(expiresAtEpochMs) + + // await this.createTOTP( + // { hash: signedTotp, active: true, attempts: 0 }, + // expiresAt, + // ) + + // // Send TOTP. + // await this.sendTOTP({ + // email: formDataEmail, + // code: _otp, + // magicLink, + // form: formData, + // request, + // }) + + // session.set(this.sessionEmailKey, formDataEmail) + // session.set(this.sessionTotpKey, signedTotp) + // session.set(this.sessionTotpExpiresAtKey, expiresAt.toISOString()) + // session.unset(options.sessionErrorKey) + + // throw redirect(options.successRedirect, { + // headers: { + // 'set-cookie': await sessionStorage.commitSession(session, { + // maxAge: this.maxAge, + // }), + // }, + // }) + // } + // } + + // /** + // * 2nd Authentication Phase. + // * Either via form submission or magic-link URL. + // */ + // if (isGET && this.magicLinkGeneration.enabled) { + // const url = new URL(request.url) + + // if (url.pathname !== this.magicLinkGeneration.callbackPath) { + // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) + // } + + // magicLinkTotp = url.searchParams.has(this.totpFieldKey) + // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') + // : undefined + // } + + // if ((isPOST && formDataTotp) || (isGET && magicLinkTotp)) { + // // Validation. + // if (!sessionEmail || !sessionTotp || !sessionTotpExpiresAt) { + // throw new Error(this.customErrors.inactiveTotp) + // } + + // const expiresAt = new Date(sessionTotpExpiresAt) + + // if (isPOST && formDataTotp) { + // await this._validateTOTP(sessionTotp, formDataTotp, expiresAt) + // } + // if (isGET && magicLinkTotp) { + // await this._validateTOTP(sessionTotp, magicLinkTotp, expiresAt) + // } + + // // Invalidation. + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + + // // Allow developer to handle user validation. + // user = await this.verify({ + // email: sessionEmail, + // form: formData, + // magicLink: magicLinkTotp, + // request, + // }) + + // session.set(options.sessionKey, user) + // session.unset(this.sessionEmailKey) + // session.unset(this.sessionTotpKey) + // session.unset(this.sessionTotpExpiresAtKey) + // session.unset(options.sessionErrorKey) + + // throw redirect(options.successRedirect, { + // headers: { + // 'set-cookie': await sessionStorage.commitSession(session, { + // maxAge: this.maxAge, + // }), + // }, + // }) + // } + // } + // } catch (error) { + // // Allow Response to pass-through. + // if (error instanceof Response && error.status === 302) throw error + // if (error instanceof Error) { + // if (error.message === ERRORS.INVALID_JWT) { + // if (sessionTotp && sessionTotpExpiresAt) { + // const dbTOTP = await this.readTOTP(sessionTotp) + // if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) + + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // } + // return await this.failure( + // this.customErrors.inactiveTotp || ERRORS.INACTIVE_TOTP, + // request, + // sessionStorage, + // options, + // error, + // ) + // } + + // return await this.failure(error.message, request, sessionStorage, options, error) + // } + // if (typeof error === 'string') { + // return await this.failure( + // error, + // request, + // sessionStorage, + // options, + // new Error(error), + // ) + // } + // return await this.failure( + // ERRORS.UNKNOWN_ERROR, + // request, + // sessionStorage, + // options, + // new Error(JSON.stringify(error, null, 2)), + // ) + // } + + // if (!user) throw new Error(ERRORS.USER_NOT_FOUND) + + // return this.success(user, request, sessionStorage, options) } private async _validateEmailDefaults(email: string) { diff --git a/src/utils.ts b/src/utils.ts index 2b46fa7..68cfa8b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,3 +86,9 @@ export function ensureStringOrUndefined(value: unknown) { } return value } +export function ensureObjectOrUndefined(value: unknown) { + if ((typeof value !== 'object' && value !== undefined) || value === null) { + throw new Error('Value must be a object or undefined.') + } + return value +} diff --git a/test/index.spec.ts b/test/index.spec.ts index beea9a9..863f3d0 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,5 +1,9 @@ import type { Session } from '@remix-run/server-runtime' -import type { SendTOTPOptions, TOTPData, TOTPStrategyOptions } from '../src/index' +import type { + SendTOTPOptions, + TOTPDataDeprecated, + TOTPStrategyOptions, +} from '../src/index' import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest' import invariant from 'tiny-invariant' @@ -75,7 +79,7 @@ describe('[ Basics ]', () => { ).rejects.toThrow(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) }) - test('Should throw a custom Error message.', async () => { + test.skip('Should throw a custom Error message.', async () => { const CUSTOM_ERROR = 'Custom error message.' const strategy = new TOTPStrategy( { @@ -103,7 +107,7 @@ describe('[ Basics ]', () => { describe('[ TOTP ]', () => { describe('1st Authentication Phase', () => { - test('Should throw an Error on missing formData email.', async () => { + test.skip('Should throw an Error on missing formData email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, '') @@ -135,98 +139,127 @@ describe('[ TOTP ]', () => { ).rejects.toThrow(ERRORS.INVALID_EMAIL) }) - test('Should call createTOTP function.', async () => { + test('Should generate/send TOTP for form email.', async () => { + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + }) const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - const request = new Request(`${HOST_URL}`, { + const request = new Request(`${HOST_URL}/login`, { method: 'POST', body: formData, }) await strategy .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', + successRedirect: '/verify', }) - .catch((reason) => { + .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toMatch('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() } else throw reason }) - expect(createTOTP).toHaveBeenCalledTimes(1) + expect(sendTOTP).toHaveBeenCalledTimes(1) }) - test('Should call sendTOTP function.', async () => { + test('Should generate/send TOTP for form email ignoring any form totp code.', async () => { + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + }) const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - const request = new Request(`${HOST_URL}`, { + formData.append(FORM_FIELDS.TOTP, '123456') + const request = new Request(`${HOST_URL}/login`, { method: 'POST', body: formData, }) await strategy .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', + successRedirect: '/verify', }) - .catch((reason) => { + .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toMatch('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() } else throw reason }) expect(sendTOTP).toHaveBeenCalledTimes(1) }) - test('Should contain auth:email, auth:totp, and auth:totpExpiresAt properties in session.', async () => { + test.only('Should generate/send TOTP for empty form data with session email.', async () => { + let session: Session | undefined + let sessionTotp: unknown const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - const request = new Request(`${HOST_URL}`, { + const requestToPopulateSessionEmail = new Request(`${HOST_URL}/login`, { method: 'POST', body: formData, }) await strategy - .authenticate(request, sessionStorage, { + .authenticate(requestToPopulateSessionEmail, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', + successRedirect: '/verify', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - const session = await sessionStorage.getSession( + expect(reason.headers.get('location')).toMatch('/verify') + session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) - expect(session.data).toHaveProperty(SESSION_KEYS.EMAIL) - expect(session.data).toHaveProperty(SESSION_KEYS.TOTP) - expect(session.data).toHaveProperty(SESSION_KEYS.TOTP_EXPIRES_AT) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + sessionTotp = session.get(SESSION_KEYS.TOTP) } else throw reason }) - }) - - test('Should contain Location header pointing to provided successRedirect url.', async () => { - const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - const request = new Request(`${HOST_URL}`, { + if (!session) throw new Error('Undefined session.') + const emptyFormRequest = new Request(`${HOST_URL}/login`, { method: 'POST', - body: formData, + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: new FormData(), }) await strategy - .authenticate(request, sessionStorage, { + .authenticate(emptyFormRequest, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', }) - .catch((reason) => { + .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) expect(reason.headers.get('location')).toMatch('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + expect(session.get(SESSION_KEYS.TOTP)).not.toEqual(sessionTotp) } else throw reason }) + expect(sendTOTP).toHaveBeenCalledTimes(2) }) - test('Re-send TOTP - Should invalidate previous TOTP.', async () => { + test.skip('Re-send TOTP - Should invalidate previous TOTP.', async () => { updateTOTP.mockImplementation(async (_, { active }) => { expect(active).toBe(false) }) @@ -260,12 +293,12 @@ describe('[ TOTP ]', () => { }) }) - describe('2nd Authentication Phase', () => { + describe.skip('2nd Authentication Phase', () => { async function setupFirstAuthPhase( totpStrategyOptions: Partial = {}, ) { const user = { name: 'Joe Schmoe' } - let totpData: TOTPData | undefined + let totpData: TOTPDataDeprecated | undefined let totpDataExpiresAt: Date | undefined let sendTOTPOptions: SendTOTPOptions | undefined let session: Session | undefined @@ -340,7 +373,7 @@ describe('[ TOTP ]', () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() for (let i = 0; i < TOTP_GENERATION_DEFAULTS.maxAttempts + 1; i++) { const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + i) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code + i) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -366,7 +399,7 @@ describe('[ TOTP ]', () => { readTOTP, }) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -389,7 +422,7 @@ describe('[ TOTP ]', () => { customErrors: { totpNotFound: CUSTOM_ERROR }, }) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -411,7 +444,7 @@ describe('[ TOTP ]', () => { Promise.resolve({ hash: 'SIGNED-JWT', attempts: 0, active: false }), }) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -433,7 +466,7 @@ describe('[ TOTP ]', () => { new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), ) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -490,7 +523,7 @@ describe('[ TOTP ]', () => { test('Should successfully validate TOTP.', async () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -514,7 +547,7 @@ describe('[ TOTP ]', () => { test('Should contain user property in session.', async () => { const { strategy, session, sendTOTPOptions, user } = await setupFirstAuthPhase() const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -540,10 +573,10 @@ describe('[ TOTP ]', () => { }) }) - describe('End to End', () => { + describe.skip('End to End', () => { test('Should authenticate user with valid TOTP.', async () => { const user = { name: 'Joe Schmoe' } - let totpData: TOTPData | undefined + let totpData: TOTPDataDeprecated | undefined let totpDataExpiresAt: Date | undefined let sendTOTPOptions: SendTOTPOptions | undefined let session: Session | undefined @@ -621,7 +654,7 @@ describe('[ TOTP ]', () => { ) { const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}`, { method: 'POST', diff --git a/test/utils.ts b/test/utils.ts index c6a0bf2..2e76b66 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -11,7 +11,7 @@ import * as crypto from 'crypto' */ export const SECRET_ENV = 'SECRET_ENV' export const HOST_URL = 'https://prodserver.com' -export const DEFAULT_EMAIL = 'localhost@3000.com' +export const DEFAULT_EMAIL = 'user@gmail.com' /** * Strategy Defaults. From 451dd9a5acb0d2e2471d25d60ebcf0178e2b0d91 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 19:22:05 -0500 Subject: [PATCH 03/44] 1st authentication phase tests --- test/index.spec.ts | 88 +++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 863f3d0..f4c3384 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -171,7 +171,7 @@ describe('[ TOTP ]', () => { expect(sendTOTP).toHaveBeenCalledTimes(1) }) - test('Should generate/send TOTP for form email ignoring any form totp code.', async () => { + test('Should generate/send TOTP for form email ignoring form totp code.', async () => { sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { expect(options.email).toBe(DEFAULT_EMAIL) expect(options.code).to.not.equal('') @@ -204,15 +204,15 @@ describe('[ TOTP ]', () => { expect(sendTOTP).toHaveBeenCalledTimes(1) }) - test.only('Should generate/send TOTP for empty form data with session email.', async () => { + test('Should generate/send TOTP for form email ignoring session email.', async () => { let session: Session | undefined let sessionTotp: unknown const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + const formDataToPopulateSessionEmail = new FormData() + formDataToPopulateSessionEmail.append(FORM_FIELDS.EMAIL, 'email@session.com') const requestToPopulateSessionEmail = new Request(`${HOST_URL}/login`, { method: 'POST', - body: formData, + body: formDataToPopulateSessionEmail, }) await strategy .authenticate(requestToPopulateSessionEmail, sessionStorage, { @@ -226,21 +226,27 @@ describe('[ TOTP ]', () => { session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) - expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.EMAIL)).toBe('email@session.com') expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() sessionTotp = session.get(SESSION_KEYS.TOTP) } else throw reason }) - if (!session) throw new Error('Undefined session.') - const emptyFormRequest = new Request(`${HOST_URL}/login`, { + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + }) + if (!session) throw new Error('Undefined session.') + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + const request = new Request(`${HOST_URL}/login`, { method: 'POST', headers: { cookie: await sessionStorage.commitSession(session), }, - body: new FormData(), + body: formData, }) await strategy - .authenticate(emptyFormRequest, sessionStorage, { + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', }) @@ -256,40 +262,66 @@ describe('[ TOTP ]', () => { expect(session.get(SESSION_KEYS.TOTP)).not.toEqual(sessionTotp) } else throw reason }) - expect(sendTOTP).toHaveBeenCalledTimes(2) + expect(sendTOTP).toHaveBeenCalledTimes(2) }) - test.skip('Re-send TOTP - Should invalidate previous TOTP.', async () => { - updateTOTP.mockImplementation(async (_, { active }) => { - expect(active).toBe(false) + test('Should generate/send TOTP for empty form data with session email.', async () => { + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') }) - + let session: Session | undefined + let sessionTotp: unknown const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const session = await sessionStorage.getSession() - session.set(SESSION_KEYS.EMAIL, DEFAULT_EMAIL) - session.set(SESSION_KEYS.TOTP, 'JWT-SIGNED') - session.set( - SESSION_KEYS.TOTP_EXPIRES_AT, - new Date(Date.now() + TOTP_GENERATION_DEFAULTS.period * 1000).toISOString(), - ) - const request = new Request(`${HOST_URL}`, { + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + const requestToPopulateSessionEmail = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(requestToPopulateSessionEmail, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toMatch('/verify') + session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + sessionTotp = session.get(SESSION_KEYS.TOTP) + } else throw reason + }) + if (!session) throw new Error('Undefined session.') + const emptyFormRequest = new Request(`${HOST_URL}/login`, { method: 'POST', headers: { cookie: await sessionStorage.commitSession(session), }, - body: new FormData(), // Empty form data indicates re-send new TOTP. + body: new FormData(), }) await strategy - .authenticate(request, sessionStorage, { + .authenticate(emptyFormRequest, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', + successRedirect: '/verify', }) - .catch((reason) => { + .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toMatch('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + expect(session.get(SESSION_KEYS.TOTP)).not.toEqual(sessionTotp) } else throw reason }) - expect(updateTOTP).toHaveBeenCalledTimes(1) + expect(sendTOTP).toHaveBeenCalledTimes(2) }) }) From a06fec114a128b3a5185d4fbaef380347884b83e Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 19:32:26 -0500 Subject: [PATCH 04/44] cleanup --- src/index.ts | 24 ++++++++++++------------ test/index.spec.ts | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index b941c44..634e631 100644 --- a/src/index.ts +++ b/src/index.ts @@ -431,20 +431,20 @@ export class TOTPStrategy extends Strategy { * * If the user is already authenticated, simply returns the user. * - * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | + * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| - * | POST | ✓ | ✗ | - | - | Generate new TOTP, send to user, store email and TOTP in session. | - * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | - * | POST | ✗ | ✗ | ✓ | ✓ | Invalidate previous TOTP, generate new one if session has email and TOTP. | - * | POST | ≠ | - | ✓ | ✓ | Invalidate previous TOTP, generate new TOTP for new email. | - * | GET | - | - | - | - | If magic-link enabled and URL has TOTP, validate it. If valid, authenticate user. | + * | POST | ✓ | ✗ | - | - | Generate new TOTP, send to user, store email and TOTP in session. | + * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | + * | POST | ✗ | ✗ | ✓ | ✓ | Invalidate previous TOTP, generate new one if session has email and TOTP. | + * | POST | ≠ | - | ✓ | ✓ | Invalidate previous TOTP, generate new TOTP for new email. | + * | GET | - | - | - | - | If magic-link enabled and URL has TOTP, validate it. If valid, authenticate user. | * - * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | - * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| - * | POST | ✓ | - | - | - | Generate new TOTP, send to user, store email and TOTP in session. | - * | POST | ✗ | ✗ | ✓ | - | Generate new TOTP for session email, send to user, store TOTP in session. | - * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | - * | GET | - | - | ✓ | ✓ | If magic-link enabled and URL has TOTP, validate against session. If valid, authenticate user. | + * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | + * |--------|-------|------|-------------|------------|------------------------------------------| + * | POST | ✓ | - | - | - | Generate/send TOTP using form email. | + * | POST | ✗ | ✗ | ✓ | - | Generate/send TOTP for session email. | + * | POST | ✗ | ✓ | ✓ | ✓ | Validate form TOTP. | + * | GET | - | - | ✓ | ✓ | Validate magic link TOTP. | * * @param {Request} request - The request object. * @param {SessionStorage} sessionStorage - The session storage instance. diff --git a/test/index.spec.ts b/test/index.spec.ts index f4c3384..82b119f 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -106,7 +106,7 @@ describe('[ Basics ]', () => { }) describe('[ TOTP ]', () => { - describe('1st Authentication Phase', () => { + describe('Generate/Send TOTP', () => { test.skip('Should throw an Error on missing formData email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() @@ -325,7 +325,7 @@ describe('[ TOTP ]', () => { }) }) - describe.skip('2nd Authentication Phase', () => { + describe.skip('Validate TOTP', () => { async function setupFirstAuthPhase( totpStrategyOptions: Partial = {}, ) { From 0fdfa9271c71c00acf886ca5c1e811998674195f Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 21:37:51 -0500 Subject: [PATCH 05/44] generateAndSendTOTP --- src/index.ts | 151 ++++++++++++++++++++++++++++++++------------------- src/utils.ts | 6 ++ 2 files changed, 101 insertions(+), 56 deletions(-) diff --git a/src/index.ts b/src/index.ts index 634e631..bcdf5fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import type { SessionStorage } from '@remix-run/server-runtime' +import type { Session, SessionStorage } from '@remix-run/server-runtime' import type { AuthenticateOptions, StrategyVerifyCallback } from 'remix-auth' import { redirect } from '@remix-run/server-runtime' @@ -12,6 +12,7 @@ import { verifyJWT, ensureStringOrUndefined, ensureObjectOrUndefined, + ensureNonEmptyStringOrNull, } from './utils.js' import { STRATEGY_NAME, FORM_FIELDS, SESSION_KEYS, ERRORS } from './constants.js' @@ -463,69 +464,43 @@ export class TOTPStrategy extends Strategy { const user: User | null = session.get(options.sessionKey) ?? null if (user) return this.success(user, request, sessionStorage, options) - if (request.method === 'GET') throw new Error('GET not implemented.') - // let magicLinkTotp: string | undefined = undefined - - const sessionEmail = ensureStringOrUndefined(session.get(this.sessionEmailKey)) - const sessionTotp = ensureObjectOrUndefined(session.get(this.sessionTotpKey)) - - const ensureNonEmptyStringOrNull = (value: unknown) => { - if (typeof value === 'string' && value.length > 0) return value - return null; - } - const formData = await request.formData() + const formData = request.method === 'POST' ? await request.formData() : new FormData() const formDataEmail = ensureNonEmptyStringOrNull(formData.get(this.emailFieldKey)) const formDataTotp = ensureNonEmptyStringOrNull(formData.get(this.totpFieldKey)) + const sessionEmail = ensureStringOrUndefined(session.get(this.sessionEmailKey)) + const sessionTotp = ensureObjectOrUndefined(session.get(this.sessionTotpKey)) const email = formDataEmail ?? (!formDataTotp ? sessionEmail : null) - console.log('authenticate:', {formDataEmail, formDataTotp, sessionEmail, sessionTotp, email}) - if (email) { - // Generate new TOTP. - await this.validateEmail(email) - - const { otp: code, ...totp } = generateTOTP({ - ...this.totpGeneration, - secret: generateSecret(), - }) - const hash = await signJWT({ - payload: totp, - expiresIn: this.totpGeneration.period ?? this._totpGenerationDefaults.period, - secretKey: this.secret, - }) - const magicLink = generateMagicLink({ - ...this.magicLinkGeneration, - code, - param: this.totpFieldKey, - request, - }) - await this.sendTOTP({ + console.log('authenticate:', { + formDataEmail, + formDataTotp, + sessionEmail, + sessionTotp, + email, + }) + if (email) + await this._generateAndSendTOTP({ email, - code, - magicLink, - form: formData, + session, + sessionStorage, request, + formData, + options, }) - const expiresAtEpochMs = // (Milliseconds since Unix epoch). - Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 - const totpData: TOTPData = { - hash, - expiresAt: new Date(expiresAtEpochMs), - attempts: 0, - } - session.set(this.sessionEmailKey, email) - session.set(this.sessionTotpKey, totpData) - session.unset(options.sessionErrorKey) - throw redirect(options.successRedirect, { - headers: { - 'set-cookie': await sessionStorage.commitSession(session, { - maxAge: this.maxAge, - }), - }, - }) - } - throw new Error('Not implemented.') + // if (request.method === 'GET') { + // if (this.magicLinkGeneration.enabled) { + // const url = new URL(request.url) + // if (url.pathname !== this.magicLinkGeneration.callbackPath) { + // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) + // } + // const code = url.searchParams.has(this.totpFieldKey) + // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') + // : undefined + // } + // } + // try { // if (!user) { // /** @@ -730,12 +705,76 @@ export class TOTPStrategy extends Strategy { // return this.success(user, request, sessionStorage, options) } + private async _generateAndSendTOTP({ + email, + session, + sessionStorage, + request, + formData, + options, + }: { + email: string + session: Session + sessionStorage: SessionStorage + request: Request + formData: FormData + options: AuthenticateOptions + }) { + if (!options.successRedirect) throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) + + await this.validateEmail(email) + const { otp: code, ...totp } = generateTOTP({ + ...this.totpGeneration, + secret: generateSecret(), + }) + const hash = await signJWT({ + payload: totp, + expiresIn: this.totpGeneration.period ?? this._totpGenerationDefaults.period, + secretKey: this.secret, + }) + const magicLink = generateMagicLink({ + ...this.magicLinkGeneration, + code, + param: this.totpFieldKey, + request, + }) + await this.sendTOTP({ + email, + code, + magicLink, + form: formData, + request, + }) + + const expiresAtEpochMs = // (Milliseconds since Unix epoch). + Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 + const totpData: TOTPData = { + hash, + expiresAt: new Date(expiresAtEpochMs), + attempts: 0, + } + session.set(this.sessionEmailKey, email) + session.set(this.sessionTotpKey, totpData) + session.unset(options.sessionErrorKey) + throw redirect(options.successRedirect, { + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, + }) + } + private async _validateEmailDefaults(email: string) { const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/gm if (!regexEmail.test(email)) throw new Error(this.customErrors.invalidEmail) } - private async _validateTOTP(sessionTotp: string, otp: string, expiresAt: Date) { + private async _validateTOTPDeprecated( + sessionTotp: string, + otp: string, + expiresAt: Date, + ) { // Retrieve encrypted TOTP from database. const dbTOTP = await this.readTOTP(sessionTotp) if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) diff --git a/src/utils.ts b/src/utils.ts index 68cfa8b..2b912a6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,6 +80,12 @@ export async function verifyJWT({ jwt, secretKey }: VerifyJWTOptions) { /** * Miscellaneous. */ + +export function ensureNonEmptyStringOrNull(value: unknown) { + if (typeof value === 'string' && value.length > 0) return value + return null +} + export function ensureStringOrUndefined(value: unknown) { if (typeof value !== 'string' && value !== undefined) { throw new Error('Value must be a string or undefined.') From 9cf10989b1f486353ad20b4f1b047e118cc1dbf5 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 22:23:21 -0500 Subject: [PATCH 06/44] validateTOTP --- src/index.ts | 483 ++++++++++++++++++++++++++++----------------------- 1 file changed, 267 insertions(+), 216 deletions(-) diff --git a/src/index.ts b/src/index.ts index bcdf5fd..edb45ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -477,7 +477,7 @@ export class TOTPStrategy extends Strategy { sessionTotp, email, }) - if (email) + if (email) { await this._generateAndSendTOTP({ email, session, @@ -486,225 +486,240 @@ export class TOTPStrategy extends Strategy { formData, options, }) + } + const code = formDataTotp ?? this._getMagicLinkCode(request) + if (code) { + if (!sessionEmail || !sessionTotp) throw new Error(ERRORS.EXPIRED_TOTP) + this._validateTOTP({ code, sessionTotp: sessionTotp as TOTPData }) + + // Allow developer to handle user validation. + const user = await this.verify({ + email: sessionEmail, + form: formData, + request, + }) - throw new Error('Not implemented.') + session.set(options.sessionKey, user) + session.unset(this.sessionEmailKey) + session.unset(this.sessionTotpKey) + session.unset(options.sessionErrorKey) + + throw redirect(options.successRedirect, { + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, + }) + } - // if (request.method === 'GET') { - // if (this.magicLinkGeneration.enabled) { - // const url = new URL(request.url) - // if (url.pathname !== this.magicLinkGeneration.callbackPath) { - // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) - // } - // const code = url.searchParams.has(this.totpFieldKey) - // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') - // : undefined - // } - // } - - // try { - // if (!user) { - // /** - // * 1st Authentication Phase. - // */ - // if (isPOST) { - // formData = await request.formData() - // const form = Object.fromEntries(formData) - - // formDataEmail = form[this.emailFieldKey] && String(form[this.emailFieldKey]) - // formDataTotp = form[this.totpFieldKey] && String(form[this.totpFieldKey]) - - // /** - // * Re-send TOTP - User has requested a new TOTP. - // * This will invalidate previous TOTP and assign session email to form email. - // */ - // if ( - // !formDataEmail && - // !formDataTotp && - // sessionEmail && - // sessionTotp && - // sessionTotpExpiresAt - // ) { - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // formDataEmail = sessionEmail - // } - - // /** - // * Invalidate previous TOTP - User has submitted a new email address. - // */ - // if ( - // formDataEmail && - // sessionEmail && - // formDataEmail !== sessionEmail && - // sessionTotp && - // sessionTotpExpiresAt - // ) { - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // } - - // /** - // * First TOTP request. - // */ - // if (!formDataTotp) { - // if (!formDataEmail) throw new Error(this.customErrors.requiredEmail) - // await this.validateEmail(formDataEmail) - - // // Generate, Sign and create Magic Link. - // const { otp: _otp, ...totp } = generateTOTP({ - // ...this.totpGeneration, - // secret: generateSecret(), - // }) - // const signedTotp = await signJWT({ - // payload: totp, - // expiresIn: - // this.totpGeneration.period ?? this._totpGenerationDefaults.period, - // secretKey: this.secret, - // }) - // const magicLink = generateMagicLink({ - // ...this.magicLinkGeneration, - // param: this.totpFieldKey, - // code: _otp, - // request, - // }) - - // // Create TOTP in application storage. (Milliseconds since Unix epoch). - // const expiresAtEpochMs = - // Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 - // const expiresAt = new Date(expiresAtEpochMs) - - // await this.createTOTP( - // { hash: signedTotp, active: true, attempts: 0 }, - // expiresAt, - // ) - - // // Send TOTP. - // await this.sendTOTP({ - // email: formDataEmail, - // code: _otp, - // magicLink, - // form: formData, - // request, - // }) - - // session.set(this.sessionEmailKey, formDataEmail) - // session.set(this.sessionTotpKey, signedTotp) - // session.set(this.sessionTotpExpiresAtKey, expiresAt.toISOString()) - // session.unset(options.sessionErrorKey) - - // throw redirect(options.successRedirect, { - // headers: { - // 'set-cookie': await sessionStorage.commitSession(session, { - // maxAge: this.maxAge, - // }), - // }, - // }) - // } - // } - - // /** - // * 2nd Authentication Phase. - // * Either via form submission or magic-link URL. - // */ - // if (isGET && this.magicLinkGeneration.enabled) { - // const url = new URL(request.url) - - // if (url.pathname !== this.magicLinkGeneration.callbackPath) { - // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) - // } - - // magicLinkTotp = url.searchParams.has(this.totpFieldKey) - // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') - // : undefined - // } - - // if ((isPOST && formDataTotp) || (isGET && magicLinkTotp)) { - // // Validation. - // if (!sessionEmail || !sessionTotp || !sessionTotpExpiresAt) { - // throw new Error(this.customErrors.inactiveTotp) - // } - - // const expiresAt = new Date(sessionTotpExpiresAt) - - // if (isPOST && formDataTotp) { - // await this._validateTOTP(sessionTotp, formDataTotp, expiresAt) - // } - // if (isGET && magicLinkTotp) { - // await this._validateTOTP(sessionTotp, magicLinkTotp, expiresAt) - // } - - // // Invalidation. - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - - // // Allow developer to handle user validation. - // user = await this.verify({ - // email: sessionEmail, - // form: formData, - // magicLink: magicLinkTotp, - // request, - // }) - - // session.set(options.sessionKey, user) - // session.unset(this.sessionEmailKey) - // session.unset(this.sessionTotpKey) - // session.unset(this.sessionTotpExpiresAtKey) - // session.unset(options.sessionErrorKey) - - // throw redirect(options.successRedirect, { - // headers: { - // 'set-cookie': await sessionStorage.commitSession(session, { - // maxAge: this.maxAge, - // }), - // }, - // }) - // } - // } - // } catch (error) { - // // Allow Response to pass-through. - // if (error instanceof Response && error.status === 302) throw error - // if (error instanceof Error) { - // if (error.message === ERRORS.INVALID_JWT) { - // if (sessionTotp && sessionTotpExpiresAt) { - // const dbTOTP = await this.readTOTP(sessionTotp) - // if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) - - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // } - // return await this.failure( - // this.customErrors.inactiveTotp || ERRORS.INACTIVE_TOTP, - // request, - // sessionStorage, - // options, - // error, - // ) - // } - - // return await this.failure(error.message, request, sessionStorage, options, error) - // } - // if (typeof error === 'string') { - // return await this.failure( - // error, - // request, - // sessionStorage, - // options, - // new Error(error), - // ) - // } - // return await this.failure( - // ERRORS.UNKNOWN_ERROR, - // request, - // sessionStorage, - // options, - // new Error(JSON.stringify(error, null, 2)), - // ) - // } - - // if (!user) throw new Error(ERRORS.USER_NOT_FOUND) - - // return this.success(user, request, sessionStorage, options) + throw new Error('Not implemented.') } + // try { + // if (!user) { + // /** + // * 1st Authentication Phase. + // */ + // if (isPOST) { + // formData = await request.formData() + // const form = Object.fromEntries(formData) + + // formDataEmail = form[this.emailFieldKey] && String(form[this.emailFieldKey]) + // formDataTotp = form[this.totpFieldKey] && String(form[this.totpFieldKey]) + + // /** + // * Re-send TOTP - User has requested a new TOTP. + // * This will invalidate previous TOTP and assign session email to form email. + // */ + // if ( + // !formDataEmail && + // !formDataTotp && + // sessionEmail && + // sessionTotp && + // sessionTotpExpiresAt + // ) { + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // formDataEmail = sessionEmail + // } + + // /** + // * Invalidate previous TOTP - User has submitted a new email address. + // */ + // if ( + // formDataEmail && + // sessionEmail && + // formDataEmail !== sessionEmail && + // sessionTotp && + // sessionTotpExpiresAt + // ) { + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // } + + // /** + // * First TOTP request. + // */ + // if (!formDataTotp) { + // if (!formDataEmail) throw new Error(this.customErrors.requiredEmail) + // await this.validateEmail(formDataEmail) + + // // Generate, Sign and create Magic Link. + // const { otp: _otp, ...totp } = generateTOTP({ + // ...this.totpGeneration, + // secret: generateSecret(), + // }) + // const signedTotp = await signJWT({ + // payload: totp, + // expiresIn: + // this.totpGeneration.period ?? this._totpGenerationDefaults.period, + // secretKey: this.secret, + // }) + // const magicLink = generateMagicLink({ + // ...this.magicLinkGeneration, + // param: this.totpFieldKey, + // code: _otp, + // request, + // }) + + // // Create TOTP in application storage. (Milliseconds since Unix epoch). + // const expiresAtEpochMs = + // Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 + // const expiresAt = new Date(expiresAtEpochMs) + + // await this.createTOTP( + // { hash: signedTotp, active: true, attempts: 0 }, + // expiresAt, + // ) + + // // Send TOTP. + // await this.sendTOTP({ + // email: formDataEmail, + // code: _otp, + // magicLink, + // form: formData, + // request, + // }) + + // session.set(this.sessionEmailKey, formDataEmail) + // session.set(this.sessionTotpKey, signedTotp) + // session.set(this.sessionTotpExpiresAtKey, expiresAt.toISOString()) + // session.unset(options.sessionErrorKey) + + // throw redirect(options.successRedirect, { + // headers: { + // 'set-cookie': await sessionStorage.commitSession(session, { + // maxAge: this.maxAge, + // }), + // }, + // }) + // } + // } + + // /** + // * 2nd Authentication Phase. + // * Either via form submission or magic-link URL. + // */ + // if (isGET && this.magicLinkGeneration.enabled) { + // const url = new URL(request.url) + + // if (url.pathname !== this.magicLinkGeneration.callbackPath) { + // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) + // } + + // magicLinkTotp = url.searchParams.has(this.totpFieldKey) + // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') + // : undefined + // } + + // if ((isPOST && formDataTotp) || (isGET && magicLinkTotp)) { + // // Validation. + // if (!sessionEmail || !sessionTotp || !sessionTotpExpiresAt) { + // throw new Error(this.customErrors.inactiveTotp) + // } + + // const expiresAt = new Date(sessionTotpExpiresAt) + + // if (isPOST && formDataTotp) { + // await this._validateTOTP(sessionTotp, formDataTotp, expiresAt) + // } + // if (isGET && magicLinkTotp) { + // await this._validateTOTP(sessionTotp, magicLinkTotp, expiresAt) + // } + + // // Invalidation. + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + + // // Allow developer to handle user validation. + // user = await this.verify({ + // email: sessionEmail, + // form: formData, + // magicLink: magicLinkTotp, + // request, + // }) + + // session.set(options.sessionKey, user) + // session.unset(this.sessionEmailKey) + // session.unset(this.sessionTotpKey) + // session.unset(this.sessionTotpExpiresAtKey) + // session.unset(options.sessionErrorKey) + + // throw redirect(options.successRedirect, { + // headers: { + // 'set-cookie': await sessionStorage.commitSession(session, { + // maxAge: this.maxAge, + // }), + // }, + // }) + // } + // } + // } catch (error) { + // // Allow Response to pass-through. + // if (error instanceof Response && error.status === 302) throw error + // if (error instanceof Error) { + // if (error.message === ERRORS.INVALID_JWT) { + // if (sessionTotp && sessionTotpExpiresAt) { + // const dbTOTP = await this.readTOTP(sessionTotp) + // if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) + + // const expiresAt = new Date(sessionTotpExpiresAt) + // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) + // } + // return await this.failure( + // this.customErrors.inactiveTotp || ERRORS.INACTIVE_TOTP, + // request, + // sessionStorage, + // options, + // error, + // ) + // } + + // return await this.failure(error.message, request, sessionStorage, options, error) + // } + // if (typeof error === 'string') { + // return await this.failure( + // error, + // request, + // sessionStorage, + // options, + // new Error(error), + // ) + // } + // return await this.failure( + // ERRORS.UNKNOWN_ERROR, + // request, + // sessionStorage, + // options, + // new Error(JSON.stringify(error, null, 2)), + // ) + // } + + // if (!user) throw new Error(ERRORS.USER_NOT_FOUND) + + // return this.success(user, request, sessionStorage, options) + // } + private async _generateAndSendTOTP({ email, session, @@ -765,11 +780,47 @@ export class TOTPStrategy extends Strategy { }) } + private _getMagicLinkCode(request: Request) { + if (request.method === 'GET') { + if (this.magicLinkGeneration.enabled) { + const url = new URL(request.url) + if (url.pathname !== this.magicLinkGeneration.callbackPath) { + throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) + } + if (url.searchParams.has(this.totpFieldKey)) { + return decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') + } + } + } + return null + } + private async _validateEmailDefaults(email: string) { const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/gm if (!regexEmail.test(email)) throw new Error(this.customErrors.invalidEmail) } + private async _validateTOTP({ + code, + sessionTotp, + }: { + code: string + sessionTotp: TOTPData + }) { + // Decryption and Verification. + const { ...totp } = (await verifyJWT({ + jwt: sessionTotp.hash, + secretKey: this.secret, + })) as Required + + // Verify TOTP (@epic-web/totp). + const isValid = verifyTOTP({ ...totp, otp: code }) + if (!isValid) { + // await this.updateTOTP(sessionTotp, { attempts: dbTOTP.attempts + 1 }, expiresAt) + throw new Error(this.customErrors.invalidTotp) + } + } + private async _validateTOTPDeprecated( sessionTotp: string, otp: string, From e51852b3f15c6ce6917675b4263eca906bb21e71 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 1 Jan 2024 23:06:28 -0500 Subject: [PATCH 07/44] validate magic link test --- src/index.ts | 10 ++++- test/index.spec.ts | 95 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index edb45ef..f19ef8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -469,7 +469,10 @@ export class TOTPStrategy extends Strategy { const formDataTotp = ensureNonEmptyStringOrNull(formData.get(this.totpFieldKey)) const sessionEmail = ensureStringOrUndefined(session.get(this.sessionEmailKey)) const sessionTotp = ensureObjectOrUndefined(session.get(this.sessionTotpKey)) - const email = formDataEmail ?? (!formDataTotp ? sessionEmail : null) + const email = + request.method === 'POST' + ? formDataEmail ?? (!formDataTotp ? sessionEmail : null) + : null console.log('authenticate:', { formDataEmail, formDataTotp, @@ -490,7 +493,7 @@ export class TOTPStrategy extends Strategy { const code = formDataTotp ?? this._getMagicLinkCode(request) if (code) { if (!sessionEmail || !sessionTotp) throw new Error(ERRORS.EXPIRED_TOTP) - this._validateTOTP({ code, sessionTotp: sessionTotp as TOTPData }) + await this._validateTOTP({ code, sessionTotp: sessionTotp as TOTPData }) // Allow developer to handle user validation. const user = await this.verify({ @@ -498,6 +501,7 @@ export class TOTPStrategy extends Strategy { form: formData, request, }) + console.log('authenticate: user', user) session.set(options.sessionKey, user) session.unset(this.sessionEmailKey) @@ -735,6 +739,7 @@ export class TOTPStrategy extends Strategy { formData: FormData options: AuthenticateOptions }) { + console.log('_generateAndSendTOTP:', { email }) if (!options.successRedirect) throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) await this.validateEmail(email) @@ -807,6 +812,7 @@ export class TOTPStrategy extends Strategy { code: string sessionTotp: TOTPData }) { + console.log('_validateTOTP:', { code, sessionTotp }) // Decryption and Verification. const { ...totp } = (await verifyJWT({ jwt: sessionTotp.hash, diff --git a/test/index.spec.ts b/test/index.spec.ts index 82b119f..7458888 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -106,7 +106,7 @@ describe('[ Basics ]', () => { }) describe('[ TOTP ]', () => { - describe('Generate/Send TOTP', () => { + describe.only('Generate/Send TOTP', () => { test.skip('Should throw an Error on missing formData email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() @@ -325,7 +325,82 @@ describe('[ TOTP ]', () => { }) }) - describe.skip('Validate TOTP', () => { + describe('Validate TOTP', () => { + async function setupGenerateSendTOTP( + totpStrategyOptions: Partial = {}, + ) { + const user = { name: 'Joe Schmoe' } + let sendTOTPOptions: SendTOTPOptions | undefined + let session: Session | undefined + + const strategy = new TOTPStrategy( + { + secret: SECRET_ENV, + createTOTP, + readTOTP, + updateTOTP, + sendTOTP: async (options) => { + sendTOTPOptions = options + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) + }, + ...totpStrategyOptions, + }, + () => { + return Promise.resolve(user) + }, + ) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/verify') + session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + } else throw reason + }) + + expect(sendTOTPOptions).toBeDefined() + invariant(sendTOTPOptions, 'Undefined sendTOTPOptions') + expect(session).toBeDefined() + invariant(session, 'Undefined session') + return { strategy, sendTOTPOptions, session, user } + } + + test.only('Should successfully validate magic-link', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + expect(sendTOTPOptions.magicLink).toBeDefined() + invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') + const request = new Request(sendTOTPOptions.magicLink, { + method: 'GET', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + }) + .catch((reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/account`) + } else throw reason + }) + }) + async function setupFirstAuthPhase( totpStrategyOptions: Partial = {}, ) { @@ -405,7 +480,7 @@ describe('[ TOTP ]', () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() for (let i = 0; i < TOTP_GENERATION_DEFAULTS.maxAttempts + 1; i++) { const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code + i) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + i) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -431,7 +506,7 @@ describe('[ TOTP ]', () => { readTOTP, }) const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -454,7 +529,7 @@ describe('[ TOTP ]', () => { customErrors: { totpNotFound: CUSTOM_ERROR }, }) const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -476,7 +551,7 @@ describe('[ TOTP ]', () => { Promise.resolve({ hash: 'SIGNED-JWT', attempts: 0, active: false }), }) const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -498,7 +573,7 @@ describe('[ TOTP ]', () => { new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), ) const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -555,7 +630,7 @@ describe('[ TOTP ]', () => { test('Should successfully validate TOTP.', async () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -579,7 +654,7 @@ describe('[ TOTP ]', () => { test('Should contain user property in session.', async () => { const { strategy, session, sendTOTPOptions, user } = await setupFirstAuthPhase() const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -686,7 +761,7 @@ describe('[ TOTP ]', () => { ) { const formData = new FormData() - formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}`, { method: 'POST', From dd285ccad3ea0d8916dd52dd33e05077fd3ed034 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Tue, 2 Jan 2024 11:33:45 -0500 Subject: [PATCH 08/44] more validate tests --- test/index.spec.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/index.spec.ts b/test/index.spec.ts index 7458888..ae8beae 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -401,6 +401,67 @@ describe('[ TOTP ]', () => { }) }) + test.only('Should throw an Error on invalid magic-link code', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + expect(sendTOTPOptions.magicLink).toBeDefined() + invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') + const request = new Request(sendTOTPOptions.magicLink + "INVALID", { + method: 'GET', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + }) + await expect(() => + strategy.authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/', + }), + ).rejects.toThrow(ERRORS.INVALID_TOTP) + }) + + test.only('Should successfully validate totp code', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + }) + .catch((reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/account`) + } else throw reason + }) + }) + + test.only('Should throw an Error on invalid totp code', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + "INVALID") + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await expect(() => + strategy.authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/', + }), + ).rejects.toThrow(ERRORS.INVALID_TOTP) + }) + async function setupFirstAuthPhase( totpStrategyOptions: Partial = {}, ) { From c5c98bd78b9d42294c42986a5f53914ea438491c Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 01:19:11 -0500 Subject: [PATCH 09/44] invalid and max totp attempts --- src/constants.ts | 1 + src/index.ts | 154 ++++++++++++----------- src/utils.ts | 15 +++ test/index.spec.ts | 304 ++++++++++++++++++--------------------------- 4 files changed, 215 insertions(+), 259 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 5ff0e00..d27ee7d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,5 +26,6 @@ export const ERRORS = { INVALID_JWT: 'Invalid JWT.', INVALID_MAGIC_LINK_PATH: 'Invalid magic-link expected path.', REQUIRED_SUCCESS_REDIRECT_URL: 'Missing required successRedirect URL.', + REQUIRED_FAILURE_REDIRECT_URL: 'Missing required failureRedirect URL.', UNKNOWN_ERROR: 'Unknown error.', } as const diff --git a/src/index.ts b/src/index.ts index f19ef8f..0631b67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,8 @@ import { ensureStringOrUndefined, ensureObjectOrUndefined, ensureNonEmptyStringOrNull, + assertIsRequiredAuthenticateOptions, + RequiredAuthenticateOptions, } from './utils.js' import { STRATEGY_NAME, FORM_FIELDS, SESSION_KEYS, ERRORS } from './constants.js' @@ -458,7 +460,7 @@ export class TOTPStrategy extends Strategy { options: AuthenticateOptions, ): Promise { if (!this.secret) throw new Error(ERRORS.REQUIRED_ENV_SECRET) - if (!options.successRedirect) throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) + assertIsRequiredAuthenticateOptions(options) const session = await sessionStorage.getSession(request.headers.get('cookie')) const user: User | null = session.get(options.sessionKey) ?? null @@ -480,43 +482,63 @@ export class TOTPStrategy extends Strategy { sessionTotp, email, }) - if (email) { - await this._generateAndSendTOTP({ - email, - session, - sessionStorage, - request, - formData, - options, - }) - } - const code = formDataTotp ?? this._getMagicLinkCode(request) - if (code) { - if (!sessionEmail || !sessionTotp) throw new Error(ERRORS.EXPIRED_TOTP) - await this._validateTOTP({ code, sessionTotp: sessionTotp as TOTPData }) - - // Allow developer to handle user validation. - const user = await this.verify({ - email: sessionEmail, - form: formData, - request, - }) - console.log('authenticate: user', user) - - session.set(options.sessionKey, user) - session.unset(this.sessionEmailKey) - session.unset(this.sessionTotpKey) - session.unset(options.sessionErrorKey) - - throw redirect(options.successRedirect, { - headers: { - 'set-cookie': await sessionStorage.commitSession(session, { - maxAge: this.maxAge, - }), - }, - }) + try { + if (email) { + await this._generateAndSendTOTP({ + email, + session, + sessionStorage, + request, + formData, + options, + }) + } + const code = formDataTotp ?? this._getMagicLinkCode(request) + if (code) { + if (!sessionEmail || !sessionTotp) throw new Error(ERRORS.EXPIRED_TOTP) + await this._validateTOTP({ + code, + sessionTotp: sessionTotp as TOTPData, + session, + sessionStorage, + options, + }) + + // Allow developer to handle user validation. + const user = await this.verify({ + email: sessionEmail, + form: formData, + request, + }) + console.log('authenticate: user', user) + + session.set(options.sessionKey, user) + session.unset(this.sessionEmailKey) + session.unset(this.sessionTotpKey) + session.unset(options.sessionErrorKey) + + throw redirect(options.successRedirect, { + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, + }) + } + } catch (throwable) { + if (throwable instanceof Response) throw throwable + if (throwable instanceof Error) { + console.log('authenticate: error:', throwable.message) + return await this.failure( + throwable.message, + request, + sessionStorage, + options, + throwable, + ) + } + throw throwable } - throw new Error('Not implemented.') } @@ -808,11 +830,17 @@ export class TOTPStrategy extends Strategy { private async _validateTOTP({ code, sessionTotp, + session, + sessionStorage, + options, }: { code: string sessionTotp: TOTPData + session: Session + sessionStorage: SessionStorage + options: RequiredAuthenticateOptions }) { - console.log('_validateTOTP:', { code, sessionTotp }) + // console.log('_validateTOTP:', { code, sessionTotp }) // Decryption and Verification. const { ...totp } = (await verifyJWT({ jwt: sessionTotp.hash, @@ -822,43 +850,19 @@ export class TOTPStrategy extends Strategy { // Verify TOTP (@epic-web/totp). const isValid = verifyTOTP({ ...totp, otp: code }) if (!isValid) { - // await this.updateTOTP(sessionTotp, { attempts: dbTOTP.attempts + 1 }, expiresAt) - throw new Error(this.customErrors.invalidTotp) - } - } - - private async _validateTOTPDeprecated( - sessionTotp: string, - otp: string, - expiresAt: Date, - ) { - // Retrieve encrypted TOTP from database. - const dbTOTP = await this.readTOTP(sessionTotp) - if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) - - if (dbTOTP.active !== true) { - throw new Error(this.customErrors.inactiveTotp) - } - - const maxAttempts = - this.totpGeneration.maxAttempts ?? this._totpGenerationDefaults.maxAttempts - - if (dbTOTP.attempts >= maxAttempts) { - await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - throw new Error(this.customErrors.inactiveTotp) - } - - // Decryption and Verification. - const { ...totp } = (await verifyJWT({ - jwt: sessionTotp, - secretKey: this.secret, - })) as Required - - // Verify TOTP (@epic-web/totp). - const isValid = verifyTOTP({ ...totp, otp }) - if (!isValid) { - await this.updateTOTP(sessionTotp, { attempts: dbTOTP.attempts + 1 }, expiresAt) - throw new Error(this.customErrors.invalidTotp) + sessionTotp.attempts += 1 + const maxAttempts = + this.totpGeneration.maxAttempts ?? this._totpGenerationDefaults.maxAttempts + if (sessionTotp.attempts >= maxAttempts) { + session.unset(this.sessionTotpKey) + } + else { + session.set(this.sessionTotpKey, sessionTotp) + } + session.flash(options.sessionErrorKey, { message: this.customErrors.invalidTotp }) + throw redirect(options.failureRedirect, { + headers: { 'set-cookie': await sessionStorage.commitSession(session) }, + }) } } } diff --git a/src/utils.ts b/src/utils.ts index 2b912a6..ccddae7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,7 @@ import { ERRORS } from './constants.js' // @ts-expect-error - `thirty-two` is not typed. import * as base32 from 'thirty-two' import * as crypto from 'crypto' +import { AuthenticateOptions } from 'remix-auth' /** * TOTP Generation. @@ -98,3 +99,17 @@ export function ensureObjectOrUndefined(value: unknown) { } return value } + +export type RequiredAuthenticateOptions = Required< + Pick +> & + Omit + +export function assertIsRequiredAuthenticateOptions(options: AuthenticateOptions): asserts options is RequiredAuthenticateOptions { + if (options.successRedirect === undefined) { + throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) + } + if (options.failureRedirect === undefined) { + throw new Error(ERRORS.REQUIRED_FAILURE_REDIRECT_URL) + } +} diff --git a/test/index.spec.ts b/test/index.spec.ts index ae8beae..43b4e42 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -61,7 +61,7 @@ describe('[ Basics ]', () => { { createTOTP, readTOTP, updateTOTP, sendTOTP }, verify, ) - const request = new Request(`${HOST_URL}`, { + const request = new Request(`${HOST_URL}/login`, { method: 'POST', }) await expect(() => @@ -71,7 +71,7 @@ describe('[ Basics ]', () => { test('Should throw an Error on missing required successRedirect option.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const request = new Request(`${HOST_URL}`, { + const request = new Request(`${HOST_URL}/login`, { method: 'POST', }) await expect(() => @@ -79,64 +79,48 @@ describe('[ Basics ]', () => { ).rejects.toThrow(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) }) - test.skip('Should throw a custom Error message.', async () => { - const CUSTOM_ERROR = 'Custom error message.' - const strategy = new TOTPStrategy( - { - ...TOTP_STRATEGY_OPTIONS, - customErrors: { - requiredEmail: CUSTOM_ERROR, - }, - }, - verify, - ) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, '') - const request = new Request(`${HOST_URL}`, { + test('Should throw an Error on missing required failureRedirect option.', async () => { + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) + const request = new Request(`${HOST_URL}/login`, { method: 'POST', - body: formData, }) await expect(() => strategy.authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', + successRedirect: '/verify', }), - ).rejects.toThrow(CUSTOM_ERROR) + ).rejects.toThrow(ERRORS.REQUIRED_FAILURE_REDIRECT_URL) }) }) describe('[ TOTP ]', () => { - describe.only('Generate/Send TOTP', () => { - test.skip('Should throw an Error on missing formData email.', async () => { - const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, '') - const request = new Request(`${HOST_URL}`, { - method: 'POST', - body: formData, - }) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrow(ERRORS.REQUIRED_EMAIL) - }) - - test('Should throw an Error on invalid form email.', async () => { + describe('Generate/Send TOTP', () => { + test('Should failure redirect on invalid email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, '@invalid-email') - const request = new Request(`${HOST_URL}`, { + const request = new Request(`${HOST_URL}/login`, { method: 'POST', body: formData, }) - await expect(() => - strategy.authenticate(request, sessionStorage, { + await strategy + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrow(ERRORS.INVALID_EMAIL) + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/login') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_EMAIL, + }) + } else throw reason + }) }) test('Should generate/send TOTP for form email.', async () => { @@ -155,11 +139,12 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toMatch('/verify') + expect(reason.headers.get('location')).toBe('/verify') const session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) @@ -188,6 +173,7 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { @@ -218,11 +204,12 @@ describe('[ TOTP ]', () => { .authenticate(requestToPopulateSessionEmail, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toMatch('/verify') + expect(reason.headers.get('location')).toBe('/verify') session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) @@ -249,11 +236,12 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toMatch('/verify') + expect(reason.headers.get('location')).toBe('/verify') const session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) @@ -283,11 +271,12 @@ describe('[ TOTP ]', () => { .authenticate(requestToPopulateSessionEmail, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toMatch('/verify') + expect(reason.headers.get('location')).toBe('/verify') session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) @@ -308,11 +297,12 @@ describe('[ TOTP ]', () => { .authenticate(emptyFormRequest, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toMatch('/verify') + expect(reason.headers.get('location')).toBe('/verify') const session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) @@ -360,6 +350,7 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/verify', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { @@ -378,7 +369,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test.only('Should successfully validate magic-link', async () => { + test('Should successfully validate magic-link', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') @@ -392,6 +383,7 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/account', + failureRedirect: '/login', }) .catch((reason) => { if (reason instanceof Response) { @@ -401,26 +393,38 @@ describe('[ TOTP ]', () => { }) }) - test.only('Should throw an Error on invalid magic-link code', async () => { + test('Should failure redirect on invalid magic-link code.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') - const request = new Request(sendTOTPOptions.magicLink + "INVALID", { + const request = new Request(sendTOTPOptions.magicLink + 'INVALID', { method: 'GET', headers: { cookie: await sessionStorage.commitSession(session), }, }) - await expect(() => - strategy.authenticate(request, sessionStorage, { + await strategy + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrow(ERRORS.INVALID_TOTP) + successRedirect: '/account', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/login`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_TOTP, + }) + } else throw reason + }) }) - test.only('Should successfully validate totp code', async () => { - const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + test('Should successfully validate totp code', async () => { + const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { @@ -434,19 +438,24 @@ describe('[ TOTP ]', () => { .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/account', + failureRedirect: '/login', }) - .catch((reason) => { + .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) expect(reason.headers.get('location')).toBe(`/account`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get('user')).toEqual(user) } else throw reason }) }) - test.only('Should throw an Error on invalid totp code', async () => { + test('Should failure redirect on invalid totp code', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + "INVALID") + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -454,12 +463,57 @@ describe('[ TOTP ]', () => { }, body: formData, }) - await expect(() => - strategy.authenticate(request, sessionStorage, { + await strategy + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrow(ERRORS.INVALID_TOTP) + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_TOTP, + }) + } else throw reason + }) + }) + + test.only('Should failure redirect on invalid and max TOTP attempts.', async () => { + let { strategy, session, sendTOTPOptions } = await setupGenerateSendTOTP() + for (let i = 0; i <= TOTP_GENERATION_DEFAULTS.maxAttempts; i++) { + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: i < TOTP_GENERATION_DEFAULTS.maxAttempts ? ERRORS.INVALID_TOTP : ERRORS.EXPIRED_TOTP, + }) + } else throw reason + }) + } }) async function setupFirstAuthPhase( @@ -562,72 +616,6 @@ describe('[ TOTP ]', () => { } }) - test('Should throw an Error on missing TOTP from database.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase({ - readTOTP, - }) - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }), - ).rejects.toThrowError(ERRORS.TOTP_NOT_FOUND) - }) - - test('Should throw a custom Error message on missing TOTP from database.', async () => { - const CUSTOM_ERROR = 'Custom error message.' - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase({ - readTOTP, - customErrors: { totpNotFound: CUSTOM_ERROR }, - }) - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }), - ).rejects.toThrowError(CUSTOM_ERROR) - }) - - test('Should throw an Error on inactive TOTP.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase({ - readTOTP: () => - Promise.resolve({ hash: 'SIGNED-JWT', attempts: 0, active: false }), - }) - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }), - ).rejects.toThrowError(ERRORS.INACTIVE_TOTP) - }) - test('Should throw an Error on expired TOTP verification.', async () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() vi.setSystemTime( @@ -687,58 +675,6 @@ describe('[ TOTP ]', () => { }), ).rejects.toThrow(ERRORS.INVALID_MAGIC_LINK_PATH) }) - - test('Should successfully validate TOTP.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }) - .catch((reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe(`/account`) - } else throw reason - }) - }) - - test('Should contain user property in session.', async () => { - const { strategy, session, sendTOTPOptions, user } = await setupFirstAuthPhase() - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }) - .catch(async (reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - const session = await sessionStorage.getSession( - reason.headers.get('set-cookie') ?? '', - ) - expect(session.has('user')).toBeTruthy() - expect(session.get('user')).toEqual(user) - } else throw reason - }) - }) }) describe.skip('End to End', () => { From e5511a7943868ef39bd667f9ed085b836255c2b3 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 12:03:42 -0500 Subject: [PATCH 10/44] jwt expired --- src/index.ts | 47 ++++++++++++++++++------------ src/utils.ts | 40 +++++++++++--------------- test/index.spec.ts | 72 +++++++++++++++++++++++++++------------------- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0631b67..7e8ee0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import type { Session, SessionStorage } from '@remix-run/server-runtime' import type { AuthenticateOptions, StrategyVerifyCallback } from 'remix-auth' +import { errors } from 'jose' import { redirect } from '@remix-run/server-runtime' import { Strategy } from 'remix-auth' @@ -368,7 +369,7 @@ export class TOTPStrategy extends Strategy { private readonly updateTOTP: UpdateTOTP private readonly sendTOTP: SendTOTP private readonly validateEmail: ValidateEmail - private readonly customErrors: CustomErrorsOptions + private readonly customErrors: Required private readonly emailFieldKey: string private readonly totpFieldKey: string private readonly sessionEmailKey: string @@ -840,26 +841,34 @@ export class TOTPStrategy extends Strategy { sessionStorage: SessionStorage options: RequiredAuthenticateOptions }) { - // console.log('_validateTOTP:', { code, sessionTotp }) - // Decryption and Verification. - const { ...totp } = (await verifyJWT({ - jwt: sessionTotp.hash, - secretKey: this.secret, - })) as Required - - // Verify TOTP (@epic-web/totp). - const isValid = verifyTOTP({ ...totp, otp: code }) - if (!isValid) { - sessionTotp.attempts += 1 - const maxAttempts = - this.totpGeneration.maxAttempts ?? this._totpGenerationDefaults.maxAttempts - if (sessionTotp.attempts >= maxAttempts) { + try { + console.log('_validateTOTP:', { code, sessionTotp }) + // Decryption and Verification. + const { ...totp } = (await verifyJWT({ + jwt: sessionTotp.hash, + secretKey: this.secret, + })) as Required + console.log('_validateTOTP: totp:', totp) + + // Verify TOTP (@epic-web/totp). + if (!verifyTOTP({ ...totp, otp: code })) + throw new Error(this.customErrors.invalidTotp) + } catch (error) { + console.error('_validateTOTP: error:', error) + if (error instanceof errors.JWTExpired) { session.unset(this.sessionTotpKey) + session.flash(options.sessionErrorKey, { message: this.customErrors.expiredTotp }) + } else { + sessionTotp.attempts += 1 + const maxAttempts = + this.totpGeneration.maxAttempts ?? this._totpGenerationDefaults.maxAttempts + if (sessionTotp.attempts >= maxAttempts) { + session.unset(this.sessionTotpKey) + } else { + session.set(this.sessionTotpKey, sessionTotp) + } + session.flash(options.sessionErrorKey, { message: this.customErrors.invalidTotp }) } - else { - session.set(this.sessionTotpKey, sessionTotp) - } - session.flash(options.sessionErrorKey, { message: this.customErrors.invalidTotp }) throw redirect(options.failureRedirect, { headers: { 'set-cookie': await sessionStorage.commitSession(session) }, }) diff --git a/src/utils.ts b/src/utils.ts index ccddae7..daf8a5a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,21 +46,17 @@ type SignJWTOptions = { } export async function signJWT({ payload, expiresIn, secretKey }: SignJWTOptions) { - try { - const algorithm = 'HS256' - const secret = new TextEncoder().encode(secretKey) - const expires = new Date(Date.now() + expiresIn * 1000) - - const token = await new SignJWT(payload) - .setProtectedHeader({ alg: algorithm }) - .setExpirationTime(expires) - .setIssuedAt() - .sign(secret) - - return token - } catch (err: unknown) { - throw new Error(ERRORS.INVALID_JWT) - } + const algorithm = 'HS256' + const secret = new TextEncoder().encode(secretKey) + const expires = new Date(Date.now() + expiresIn * 1000) + + const token = await new SignJWT(payload) + .setProtectedHeader({ alg: algorithm }) + .setExpirationTime(expires) + .setIssuedAt() + .sign(secret) + + return token } type VerifyJWTOptions = { @@ -69,13 +65,9 @@ type VerifyJWTOptions = { } export async function verifyJWT({ jwt, secretKey }: VerifyJWTOptions) { - try { - const secret = new TextEncoder().encode(secretKey) - const { payload } = await jwtVerify(jwt, secret) - return payload - } catch (err: unknown) { - throw new Error(ERRORS.INVALID_JWT) - } + const secret = new TextEncoder().encode(secretKey) + const { payload } = await jwtVerify(jwt, secret) + return payload } /** @@ -105,7 +97,9 @@ export type RequiredAuthenticateOptions = Required< > & Omit -export function assertIsRequiredAuthenticateOptions(options: AuthenticateOptions): asserts options is RequiredAuthenticateOptions { +export function assertIsRequiredAuthenticateOptions( + options: AuthenticateOptions, +): asserts options is RequiredAuthenticateOptions { if (options.successRedirect === undefined) { throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) } diff --git a/test/index.spec.ts b/test/index.spec.ts index 43b4e42..8d19d63 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -369,7 +369,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test('Should successfully validate magic-link', async () => { + test.only('Should successfully validate magic-link', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') @@ -393,7 +393,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on invalid magic-link code.', async () => { + test.only('Should failure redirect on invalid magic-link code.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') @@ -423,7 +423,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should successfully validate totp code', async () => { + test.only('Should successfully validate totp code', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) @@ -452,7 +452,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on invalid totp code', async () => { + test.only('Should failure redirect on invalid totp code', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') @@ -509,13 +509,50 @@ describe('[ TOTP ]', () => { reason.headers.get('set-cookie') ?? '', ) expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ - message: i < TOTP_GENERATION_DEFAULTS.maxAttempts ? ERRORS.INVALID_TOTP : ERRORS.EXPIRED_TOTP, + message: + i < TOTP_GENERATION_DEFAULTS.maxAttempts + ? ERRORS.INVALID_TOTP + : ERRORS.EXPIRED_TOTP, }) } else throw reason }) } }) + test.only('Should failure redirect on expired totp code', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + vi.setSystemTime( + new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), + ) + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.EXPIRED_TOTP, + }) + } else throw reason + }) + }) + async function setupFirstAuthPhase( totpStrategyOptions: Partial = {}, ) { @@ -591,31 +628,6 @@ describe('[ TOTP ]', () => { return { strategy, totpData, totpDataExpiresAt, sendTOTPOptions, session, user } } - test('Should throw an Error on invalid and max TOTP attempts.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() - for (let i = 0; i < TOTP_GENERATION_DEFAULTS.maxAttempts + 1; i++) { - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + i) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, - }) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrowError( - i < TOTP_GENERATION_DEFAULTS.maxAttempts - ? ERRORS.INVALID_TOTP - : ERRORS.INACTIVE_TOTP, - ) - } - }) - test('Should throw an Error on expired TOTP verification.', async () => { const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() vi.setSystemTime( From 7865a29e7071e59b545a745b8d725de25520a25f Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 12:45:27 -0500 Subject: [PATCH 11/44] cleanup --- src/index.ts | 204 ---------------------- test/index.spec.ts | 425 +++++++++++++-------------------------------- 2 files changed, 125 insertions(+), 504 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7e8ee0b..675af63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -543,210 +543,6 @@ export class TOTPStrategy extends Strategy { throw new Error('Not implemented.') } - // try { - // if (!user) { - // /** - // * 1st Authentication Phase. - // */ - // if (isPOST) { - // formData = await request.formData() - // const form = Object.fromEntries(formData) - - // formDataEmail = form[this.emailFieldKey] && String(form[this.emailFieldKey]) - // formDataTotp = form[this.totpFieldKey] && String(form[this.totpFieldKey]) - - // /** - // * Re-send TOTP - User has requested a new TOTP. - // * This will invalidate previous TOTP and assign session email to form email. - // */ - // if ( - // !formDataEmail && - // !formDataTotp && - // sessionEmail && - // sessionTotp && - // sessionTotpExpiresAt - // ) { - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // formDataEmail = sessionEmail - // } - - // /** - // * Invalidate previous TOTP - User has submitted a new email address. - // */ - // if ( - // formDataEmail && - // sessionEmail && - // formDataEmail !== sessionEmail && - // sessionTotp && - // sessionTotpExpiresAt - // ) { - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // } - - // /** - // * First TOTP request. - // */ - // if (!formDataTotp) { - // if (!formDataEmail) throw new Error(this.customErrors.requiredEmail) - // await this.validateEmail(formDataEmail) - - // // Generate, Sign and create Magic Link. - // const { otp: _otp, ...totp } = generateTOTP({ - // ...this.totpGeneration, - // secret: generateSecret(), - // }) - // const signedTotp = await signJWT({ - // payload: totp, - // expiresIn: - // this.totpGeneration.period ?? this._totpGenerationDefaults.period, - // secretKey: this.secret, - // }) - // const magicLink = generateMagicLink({ - // ...this.magicLinkGeneration, - // param: this.totpFieldKey, - // code: _otp, - // request, - // }) - - // // Create TOTP in application storage. (Milliseconds since Unix epoch). - // const expiresAtEpochMs = - // Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 - // const expiresAt = new Date(expiresAtEpochMs) - - // await this.createTOTP( - // { hash: signedTotp, active: true, attempts: 0 }, - // expiresAt, - // ) - - // // Send TOTP. - // await this.sendTOTP({ - // email: formDataEmail, - // code: _otp, - // magicLink, - // form: formData, - // request, - // }) - - // session.set(this.sessionEmailKey, formDataEmail) - // session.set(this.sessionTotpKey, signedTotp) - // session.set(this.sessionTotpExpiresAtKey, expiresAt.toISOString()) - // session.unset(options.sessionErrorKey) - - // throw redirect(options.successRedirect, { - // headers: { - // 'set-cookie': await sessionStorage.commitSession(session, { - // maxAge: this.maxAge, - // }), - // }, - // }) - // } - // } - - // /** - // * 2nd Authentication Phase. - // * Either via form submission or magic-link URL. - // */ - // if (isGET && this.magicLinkGeneration.enabled) { - // const url = new URL(request.url) - - // if (url.pathname !== this.magicLinkGeneration.callbackPath) { - // throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) - // } - - // magicLinkTotp = url.searchParams.has(this.totpFieldKey) - // ? decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') - // : undefined - // } - - // if ((isPOST && formDataTotp) || (isGET && magicLinkTotp)) { - // // Validation. - // if (!sessionEmail || !sessionTotp || !sessionTotpExpiresAt) { - // throw new Error(this.customErrors.inactiveTotp) - // } - - // const expiresAt = new Date(sessionTotpExpiresAt) - - // if (isPOST && formDataTotp) { - // await this._validateTOTP(sessionTotp, formDataTotp, expiresAt) - // } - // if (isGET && magicLinkTotp) { - // await this._validateTOTP(sessionTotp, magicLinkTotp, expiresAt) - // } - - // // Invalidation. - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - - // // Allow developer to handle user validation. - // user = await this.verify({ - // email: sessionEmail, - // form: formData, - // magicLink: magicLinkTotp, - // request, - // }) - - // session.set(options.sessionKey, user) - // session.unset(this.sessionEmailKey) - // session.unset(this.sessionTotpKey) - // session.unset(this.sessionTotpExpiresAtKey) - // session.unset(options.sessionErrorKey) - - // throw redirect(options.successRedirect, { - // headers: { - // 'set-cookie': await sessionStorage.commitSession(session, { - // maxAge: this.maxAge, - // }), - // }, - // }) - // } - // } - // } catch (error) { - // // Allow Response to pass-through. - // if (error instanceof Response && error.status === 302) throw error - // if (error instanceof Error) { - // if (error.message === ERRORS.INVALID_JWT) { - // if (sessionTotp && sessionTotpExpiresAt) { - // const dbTOTP = await this.readTOTP(sessionTotp) - // if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound) - - // const expiresAt = new Date(sessionTotpExpiresAt) - // await this.updateTOTP(sessionTotp, { active: false }, expiresAt) - // } - // return await this.failure( - // this.customErrors.inactiveTotp || ERRORS.INACTIVE_TOTP, - // request, - // sessionStorage, - // options, - // error, - // ) - // } - - // return await this.failure(error.message, request, sessionStorage, options, error) - // } - // if (typeof error === 'string') { - // return await this.failure( - // error, - // request, - // sessionStorage, - // options, - // new Error(error), - // ) - // } - // return await this.failure( - // ERRORS.UNKNOWN_ERROR, - // request, - // sessionStorage, - // options, - // new Error(JSON.stringify(error, null, 2)), - // ) - // } - - // if (!user) throw new Error(ERRORS.USER_NOT_FOUND) - - // return this.success(user, request, sessionStorage, options) - // } - private async _generateAndSendTOTP({ email, session, diff --git a/test/index.spec.ts b/test/index.spec.ts index 8d19d63..0c61453 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -95,34 +95,6 @@ describe('[ Basics ]', () => { describe('[ TOTP ]', () => { describe('Generate/Send TOTP', () => { - test('Should failure redirect on invalid email.', async () => { - const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, '@invalid-email') - const request = new Request(`${HOST_URL}/login`, { - method: 'POST', - body: formData, - }) - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/verify', - failureRedirect: '/login', - }) - .catch(async (reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe('/login') - const session = await sessionStorage.getSession( - reason.headers.get('set-cookie') ?? '', - ) - expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ - message: ERRORS.INVALID_EMAIL, - }) - } else throw reason - }) - }) - test('Should generate/send TOTP for form email.', async () => { sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { expect(options.email).toBe(DEFAULT_EMAIL) @@ -313,6 +285,34 @@ describe('[ TOTP ]', () => { }) expect(sendTOTP).toHaveBeenCalledTimes(2) }) + + test('Should failure redirect on invalid email.', async () => { + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, '@invalid-email') + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/login') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_EMAIL, + }) + } else throw reason + }) + }) }) describe('Validate TOTP', () => { @@ -323,20 +323,16 @@ describe('[ TOTP ]', () => { let sendTOTPOptions: SendTOTPOptions | undefined let session: Session | undefined + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + sendTOTPOptions = options + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) + }) + const strategy = new TOTPStrategy( - { - secret: SECRET_ENV, - createTOTP, - readTOTP, - updateTOTP, - sendTOTP: async (options) => { - sendTOTPOptions = options - expect(options.email).toBe(DEFAULT_EMAIL) - expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) - }, - ...totpStrategyOptions, - }, - () => { + { ...TOTP_STRATEGY_OPTIONS, ...totpStrategyOptions }, + async () => { return Promise.resolve(user) }, ) @@ -359,9 +355,12 @@ describe('[ TOTP ]', () => { session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() } else throw reason }) + expect(sendTOTP).toHaveBeenCalledTimes(1) expect(sendTOTPOptions).toBeDefined() invariant(sendTOTPOptions, 'Undefined sendTOTPOptions') expect(session).toBeDefined() @@ -369,61 +368,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test.only('Should successfully validate magic-link', async () => { - const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() - expect(sendTOTPOptions.magicLink).toBeDefined() - invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') - const request = new Request(sendTOTPOptions.magicLink, { - method: 'GET', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - }) - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - failureRedirect: '/login', - }) - .catch((reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe(`/account`) - } else throw reason - }) - }) - - test.only('Should failure redirect on invalid magic-link code.', async () => { - const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() - expect(sendTOTPOptions.magicLink).toBeDefined() - invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') - const request = new Request(sendTOTPOptions.magicLink + 'INVALID', { - method: 'GET', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - }) - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - failureRedirect: '/login', - }) - .catch(async (reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe(`/login`) - const session = await sessionStorage.getSession( - reason.headers.get('set-cookie') ?? '', - ) - expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ - message: ERRORS.INVALID_TOTP, - }) - } else throw reason - }) - }) - - test.only('Should successfully validate totp code', async () => { + test('Should successfully validate totp code', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) @@ -448,11 +393,13 @@ describe('[ TOTP ]', () => { reason.headers.get('set-cookie') ?? '', ) expect(session.get('user')).toEqual(user) + expect(session.get(SESSION_KEYS.EMAIL)).not.toBeDefined() + expect(session.get(SESSION_KEYS.TOTP)).not.toBeDefined() } else throw reason }) }) - test.only('Should failure redirect on invalid totp code', async () => { + test('Should failure redirect on invalid totp code', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') @@ -483,7 +430,7 @@ describe('[ TOTP ]', () => { }) }) - test.only('Should failure redirect on invalid and max TOTP attempts.', async () => { + test('Should failure redirect on invalid and max TOTP attempts.', async () => { let { strategy, session, sendTOTPOptions } = await setupGenerateSendTOTP() for (let i = 0; i <= TOTP_GENERATION_DEFAULTS.maxAttempts; i++) { const formData = new FormData() @@ -519,7 +466,7 @@ describe('[ TOTP ]', () => { } }) - test.only('Should failure redirect on expired totp code', async () => { + test('Should failure redirect on expired totp code', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() vi.setSystemTime( new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), @@ -553,105 +500,69 @@ describe('[ TOTP ]', () => { }) }) - async function setupFirstAuthPhase( - totpStrategyOptions: Partial = {}, - ) { - const user = { name: 'Joe Schmoe' } - let totpData: TOTPDataDeprecated | undefined - let totpDataExpiresAt: Date | undefined - let sendTOTPOptions: SendTOTPOptions | undefined - let session: Session | undefined - const strategy = new TOTPStrategy( - { - secret: SECRET_ENV, - createTOTP: async (data, expiresAt) => { - expect(totpData).not.toBeDefined() - expect(data.active).toBeTruthy() - expect(data.attempts).toBe(0) - totpData = data - totpDataExpiresAt = expiresAt - }, - readTOTP: async (hash) => { - expect(totpData).toBeDefined() - invariant(totpData, 'Undefined totpData') - expect(totpData.hash).toBe(hash) - return totpData - }, - updateTOTP: async (hash, data, expiresAt) => { - expect(totpData).toBeDefined() - invariant(totpData, 'Undefined totpData') - expect(totpData.hash).toBe(hash) - expect(totpDataExpiresAt).toEqual(expiresAt) - totpData = { ...totpData, ...data } - }, - sendTOTP: async (options) => { - sendTOTPOptions = options - expect(options.email).toBe(DEFAULT_EMAIL) - expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) - }, - ...totpStrategyOptions, - }, - () => { - return Promise.resolve(user) + test('Should successfully validate magic-link', async () => { + const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() + expect(sendTOTPOptions.magicLink).toBeDefined() + invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') + const request = new Request(sendTOTPOptions.magicLink, { + method: 'GET', + headers: { + cookie: await sessionStorage.commitSession(session), }, - ) - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - const request = new Request(`${HOST_URL}/login`, { - method: 'POST', - body: formData, }) await strategy .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/verify', + successRedirect: '/account', + failureRedirect: '/login', }) .catch(async (reason) => { if (reason instanceof Response) { expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe('/verify') - session = await sessionStorage.getSession( + expect(reason.headers.get('location')).toBe(`/account`) + const session = await sessionStorage.getSession( reason.headers.get('set-cookie') ?? '', ) + expect(session.get('user')).toEqual(user) + expect(session.get('user')).toEqual(user) + expect(session.get(SESSION_KEYS.EMAIL)).not.toBeDefined() + expect(session.get(SESSION_KEYS.TOTP)).not.toBeDefined() } else throw reason }) + }) - expect(totpData).toBeDefined() - invariant(totpData, 'Undefined totpData') - expect(totpData.active).toBeTruthy() - expect(totpDataExpiresAt).toBeDefined() - invariant(totpDataExpiresAt, 'Undefined totpDataExpiresAt') - expect(sendTOTPOptions).toBeDefined() - invariant(sendTOTPOptions, 'Undefined sendTOTPOptions') - expect(session).toBeDefined() - invariant(session, 'Undefined session') - return { strategy, totpData, totpDataExpiresAt, sendTOTPOptions, session, user } - } - - test('Should throw an Error on expired TOTP verification.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() - vi.setSystemTime( - new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), - ) - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - const request = new Request(`${HOST_URL}/verify`, { - method: 'POST', + test('Should failure redirect on invalid magic-link code.', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() + expect(sendTOTPOptions.magicLink).toBeDefined() + invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') + const request = new Request(sendTOTPOptions.magicLink + 'INVALID', { + method: 'GET', headers: { cookie: await sessionStorage.commitSession(session), }, - body: formData, }) - await expect(() => - strategy.authenticate(request, sessionStorage, { + await strategy + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, - successRedirect: '/', - }), - ).rejects.toThrow(ERRORS.INACTIVE_TOTP) + successRedirect: '/account', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/login`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_TOTP, + }) + } else throw reason + }) }) - test('Should throw an Error on expired magic-link TOTP verification.', async () => { - const { strategy, session, sendTOTPOptions } = await setupFirstAuthPhase() + test('Should failure redirect on expired magic-link', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') vi.setSystemTime( @@ -663,144 +574,58 @@ describe('[ TOTP ]', () => { cookie: await sessionStorage.commitSession(session), }, }) - await expect(() => - strategy.authenticate(request, sessionStorage, { + await strategy + .authenticate(request, sessionStorage, { ...AUTH_OPTIONS, successRedirect: '/account', - }), - ).rejects.toThrow(ERRORS.INACTIVE_TOTP) + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/login`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.EXPIRED_TOTP, + }) + } else throw reason + }) }) - test('Should throw an Error on invalid magic-link path.', async () => { - const { strategy, sendTOTPOptions } = await setupFirstAuthPhase() + test('Should failure redirect on invalid magic-link path.', async () => { + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') expect(sendTOTPOptions.magicLink).toMatch(/\/magic-link/) const request = new Request( sendTOTPOptions.magicLink.replace(/\/magic-link/, '/invalid-magic-link'), - { method: 'GET' }, - ) - await expect(() => - strategy.authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }), - ).rejects.toThrow(ERRORS.INVALID_MAGIC_LINK_PATH) - }) - }) - - describe.skip('End to End', () => { - test('Should authenticate user with valid TOTP.', async () => { - const user = { name: 'Joe Schmoe' } - let totpData: TOTPDataDeprecated | undefined - let totpDataExpiresAt: Date | undefined - let sendTOTPOptions: SendTOTPOptions | undefined - let session: Session | undefined - const strategy = new TOTPStrategy( { - secret: SECRET_ENV, - createTOTP: async (data, expiresAt) => { - expect(totpData).not.toBeDefined() - expect(data.active).toBeTruthy() - expect(data.attempts).toBe(0) - totpData = data - totpDataExpiresAt = expiresAt - }, - readTOTP: async (hash) => { - expect(totpData).toBeDefined() - invariant(totpData, 'TOTP data is undefined.') - expect(totpData.hash).toBe(hash) - return totpData - }, - updateTOTP: async (hash, data, expiresAt) => { - expect(totpData).toBeDefined() - invariant(totpData, 'TOTP data is undefined.') - expect(totpData.hash).toBe(hash) - expect(totpDataExpiresAt).toEqual(expiresAt) - totpData = { ...totpData, ...data } - }, - sendTOTP: async (options) => { - sendTOTPOptions = options - expect(options.email).toBe(DEFAULT_EMAIL) - expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) + method: 'GET', + headers: { + cookie: await sessionStorage.commitSession(session), }, }, - () => { - return Promise.resolve(user) - }, ) - { - const formData = new FormData() - formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - - const request = new Request(`${HOST_URL}`, { - method: 'POST', - body: formData, + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/login', }) - - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/verify', - }) - .catch(async (reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe('/verify') - session = await sessionStorage.getSession( - reason.headers.get('set-cookie') ?? '', - ) - } else throw reason - }) - } - expect(totpData).toBeDefined() - invariant(totpData, 'Undefined totpData') - expect(totpData.active).toBeTruthy() - expect(totpData.attempts).toBe(0) - expect(totpDataExpiresAt).toBeDefined() - invariant(totpDataExpiresAt, 'Undefined totpDataExpiresAt') - expect(sendTOTPOptions).toBeDefined() - invariant(sendTOTPOptions, 'Undefined sendTOTPOptions') - expect(session).toBeDefined() - invariant(session, 'Undefined session') - expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) - expect(session.get(SESSION_KEYS.TOTP)).toBe(totpData?.hash) - expect(session.get(SESSION_KEYS.TOTP_EXPIRES_AT)).toBe( - totpDataExpiresAt?.toISOString(), - ) - { - const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) - - const request = new Request(`${HOST_URL}`, { - method: 'POST', - headers: { - cookie: await sessionStorage.commitSession(session), - }, - body: formData, + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/login`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.INVALID_MAGIC_LINK_PATH, + }) + } else throw reason }) - - await strategy - .authenticate(request, sessionStorage, { - ...AUTH_OPTIONS, - successRedirect: '/account', - }) - .catch(async (reason) => { - if (reason instanceof Response) { - expect(reason.status).toBe(302) - expect(reason.headers.get('location')).toBe('/account') - session = await sessionStorage.getSession( - reason.headers.get('set-cookie') ?? '', - ) - } else throw reason - }) - } - expect(totpData).toBeDefined() - expect(totpData.active).toBeFalsy() - expect(totpData.attempts).toBe(0) - expect(session).toBeDefined() - invariant(session, 'Undefined session') - expect(session.get('user')).toEqual(user) }) }) }) From 978eec8ce4ad96bf364072e1d60983d6fff155c1 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 17:11:05 -0500 Subject: [PATCH 12/44] remove crud --- src/constants.ts | 1 - src/index.ts | 60 ++++------------------------------------------ test/index.spec.ts | 8 +------ 3 files changed, 6 insertions(+), 63 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index d27ee7d..607b427 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -23,7 +23,6 @@ export const ERRORS = { // Miscellaneous errors. REQUIRED_ENV_SECRET: 'Missing required .env secret.', USER_NOT_FOUND: 'User not found.', - INVALID_JWT: 'Invalid JWT.', INVALID_MAGIC_LINK_PATH: 'Invalid magic-link expected path.', REQUIRED_SUCCESS_REDIRECT_URL: 'Missing required successRedirect URL.', REQUIRED_FAILURE_REDIRECT_URL: 'Missing required failureRedirect URL.', diff --git a/src/index.ts b/src/index.ts index 675af63..75ed237 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,39 +124,6 @@ export interface MagicLinkGenerationOptions { callbackPath?: string } -/** - * The create TOTP CRUD method. - * - * @param data The TOTP data. - * @param expiresAt The TOTP expiration date. - */ -export interface CreateTOTP { - (data: TOTPDataDeprecated, expiresAt: Date): Promise -} - -/** - * The read TOTP CRUD method. - * @param hash The hash of the TOTP. - */ -export interface ReadTOTP { - (hash: string): Promise -} - -/** - * The update TOTP CRUD method. - * - * @param hash The hash of the TOTP. - * @param data The TOTP data to be updated. - * @param expiresAt The TOTP expiration date. It is always the same as the expiration passed into createTOTP(). - */ -export interface UpdateTOTP { - ( - hash: string, - data: Partial>, - expiresAt: Date, - ): Promise -} - /** * The send TOTP configuration. */ @@ -265,21 +232,6 @@ export interface TOTPStrategyOptions { */ magicLinkGeneration?: MagicLinkGenerationOptions - /** - * The create TOTP method. - */ - createTOTP: CreateTOTP - - /** - * The read TOTP method. - */ - readTOTP: ReadTOTP - - /** - * The update TOTP method. - */ - updateTOTP: UpdateTOTP - /** * The send TOTP method. */ @@ -364,9 +316,6 @@ export class TOTPStrategy extends Strategy { private readonly maxAge: number | undefined private readonly totpGeneration: TOTPGenerationOptions private readonly magicLinkGeneration: MagicLinkGenerationOptions - private readonly createTOTP: CreateTOTP - private readonly readTOTP: ReadTOTP - private readonly updateTOTP: UpdateTOTP private readonly sendTOTP: SendTOTP private readonly validateEmail: ValidateEmail private readonly customErrors: Required @@ -404,9 +353,6 @@ export class TOTPStrategy extends Strategy { super(verify) this.secret = options.secret this.maxAge = options.maxAge ?? undefined - this.createTOTP = options.createTOTP - this.readTOTP = options.readTOTP - this.updateTOTP = options.updateTOTP this.sendTOTP = options.sendTOTP this.validateEmail = options.validateEmail ?? this._validateEmailDefaults this.emailFieldKey = options.emailFieldKey ?? FORM_FIELDS.EMAIL @@ -666,7 +612,11 @@ export class TOTPStrategy extends Strategy { session.flash(options.sessionErrorKey, { message: this.customErrors.invalidTotp }) } throw redirect(options.failureRedirect, { - headers: { 'set-cookie': await sessionStorage.commitSession(session) }, + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, }) } } diff --git a/test/index.spec.ts b/test/index.spec.ts index 0c61453..e427fb7 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -26,17 +26,11 @@ import { * Mocks. */ export const verify = vi.fn() -export const createTOTP = vi.fn() -export const readTOTP = vi.fn() -export const updateTOTP = vi.fn() export const sendTOTP = vi.fn() export const validateEmail = vi.fn() const TOTP_STRATEGY_OPTIONS: TOTPStrategyOptions = { secret: SECRET_ENV, - createTOTP, - readTOTP, - updateTOTP, sendTOTP, } @@ -58,7 +52,7 @@ describe('[ Basics ]', () => { test('Should throw an Error on missing required secret option.', async () => { const strategy = new TOTPStrategy( // @ts-expect-error - Error is expected since missing secret option. - { createTOTP, readTOTP, updateTOTP, sendTOTP }, + { sendTOTP }, verify, ) const request = new Request(`${HOST_URL}/login`, { From 9aa1c0a80a651eb09cfd4dcc57ee6988c3e1b931 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 17:25:04 -0500 Subject: [PATCH 13/44] remove unused constants --- src/constants.ts | 5 ---- src/index.ts | 69 +--------------------------------------------- test/index.spec.ts | 1 - 3 files changed, 1 insertion(+), 74 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 607b427..a6c0420 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,17 +8,13 @@ export const FORM_FIELDS = { export const SESSION_KEYS = { EMAIL: 'auth:email', TOTP: 'auth:totp', - TOTP_EXPIRES_AT: 'auth:totp:expiresAt', } as const export const ERRORS = { // Customizable errors. - REQUIRED_EMAIL: 'Email is required.', INVALID_EMAIL: 'Email is not valid.', INVALID_TOTP: 'Code is not valid.', EXPIRED_TOTP: 'Code has expired.', - INACTIVE_TOTP: 'Code is no longer active.', - TOTP_NOT_FOUND: 'Database TOTP not found.', // Miscellaneous errors. REQUIRED_ENV_SECRET: 'Missing required .env secret.', @@ -26,5 +22,4 @@ export const ERRORS = { INVALID_MAGIC_LINK_PATH: 'Invalid magic-link expected path.', REQUIRED_SUCCESS_REDIRECT_URL: 'Missing required successRedirect URL.', REQUIRED_FAILURE_REDIRECT_URL: 'Missing required failureRedirect URL.', - UNKNOWN_ERROR: 'Unknown error.', } as const diff --git a/src/index.ts b/src/index.ts index 75ed237..5149e43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,35 +28,6 @@ export interface TOTPData { */ hash: string - /** - * The expiration date for the TOTP secret. - * After this date, the TOTP will no longer be valid. - */ - expiresAt: Date - - /** - * The number of attempts the user tried to verify the TOTP. - * @default 0 - */ - attempts: number -} - -/** - * The TOTP data the application stores. - * Used in CRUD functions provided by the application. - */ -export interface TOTPDataDeprecated { - /** - * The encrypted TOTP. - */ - hash: string - - /** - * The status of the TOTP. - * @default true - */ - active: boolean - /** * The number of attempts the user tried to verify the TOTP. * @default 0 @@ -176,11 +147,6 @@ export interface ValidateEmail { * The custom errors configuration. */ export interface CustomErrorsOptions { - /** - * The required email error message. - */ - requiredEmail?: string - /** * The invalid email error message. */ @@ -195,16 +161,6 @@ export interface CustomErrorsOptions { * The expired TOTP error message. */ expiredTotp?: string - - /** - * The inactive TOTP error message. - */ - inactiveTotp?: string - - /** - * The TOTP not found error message. - */ - totpNotFound?: string } /** @@ -255,7 +211,7 @@ export interface TOTPStrategyOptions { /** * The form input name used to get the TOTP. - * @default "totp" + * @default "code" */ totpFieldKey?: string @@ -270,12 +226,6 @@ export interface TOTPStrategyOptions { * @default "auth:totp" */ sessionTotpKey?: string - - /** - * The session key that stores the expiration of the TOTP. - * @default "auth:totpExpiresAt" - */ - sessionTotpExpiresAtKey?: string } /** @@ -323,7 +273,6 @@ export class TOTPStrategy extends Strategy { private readonly totpFieldKey: string private readonly sessionEmailKey: string private readonly sessionTotpKey: string - private readonly sessionTotpExpiresAtKey: string private readonly _totpGenerationDefaults = { secret: generateSecret(), @@ -338,12 +287,9 @@ export class TOTPStrategy extends Strategy { callbackPath: '/magic-link', } satisfies MagicLinkGenerationOptions private readonly _customErrorsDefaults = { - requiredEmail: ERRORS.REQUIRED_EMAIL, invalidEmail: ERRORS.INVALID_EMAIL, invalidTotp: ERRORS.INVALID_TOTP, expiredTotp: ERRORS.EXPIRED_TOTP, - inactiveTotp: ERRORS.INACTIVE_TOTP, - totpNotFound: ERRORS.TOTP_NOT_FOUND, } satisfies CustomErrorsOptions constructor( @@ -359,8 +305,6 @@ export class TOTPStrategy extends Strategy { this.totpFieldKey = options.totpFieldKey ?? FORM_FIELDS.TOTP this.sessionEmailKey = options.sessionEmailKey ?? SESSION_KEYS.EMAIL this.sessionTotpKey = options.sessionTotpKey ?? SESSION_KEYS.TOTP - this.sessionTotpExpiresAtKey = - options.sessionTotpExpiresAtKey ?? SESSION_KEYS.TOTP_EXPIRES_AT this.totpGeneration = { ...this._totpGenerationDefaults, @@ -381,14 +325,6 @@ export class TOTPStrategy extends Strategy { * * If the user is already authenticated, simply returns the user. * - * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | - * |--------|-------|------|-------------|------------|----------------------------------------------------------------------------------------------------------------| - * | POST | ✓ | ✗ | - | - | Generate new TOTP, send to user, store email and TOTP in session. | - * | POST | ✗ | ✓ | ✓ | ✓ | Validate TOTP against session. If valid, authenticate user. | - * | POST | ✗ | ✗ | ✓ | ✓ | Invalidate previous TOTP, generate new one if session has email and TOTP. | - * | POST | ≠ | - | ✓ | ✓ | Invalidate previous TOTP, generate new TOTP for new email. | - * | GET | - | - | - | - | If magic-link enabled and URL has TOTP, validate it. If valid, authenticate user. | - * * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | * |--------|-------|------|-------------|------------|------------------------------------------| * | POST | ✓ | - | - | - | Generate/send TOTP using form email. | @@ -531,11 +467,8 @@ export class TOTPStrategy extends Strategy { request, }) - const expiresAtEpochMs = // (Milliseconds since Unix epoch). - Date.now() + (totp.period ?? this._totpGenerationDefaults.period) * 1000 const totpData: TOTPData = { hash, - expiresAt: new Date(expiresAtEpochMs), attempts: 0, } session.set(this.sessionEmailKey, email) diff --git a/test/index.spec.ts b/test/index.spec.ts index e427fb7..98f4956 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,7 +1,6 @@ import type { Session } from '@remix-run/server-runtime' import type { SendTOTPOptions, - TOTPDataDeprecated, TOTPStrategyOptions, } from '../src/index' From 91b0887086040b356790700de67c83860d5f0f0c Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 18:08:46 -0500 Subject: [PATCH 14/44] Required<> --- src/index.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5149e43..fa2c24e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -264,8 +264,8 @@ export class TOTPStrategy extends Strategy { private readonly secret: string private readonly maxAge: number | undefined - private readonly totpGeneration: TOTPGenerationOptions - private readonly magicLinkGeneration: MagicLinkGenerationOptions + private readonly totpGeneration: Required + private readonly magicLinkGeneration: Required private readonly sendTOTP: SendTOTP private readonly validateEmail: ValidateEmail private readonly customErrors: Required @@ -274,23 +274,23 @@ export class TOTPStrategy extends Strategy { private readonly sessionEmailKey: string private readonly sessionTotpKey: string - private readonly _totpGenerationDefaults = { + private readonly _totpGenerationDefaults: Required = { secret: generateSecret(), algorithm: 'SHA1', charSet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', digits: 6, period: 60, maxAttempts: 3, - } satisfies TOTPGenerationOptions - private readonly _magicLinkGenerationDefaults = { + } + private readonly _magicLinkGenerationDefaults: Required = { enabled: true, callbackPath: '/magic-link', - } satisfies MagicLinkGenerationOptions - private readonly _customErrorsDefaults = { + } + private readonly _customErrorsDefaults: Required = { invalidEmail: ERRORS.INVALID_EMAIL, invalidTotp: ERRORS.INVALID_TOTP, expiredTotp: ERRORS.EXPIRED_TOTP, - } satisfies CustomErrorsOptions + } constructor( options: TOTPStrategyOptions, @@ -298,9 +298,9 @@ export class TOTPStrategy extends Strategy { ) { super(verify) this.secret = options.secret - this.maxAge = options.maxAge ?? undefined + this.maxAge = options.maxAge this.sendTOTP = options.sendTOTP - this.validateEmail = options.validateEmail ?? this._validateEmailDefaults + this.validateEmail = options.validateEmail ?? this._validateEmailDefault this.emailFieldKey = options.emailFieldKey ?? FORM_FIELDS.EMAIL this.totpFieldKey = options.totpFieldKey ?? FORM_FIELDS.TOTP this.sessionEmailKey = options.sessionEmailKey ?? SESSION_KEYS.EMAIL @@ -378,7 +378,7 @@ export class TOTPStrategy extends Strategy { } const code = formDataTotp ?? this._getMagicLinkCode(request) if (code) { - if (!sessionEmail || !sessionTotp) throw new Error(ERRORS.EXPIRED_TOTP) + if (!sessionEmail || !sessionTotp) throw new Error(this.customErrors.expiredTotp) await this._validateTOTP({ code, sessionTotp: sessionTotp as TOTPData, @@ -450,7 +450,7 @@ export class TOTPStrategy extends Strategy { }) const hash = await signJWT({ payload: totp, - expiresIn: this.totpGeneration.period ?? this._totpGenerationDefaults.period, + expiresIn: this.totpGeneration.period, secretKey: this.secret, }) const magicLink = generateMagicLink({ @@ -498,7 +498,7 @@ export class TOTPStrategy extends Strategy { return null } - private async _validateEmailDefaults(email: string) { + private async _validateEmailDefault(email: string) { const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/gm if (!regexEmail.test(email)) throw new Error(this.customErrors.invalidEmail) } @@ -535,9 +535,7 @@ export class TOTPStrategy extends Strategy { session.flash(options.sessionErrorKey, { message: this.customErrors.expiredTotp }) } else { sessionTotp.attempts += 1 - const maxAttempts = - this.totpGeneration.maxAttempts ?? this._totpGenerationDefaults.maxAttempts - if (sessionTotp.attempts >= maxAttempts) { + if (sessionTotp.attempts >= this.totpGeneration.maxAttempts) { session.unset(this.sessionTotpKey) } else { session.set(this.sessionTotpKey, sessionTotp) From ae8a8f9a785405fe4238bcdaf3a9093f4d95af3d Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 18:52:17 -0500 Subject: [PATCH 15/44] coerce --- src/index.ts | 24 +++++++++++------------- src/utils.ts | 30 ++++++++++++++++++++++-------- test/index.spec.ts | 2 +- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/index.ts b/src/index.ts index fa2c24e..de28bab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,9 @@ import { generateMagicLink, signJWT, verifyJWT, - ensureStringOrUndefined, - ensureObjectOrUndefined, - ensureNonEmptyStringOrNull, + coerceToOptionalString, + coerceToOptionalTotpData, + coerceToOptionalNonEmptyString, assertIsRequiredAuthenticateOptions, RequiredAuthenticateOptions, } from './utils.js' @@ -350,10 +350,10 @@ export class TOTPStrategy extends Strategy { if (user) return this.success(user, request, sessionStorage, options) const formData = request.method === 'POST' ? await request.formData() : new FormData() - const formDataEmail = ensureNonEmptyStringOrNull(formData.get(this.emailFieldKey)) - const formDataTotp = ensureNonEmptyStringOrNull(formData.get(this.totpFieldKey)) - const sessionEmail = ensureStringOrUndefined(session.get(this.sessionEmailKey)) - const sessionTotp = ensureObjectOrUndefined(session.get(this.sessionTotpKey)) + const formDataEmail = coerceToOptionalNonEmptyString(formData.get(this.emailFieldKey)) + const formDataTotp = coerceToOptionalNonEmptyString(formData.get(this.totpFieldKey)) + const sessionEmail = coerceToOptionalString(session.get(this.sessionEmailKey)) + const sessionTotp = coerceToOptionalTotpData(session.get(this.sessionTotpKey)) const email = request.method === 'POST' ? formDataEmail ?? (!formDataTotp ? sessionEmail : null) @@ -381,7 +381,7 @@ export class TOTPStrategy extends Strategy { if (!sessionEmail || !sessionTotp) throw new Error(this.customErrors.expiredTotp) await this._validateTOTP({ code, - sessionTotp: sessionTotp as TOTPData, + sessionTotp: sessionTotp, session, sessionStorage, options, @@ -438,11 +438,8 @@ export class TOTPStrategy extends Strategy { sessionStorage: SessionStorage request: Request formData: FormData - options: AuthenticateOptions + options: RequiredAuthenticateOptions }) { - console.log('_generateAndSendTOTP:', { email }) - if (!options.successRedirect) throw new Error(ERRORS.REQUIRED_SUCCESS_REDIRECT_URL) - await this.validateEmail(email) const { otp: code, ...totp } = generateTOTP({ ...this.totpGeneration, @@ -518,8 +515,9 @@ export class TOTPStrategy extends Strategy { }) { try { console.log('_validateTOTP:', { code, sessionTotp }) + // Decryption and Verification. - const { ...totp } = (await verifyJWT({ + const totp = (await verifyJWT({ jwt: sessionTotp.hash, secretKey: this.secret, })) as Required diff --git a/src/utils.ts b/src/utils.ts index daf8a5a..14eb902 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,8 @@ -import type { TOTPGenerationOptions, MagicLinkGenerationOptions } from './index.js' +import type { + TOTPGenerationOptions, + MagicLinkGenerationOptions, + TOTPData, +} from './index.js' import { SignJWT, jwtVerify } from 'jose' import { generateTOTP as _generateTOTP } from '@epic-web/totp' import { ERRORS } from './constants.js' @@ -11,6 +15,9 @@ import { AuthenticateOptions } from 'remix-auth' /** * TOTP Generation. */ + +type TOTPPayload = Omit, "otp"> + export function generateSecret() { return base32.encode(crypto.randomBytes(10)).toString() as string } @@ -74,22 +81,29 @@ export async function verifyJWT({ jwt, secretKey }: VerifyJWTOptions) { * Miscellaneous. */ -export function ensureNonEmptyStringOrNull(value: unknown) { +export function coerceToOptionalNonEmptyString(value: unknown) { if (typeof value === 'string' && value.length > 0) return value - return null + return undefined } -export function ensureStringOrUndefined(value: unknown) { +export function coerceToOptionalString(value: unknown) { if (typeof value !== 'string' && value !== undefined) { throw new Error('Value must be a string or undefined.') } return value } -export function ensureObjectOrUndefined(value: unknown) { - if ((typeof value !== 'object' && value !== undefined) || value === null) { - throw new Error('Value must be a object or undefined.') +export function coerceToOptionalTotpData(value: unknown) { + if ( + typeof value === 'object' && + value !== null && + 'hash' in value && + typeof (value as { hash: unknown }).hash === 'string' && + 'attempts' in value && + typeof (value as { attempts: unknown }).attempts === 'number' + ) { + return value as TOTPData } - return value + return undefined } export type RequiredAuthenticateOptions = Required< diff --git a/test/index.spec.ts b/test/index.spec.ts index 98f4956..2bb34b2 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -361,7 +361,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test('Should successfully validate totp code', async () => { + test.only('Should successfully validate totp code', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) From a8921204048507e90756ab5be93d07d0f39e4e03 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 19:07:41 -0500 Subject: [PATCH 16/44] TOTPPayload --- src/index.ts | 24 ++++++------------------ src/utils.ts | 22 +++++++++++----------- test/index.spec.ts | 2 +- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/index.ts b/src/index.ts index de28bab..6976f7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -358,13 +358,6 @@ export class TOTPStrategy extends Strategy { request.method === 'POST' ? formDataEmail ?? (!formDataTotp ? sessionEmail : null) : null - console.log('authenticate:', { - formDataEmail, - formDataTotp, - sessionEmail, - sessionTotp, - email, - }) try { if (email) { await this._generateAndSendTOTP({ @@ -393,7 +386,6 @@ export class TOTPStrategy extends Strategy { form: formData, request, }) - console.log('authenticate: user', user) session.set(options.sessionKey, user) session.unset(this.sessionEmailKey) @@ -411,7 +403,6 @@ export class TOTPStrategy extends Strategy { } catch (throwable) { if (throwable instanceof Response) throw throwable if (throwable instanceof Error) { - console.log('authenticate: error:', throwable.message) return await this.failure( throwable.message, request, @@ -441,12 +432,12 @@ export class TOTPStrategy extends Strategy { options: RequiredAuthenticateOptions }) { await this.validateEmail(email) - const { otp: code, ...totp } = generateTOTP({ + const { otp: code, ...totpPayload } = generateTOTP({ ...this.totpGeneration, secret: generateSecret(), }) const hash = await signJWT({ - payload: totp, + payload: totpPayload, expiresIn: this.totpGeneration.period, secretKey: this.secret, }) @@ -514,20 +505,17 @@ export class TOTPStrategy extends Strategy { options: RequiredAuthenticateOptions }) { try { - console.log('_validateTOTP:', { code, sessionTotp }) - // Decryption and Verification. - const totp = (await verifyJWT({ + const totpPayload = await verifyJWT({ jwt: sessionTotp.hash, secretKey: this.secret, - })) as Required - console.log('_validateTOTP: totp:', totp) + }) // Verify TOTP (@epic-web/totp). - if (!verifyTOTP({ ...totp, otp: code })) + if (!verifyTOTP({ ...totpPayload, otp: code })) { throw new Error(this.customErrors.invalidTotp) + } } catch (error) { - console.error('_validateTOTP: error:', error) if (error instanceof errors.JWTExpired) { session.unset(this.sessionTotpKey) session.flash(options.sessionErrorKey, { message: this.customErrors.expiredTotp }) diff --git a/src/utils.ts b/src/utils.ts index 14eb902..16bd1d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import type { MagicLinkGenerationOptions, TOTPData, } from './index.js' +import { AuthenticateOptions } from 'remix-auth' import { SignJWT, jwtVerify } from 'jose' import { generateTOTP as _generateTOTP } from '@epic-web/totp' import { ERRORS } from './constants.js' @@ -10,14 +11,10 @@ import { ERRORS } from './constants.js' // @ts-expect-error - `thirty-two` is not typed. import * as base32 from 'thirty-two' import * as crypto from 'crypto' -import { AuthenticateOptions } from 'remix-auth' /** * TOTP Generation. */ - -type TOTPPayload = Omit, "otp"> - export function generateSecret() { return base32.encode(crypto.randomBytes(10)).toString() as string } @@ -46,8 +43,10 @@ export function generateMagicLink( /** * JSON Web Token (JWT). */ +type TOTPPayload = Omit, 'otp'> + type SignJWTOptions = { - payload: { [key: string]: any } + payload: TOTPPayload expiresIn: number secretKey: string } @@ -73,7 +72,7 @@ type VerifyJWTOptions = { export async function verifyJWT({ jwt, secretKey }: VerifyJWTOptions) { const secret = new TextEncoder().encode(secretKey) - const { payload } = await jwtVerify(jwt, secret) + const { payload } = await jwtVerify(jwt, secret) return payload } @@ -81,17 +80,18 @@ export async function verifyJWT({ jwt, secretKey }: VerifyJWTOptions) { * Miscellaneous. */ -export function coerceToOptionalNonEmptyString(value: unknown) { - if (typeof value === 'string' && value.length > 0) return value - return undefined -} - export function coerceToOptionalString(value: unknown) { if (typeof value !== 'string' && value !== undefined) { throw new Error('Value must be a string or undefined.') } return value } + +export function coerceToOptionalNonEmptyString(value: unknown) { + if (typeof value === 'string' && value.length > 0) return value + return undefined +} + export function coerceToOptionalTotpData(value: unknown) { if ( typeof value === 'object' && diff --git a/test/index.spec.ts b/test/index.spec.ts index 2bb34b2..98f4956 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -361,7 +361,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test.only('Should successfully validate totp code', async () => { + test('Should successfully validate totp code', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) From 60b5201e2f37a4d39002e8238b58b399264b24eb Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 20:56:41 -0500 Subject: [PATCH 17/44] required email --- src/constants.ts | 1 + src/index.ts | 8 +++++++- test/index.spec.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index a6c0420..59e1d47 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const SESSION_KEYS = { export const ERRORS = { // Customizable errors. + REQUIRED_EMAIL: 'Email is required.', INVALID_EMAIL: 'Email is not valid.', INVALID_TOTP: 'Code is not valid.', EXPIRED_TOTP: 'Code has expired.', diff --git a/src/index.ts b/src/index.ts index 6976f7b..83d20e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -147,6 +147,11 @@ export interface ValidateEmail { * The custom errors configuration. */ export interface CustomErrorsOptions { + /** + * The required email error message. + */ + requiredEmail?: string + /** * The invalid email error message. */ @@ -287,6 +292,7 @@ export class TOTPStrategy extends Strategy { callbackPath: '/magic-link', } private readonly _customErrorsDefaults: Required = { + requiredEmail: ERRORS.REQUIRED_EMAIL, invalidEmail: ERRORS.INVALID_EMAIL, invalidTotp: ERRORS.INVALID_TOTP, expiredTotp: ERRORS.EXPIRED_TOTP, @@ -400,6 +406,7 @@ export class TOTPStrategy extends Strategy { }, }) } + throw new Error(this.customErrors.requiredEmail) } catch (throwable) { if (throwable instanceof Response) throw throwable if (throwable instanceof Error) { @@ -413,7 +420,6 @@ export class TOTPStrategy extends Strategy { } throw throwable } - throw new Error('Not implemented.') } private async _generateAndSendTOTP({ diff --git a/test/index.spec.ts b/test/index.spec.ts index 98f4956..3d12f4b 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -306,6 +306,32 @@ describe('[ TOTP ]', () => { } else throw reason }) }) + + test('Should failure redirect on missing email.', async () => { + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: new FormData(), + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/login') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.REQUIRED_EMAIL, + }) + } else throw reason + }) + }) }) describe('Validate TOTP', () => { From 6cf3e184384d03aa111dc1e066ae0677adfb31ec Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 21:12:19 -0500 Subject: [PATCH 18/44] stale magic-link and attempts tests --- test/index.spec.ts | 66 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 3d12f4b..b094a99 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,8 +1,5 @@ import type { Session } from '@remix-run/server-runtime' -import type { - SendTOTPOptions, - TOTPStrategyOptions, -} from '../src/index' +import type { SendTOTPOptions, TOTPStrategyOptions } from '../src/index' import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest' import invariant from 'tiny-invariant' @@ -646,6 +643,67 @@ describe('[ TOTP ]', () => { } else throw reason }) }) + + test('Should failure redirect on stale magic-link.', async () => { + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify); + const request = new Request('https://prodserver.com/magic-link?code=KJJERI', { + method: 'GET', + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/login`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERRORS.EXPIRED_TOTP, + }) + } else throw reason + }) + }) + + test('Should failure redirect on magic-link invalid and max TOTP attempts.', async () => { + let { strategy, session, sendTOTPOptions } = await setupGenerateSendTOTP() + expect(sendTOTPOptions.magicLink).toBeDefined() + invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') + for (let i = 0; i <= TOTP_GENERATION_DEFAULTS.maxAttempts; i++) { + const request = new Request(sendTOTPOptions.magicLink + 'INVALID', { + method: 'GET', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: + i < TOTP_GENERATION_DEFAULTS.maxAttempts + ? ERRORS.INVALID_TOTP + : ERRORS.EXPIRED_TOTP, + }) + } else throw reason + }) + } + }) + }) }) From f6f5fe1be4cc7f8728429b6a719dc555306da7c1 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 21:37:12 -0500 Subject: [PATCH 19/44] test custom errors --- test/index.spec.ts | 152 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index b094a99..054dae3 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -304,6 +304,80 @@ describe('[ TOTP ]', () => { }) }) + test('Should failure redirect on invalid email with custom error', async () => { + const CUSTOM_ERROR = 'TEST: Invalid email.' + const strategy = new TOTPStrategy( + { + ...TOTP_STRATEGY_OPTIONS, + customErrors: { + invalidEmail: CUSTOM_ERROR, + }, + }, + verify, + ) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, '@invalid-email') + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/login') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: CUSTOM_ERROR, + }) + } else throw reason + }) + }) + + test('Should failure redirect when custom validateEmail throws Error', async () => { + const ERROR_MESSAGE = 'TEST: Invalid email.' + const strategy = new TOTPStrategy( + { + ...TOTP_STRATEGY_OPTIONS, + validateEmail: () => { + throw new Error(ERROR_MESSAGE) + }, + }, + verify, + ) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, '@invalid-email') + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/login') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: ERROR_MESSAGE, + }) + } else throw reason + }) + }) + test('Should failure redirect on missing email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const request = new Request(`${HOST_URL}/login`, { @@ -446,6 +520,42 @@ describe('[ TOTP ]', () => { }) }) + test('Should failure redirect on invalid totp code with custom error', async () => { + const CUSTOM_ERROR = 'TEST: invalid totp code' + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP({ + customErrors: { + invalidTotp: CUSTOM_ERROR, + }, + }) + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: CUSTOM_ERROR, + }) + } else throw reason + }) + }) + test('Should failure redirect on invalid and max TOTP attempts.', async () => { let { strategy, session, sendTOTPOptions } = await setupGenerateSendTOTP() for (let i = 0; i <= TOTP_GENERATION_DEFAULTS.maxAttempts; i++) { @@ -516,6 +626,45 @@ describe('[ TOTP ]', () => { }) }) + test('Should failure redirect on expired totp code with custom error', async () => { + const CUSTOM_ERROR = 'TEST: expired totp code' + const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP({ + customErrors: { + expiredTotp: CUSTOM_ERROR, + }, + }) + vi.setSystemTime( + new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), + ) + const formData = new FormData() + formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + const request = new Request(`${HOST_URL}/verify`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/account', + failureRedirect: '/verify', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe(`/verify`) + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(AUTH_OPTIONS.sessionErrorKey)).toEqual({ + message: CUSTOM_ERROR, + }) + } else throw reason + }) + }) + test('Should successfully validate magic-link', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() @@ -645,7 +794,7 @@ describe('[ TOTP ]', () => { }) test('Should failure redirect on stale magic-link.', async () => { - const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify); + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const request = new Request('https://prodserver.com/magic-link?code=KJJERI', { method: 'GET', }) @@ -703,7 +852,6 @@ describe('[ TOTP ]', () => { }) } }) - }) }) From b660e081ba7e9192cb84eb49586fdf55a9043ac5 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jan 2024 21:48:05 -0500 Subject: [PATCH 20/44] cleanup --- test/index.spec.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 054dae3..fe7e165 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -21,9 +21,8 @@ import { /** * Mocks. */ -export const verify = vi.fn() -export const sendTOTP = vi.fn() -export const validateEmail = vi.fn() +const verify = vi.fn() +const sendTOTP = vi.fn() const TOTP_STRATEGY_OPTIONS: TOTPStrategyOptions = { secret: SECRET_ENV, @@ -304,7 +303,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on invalid email with custom error', async () => { + test('Should failure redirect on invalid email with custom error.', async () => { const CUSTOM_ERROR = 'TEST: Invalid email.' const strategy = new TOTPStrategy( { @@ -341,7 +340,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect when custom validateEmail throws Error', async () => { + test('Should failure redirect when custom validateEmail throws Error.', async () => { const ERROR_MESSAGE = 'TEST: Invalid email.' const strategy = new TOTPStrategy( { @@ -458,7 +457,7 @@ describe('[ TOTP ]', () => { return { strategy, sendTOTPOptions, session, user } } - test('Should successfully validate totp code', async () => { + test('Should successfully validate totp code.', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) @@ -489,7 +488,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on invalid totp code', async () => { + test('Should failure redirect on invalid totp code.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() const formData = new FormData() formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') @@ -520,7 +519,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on invalid totp code with custom error', async () => { + test('Should failure redirect on invalid totp code with custom error.', async () => { const CUSTOM_ERROR = 'TEST: invalid totp code' const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP({ customErrors: { @@ -592,7 +591,7 @@ describe('[ TOTP ]', () => { } }) - test('Should failure redirect on expired totp code', async () => { + test('Should failure redirect on expired totp code.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() vi.setSystemTime( new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), @@ -626,7 +625,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on expired totp code with custom error', async () => { + test('Should failure redirect on expired totp code with custom error.', async () => { const CUSTOM_ERROR = 'TEST: expired totp code' const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP({ customErrors: { @@ -665,7 +664,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should successfully validate magic-link', async () => { + test('Should successfully validate magic-link.', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') @@ -726,7 +725,7 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect on expired magic-link', async () => { + test('Should failure redirect on expired magic-link.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') From 51861499db6eafad13e94531c920bc7b934320e8 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 17:58:33 -0500 Subject: [PATCH 21/44] renamed totpFieldKey to codeFieldKey --- docs/customization.md | 99 ++++++++++++++++++++----------------------- src/constants.ts | 2 +- src/index.ts | 14 +++--- test/index.spec.ts | 14 +++--- 4 files changed, 60 insertions(+), 69 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index d86a680..7d3722b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -116,11 +116,10 @@ export interface CustomErrorsOptions { * The inactive TOTP error message. */ inactiveTotp?: string - /** + /** * The TOTP not found error message. */ totpNotFound?: string - } authenticator.use( @@ -154,9 +153,9 @@ export interface TOTPStrategyOptions { emailFieldKey?: string /** * The form input name used to get the TOTP. - * @default "totp" + * @default "code" */ - totpFieldKey?: string + codeFieldKey?: string /** * The session key that stores the email address. * @default "auth:email" @@ -172,7 +171,6 @@ export interface TOTPStrategyOptions { * @default "auth:totpExpiresAt" */ sessionTotpExpiresAtKey?: string - } ``` @@ -189,62 +187,55 @@ export default { globals: { Buffer: true, }, - } + }, } ``` ### Using Cloudflare KV for session and TOTP storage ```ts - const sessionStorage = createWorkersKVSessionStorage({ - kv: KV, - cookie: { - name: "_auth", - path: "/", - sameSite: "lax", - httpOnly: true, - secrets: [SESSION_SECRET], - secure: ENVIRONMENT === "production", - }, - }); - const authenticator = new Authenticator(sessionStorage, { - throwOnError: true, - }); - authenticator.use( - new TOTPStrategy( - { - secret: TOTP_SECRET, - magicLinkGeneration: { callbackPath: "/magic-link" }, - - createTOTP: async (data, expiresAt) => { - await KV.put(`totp:${data.hash}`, JSON.stringify(data), { - expirationTtl: Math.max( - (expiresAt.getTime() - Date.now()) / 1000, - 60, - ), // >= 60 secs per Cloudflare KV - }); - }, - readTOTP: async (hash) => { - const totpJson = await KV.get(`totp:${hash}`); - return totpJson ? JSON.parse(totpJson) : null; - }, - updateTOTP: async (hash, data, expiresAt) => { - const totpJson = await KV.get(`totp:${hash}`); - if (!totpJson) throw new Error("TOTP not found"); - const totp = JSON.parse(totpJson); - await KV.put(`totp:${hash}`, JSON.stringify({ ...totp, ...data }), { - expirationTtl: Math.max( - (expiresAt.getTime() - Date.now()) / 1000, - 60, - ), // >= 60 secs per Cloudflare KV - }); - }, - sendTOTP: async ({ email, code, magicLink }) => {} +const sessionStorage = createWorkersKVSessionStorage({ + kv: KV, + cookie: { + name: '_auth', + path: '/', + sameSite: 'lax', + httpOnly: true, + secrets: [SESSION_SECRET], + secure: ENVIRONMENT === 'production', + }, +}) +const authenticator = new Authenticator(sessionStorage, { + throwOnError: true, +}) +authenticator.use( + new TOTPStrategy( + { + secret: TOTP_SECRET, + magicLinkGeneration: { callbackPath: '/magic-link' }, + + createTOTP: async (data, expiresAt) => { + await KV.put(`totp:${data.hash}`, JSON.stringify(data), { + expirationTtl: Math.max((expiresAt.getTime() - Date.now()) / 1000, 60), // >= 60 secs per Cloudflare KV + }) }, - async ({ email }) => {} - ), - ); - + readTOTP: async (hash) => { + const totpJson = await KV.get(`totp:${hash}`) + return totpJson ? JSON.parse(totpJson) : null + }, + updateTOTP: async (hash, data, expiresAt) => { + const totpJson = await KV.get(`totp:${hash}`) + if (!totpJson) throw new Error('TOTP not found') + const totp = JSON.parse(totpJson) + await KV.put(`totp:${hash}`, JSON.stringify({ ...totp, ...data }), { + expirationTtl: Math.max((expiresAt.getTime() - Date.now()) / 1000, 60), // >= 60 secs per Cloudflare KV + }) + }, + sendTOTP: async ({ email, code, magicLink }) => {}, + }, + async ({ email }) => {}, + ), +) ``` ## Contributing diff --git a/src/constants.ts b/src/constants.ts index 59e1d47..0826fd6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,7 @@ export const STRATEGY_NAME = 'TOTP' export const FORM_FIELDS = { EMAIL: 'email', - TOTP: 'code', + CODE: 'code', } as const export const SESSION_KEYS = { diff --git a/src/index.ts b/src/index.ts index 83d20e4..0af861f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -218,7 +218,7 @@ export interface TOTPStrategyOptions { * The form input name used to get the TOTP. * @default "code" */ - totpFieldKey?: string + codeFieldKey?: string /** * The session key that stores the email address. @@ -275,7 +275,7 @@ export class TOTPStrategy extends Strategy { private readonly validateEmail: ValidateEmail private readonly customErrors: Required private readonly emailFieldKey: string - private readonly totpFieldKey: string + private readonly codeFieldKey: string private readonly sessionEmailKey: string private readonly sessionTotpKey: string @@ -308,7 +308,7 @@ export class TOTPStrategy extends Strategy { this.sendTOTP = options.sendTOTP this.validateEmail = options.validateEmail ?? this._validateEmailDefault this.emailFieldKey = options.emailFieldKey ?? FORM_FIELDS.EMAIL - this.totpFieldKey = options.totpFieldKey ?? FORM_FIELDS.TOTP + this.codeFieldKey = options.codeFieldKey ?? FORM_FIELDS.CODE this.sessionEmailKey = options.sessionEmailKey ?? SESSION_KEYS.EMAIL this.sessionTotpKey = options.sessionTotpKey ?? SESSION_KEYS.TOTP @@ -357,7 +357,7 @@ export class TOTPStrategy extends Strategy { const formData = request.method === 'POST' ? await request.formData() : new FormData() const formDataEmail = coerceToOptionalNonEmptyString(formData.get(this.emailFieldKey)) - const formDataTotp = coerceToOptionalNonEmptyString(formData.get(this.totpFieldKey)) + const formDataTotp = coerceToOptionalNonEmptyString(formData.get(this.codeFieldKey)) const sessionEmail = coerceToOptionalString(session.get(this.sessionEmailKey)) const sessionTotp = coerceToOptionalTotpData(session.get(this.sessionTotpKey)) const email = @@ -450,7 +450,7 @@ export class TOTPStrategy extends Strategy { const magicLink = generateMagicLink({ ...this.magicLinkGeneration, code, - param: this.totpFieldKey, + param: this.codeFieldKey, request, }) await this.sendTOTP({ @@ -484,8 +484,8 @@ export class TOTPStrategy extends Strategy { if (url.pathname !== this.magicLinkGeneration.callbackPath) { throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) } - if (url.searchParams.has(this.totpFieldKey)) { - return decodeURIComponent(url.searchParams.get(this.totpFieldKey) ?? '') + if (url.searchParams.has(this.codeFieldKey)) { + return decodeURIComponent(url.searchParams.get(this.codeFieldKey) ?? '') } } } diff --git a/test/index.spec.ts b/test/index.spec.ts index fe7e165..28f2f7c 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -125,7 +125,7 @@ describe('[ TOTP ]', () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) - formData.append(FORM_FIELDS.TOTP, '123456') + formData.append(FORM_FIELDS.CODE, '123456') const request = new Request(`${HOST_URL}/login`, { method: 'POST', body: formData, @@ -460,7 +460,7 @@ describe('[ TOTP ]', () => { test('Should successfully validate totp code.', async () => { const { strategy, sendTOTPOptions, session, user } = await setupGenerateSendTOTP() const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -491,7 +491,7 @@ describe('[ TOTP ]', () => { test('Should failure redirect on invalid totp code.', async () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code + 'INVALID') const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -527,7 +527,7 @@ describe('[ TOTP ]', () => { }, }) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code + 'INVALID') const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -559,7 +559,7 @@ describe('[ TOTP ]', () => { let { strategy, session, sendTOTPOptions } = await setupGenerateSendTOTP() for (let i = 0; i <= TOTP_GENERATION_DEFAULTS.maxAttempts; i++) { const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code + 'INVALID') + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code + 'INVALID') const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -597,7 +597,7 @@ describe('[ TOTP ]', () => { new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), ) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { @@ -636,7 +636,7 @@ describe('[ TOTP ]', () => { new Date(Date.now() + 1000 * 60 * (TOTP_GENERATION_DEFAULTS.period + 1)), ) const formData = new FormData() - formData.append(FORM_FIELDS.TOTP, sendTOTPOptions.code) + formData.append(FORM_FIELDS.CODE, sendTOTPOptions.code) const request = new Request(`${HOST_URL}/verify`, { method: 'POST', headers: { From f47ed6a7bcb0ec315474fcfb9fcbf70eee921224 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 19:11:59 -0500 Subject: [PATCH 22/44] update readme --- README.md | 177 +++++++++++++++++++++++------------------------------- 1 file changed, 74 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 8f3ac4d..a7f4ee2 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,8 @@ npm install remix-auth-totp ## Usage -Remix Auth TOTP exports four required methods: +Remix Auth TOTP exports one required method: -- `createTOTP` - Create the TOTP data in the database. -- `readTOTP` - Read the TOTP data from the database. -- `updateTOTP` - Update the TOTP data in the database. - `sendTOTP` - Sends the TOTP code to the user via email or any other method. Here's a basic overview of the authentication process. @@ -63,41 +60,6 @@ Here's a basic overview of the authentication process. Let's see how we can implement the Strategy into our Remix App. -## Database - -We'll require a database to store our TOTP data. - -For this example we'll use Prisma ORM with a SQLite database. As long as your database supports the following fields, you can use any database of choice. - -```ts -/** - * Fields: - * - `hash`: String - * - `active`: Boolean - * - `attempts`: Int (Number) - * - `expiresAt`: DateTime (Date) - */ -model Totp { - // The encrypted data used to generate the OTP. - hash String @unique - - // The status of the TOTP. - // Used internally / programmatically to invalidate TOTPs. - active Boolean - - // The input attempts of the TOTP. - // Used internally to invalidate TOTPs after a certain amount of attempts. - attempts Int - - // The expiration date of the TOTP. - // Used programmatically to invalidate unused TOTPs. - expiresAt DateTime - - // Index for expiresAt - @@index([expiresAt]) -} -``` - ## Email Service We'll require an Email Service to send the codes to our users. Feel free to use any service of choice, such as [Resend](https://resend.com), [Mailgun](https://www.mailgun.com), [Sendgrid](https://sendgrid.com), etc. The goal is to have a sender function similar to the following one. @@ -172,17 +134,12 @@ type User = { email: string } -export let authenticator = new Authenticator(sessionStorage, { - throwOnError: true, -}) +export let authenticator = new Authenticator(sessionStorage) authenticator.use( new TOTPStrategy( { secret: process.env.ENCRYPTION_SECRET || 'NOT_A_STRONG_SECRET', - createTOTP: async (data, expiresAt) => {}, - readTOTP: async (hash) => {}, - updateTOTP: async (hash, data, expiresAt) => {}, sendTOTP: async ({ email, code, magicLink, user, form, request }) => {}, }, async ({ email, code, form, magicLink, request }) => {}, @@ -195,7 +152,7 @@ authenticator.use( ### 2: Implementing the Strategy Logic. -The Strategy Instance requires the following four methods: `createTOTP`, `readTOTP`, `updateTOTP`, `sendTOTP`. +The Strategy Instance requires the following method: `sendTOTP`. ```ts authenticator.use( @@ -203,26 +160,6 @@ authenticator.use( { secret: process.env.ENCRYPTION_SECRET, - createTOTP: async (data, expiresAt) => { - await prisma.totp.create({ data: { ...data, expiresAt } }) - - try { - // Optional - Delete expired TOTP records. - // Feel free to handle this on a scheduled task. - await prisma.totp.deleteMany({ where: { expiresAt: { lt: new Date() } } }) - } catch (error) { - console.warn('Error deleting expired TOTP records', error) - } - }, - readTOTP: async (hash) => { - // Get the TOTP data from the database. - return await db.totp.findUnique({ where: { hash } }) - }, - updateTOTP: async (hash, data, expiresAt) => { - // Update the TOTP data in the database. - // No need to update expiresAt since it does not change after createTOTP(). - await db.totp.update({ where: { hash }, data }) - }, sendTOTP: async ({ email, code, magicLink }) => { // Send the TOTP code to the user. await sendEmail({ email, code, magicLink }) @@ -233,8 +170,6 @@ authenticator.use( ) ``` -All these CRUD methods should be replaced and adapted with the ones provided by our database. - ### 3. Creating and Storing the User. The Strategy returns a `verify` method that allows handling our own logic. This includes creating the user, updating the user, etc.
@@ -294,13 +229,12 @@ export async function loader({ request }: DataFunctionArgs) { successRedirect: '/account', }) - const cookie = await getSession(request.headers.get('Cookie')) - const authEmail = cookie.get('auth:email') - const authError = cookie.get(authenticator.sessionErrorKey) + const session = await getSession(request.headers.get('Cookie')) + const authError = session.get(authenticator.sessionErrorKey) // Commit session to clear any `flash` error message. return json( - { authEmail, authError }, + { authError }, { headers: { 'set-cookie': await commitSession(session), @@ -312,56 +246,94 @@ export async function loader({ request }: DataFunctionArgs) { export async function action({ request }: DataFunctionArgs) { await authenticator.authenticate('TOTP', request, { // The `successRedirect` route it's required. - // ... // User is not authenticated yet. // We want to redirect to our verify code form. (/verify-code or any other route). successRedirect: '/verify', // The `failureRedirect` route it's required. - // ... // We want to display any possible error message. - // If not provided, ErrorBoundary will be rendered instead. failureRedirect: '/login', }) } export default function Login() { - let { authEmail, authError } = useLoaderData() + let { authError } = useLoaderData() return (
- {/* Email Form. */} - {!authEmail && ( + {/* Login Form. */}
- )} - - {/* Code Verification Form. */} - {authEmail && ( -
- {/* Renders the form that verifies the code. */} -
- - - - -
- - {/* Renders the form that requests a new code. */} - {/* Email input is not required, it's already stored in Session. */} -
- -
-
- )} - - {/* Email Errors Handling. */} - {!authEmail && ({authError?.message || email?.error})} + + {/* Login Errors Handling. */} + {authError?.message} +
+ ) +} +``` + +### `verify.tsx` + +```tsx +// app/routes/verify.tsx +import type { DataFunctionArgs } from '@remix-run/node' +import { json, redirect } from '@remix-run/node' +import { Form, useLoaderData } from '@remix-run/react' + +import { authenticator } from '~/modules/auth/auth.server.ts' +import { getSession, commitSession } from '~/modules/auth/auth-session.server.ts' + +export async function loader({ request }: DataFunctionArgs) { + await authenticator.isAuthenticated(request, { + successRedirect: '/account', + }) + + const session = await getSession(request.headers.get('cookie')) + const authEmail = session.get('auth:email') + const authError = session.get(authenticator.sessionErrorKey) + if (!authEmail) return redirect('/login') + + // Commit session to clear any `flash` error message. + return json({ authError }, { + headers: { + 'set-cookie': await commitSession(session), + }, + }) +} + +export async function action({ request }: DataFunctionArgs) { + const url = new URL(request.url) + const currentPath = url.pathname + + await authenticator.authenticate('TOTP', request, { + successRedirect: currentPath, + failureRedirect: currentPath, + }) +} + +export default function Verify() { + const { authError } = useLoaderData() + + return ( +
+ {/* Code Verification Form */} +
+ + + +
+ + {/* Renders the form that requests a new code. */} + {/* Email input is not required, it's already stored in Session. */} +
+ +
+ {/* Code Errors Handling. */} - {authEmail && ({authError?.message || code?.error})} + {authError?.message}
) } @@ -372,7 +344,6 @@ export default function Login() { ```tsx // app/routes/account.tsx import type { DataFunctionArgs } from '@remix-run/node' - import { json } from '@remix-run/node' import { Form, useLoaderData } from '@remix-run/react' import { authenticator } from '~/modules/auth/auth.server' From c472abd8cac52195f9ee7962d6dce6ca7f7d151e Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 19:17:26 -0500 Subject: [PATCH 23/44] update customization doc --- docs/customization.md | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 7d3722b..c4de584 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -113,13 +113,9 @@ export interface CustomErrorsOptions { */ invalidTotp?: string /** - * The inactive TOTP error message. + * The expired TOTP error message. */ - inactiveTotp?: string - /** - * The TOTP not found error message. - */ - totpNotFound?: string + expiredTotp?: string } authenticator.use( @@ -162,15 +158,10 @@ export interface TOTPStrategyOptions { */ sessionEmailKey?: string /** - * The session key that stores the encrypted TOTP. + * The session key that stores the TOTP data. * @default "auth:totp" */ sessionTotpKey?: string - /** - * The session key that stores the expiration of the TOTP. - * @default "auth:totpExpiresAt" - */ - sessionTotpExpiresAtKey?: string } ``` @@ -191,7 +182,7 @@ export default { } ``` -### Using Cloudflare KV for session and TOTP storage +### Using Cloudflare KV for session storage ```ts const sessionStorage = createWorkersKVSessionStorage({ @@ -205,32 +196,11 @@ const sessionStorage = createWorkersKVSessionStorage({ secure: ENVIRONMENT === 'production', }, }) -const authenticator = new Authenticator(sessionStorage, { - throwOnError: true, -}) +const authenticator = new Authenticator(sessionStorage) authenticator.use( new TOTPStrategy( { secret: TOTP_SECRET, - magicLinkGeneration: { callbackPath: '/magic-link' }, - - createTOTP: async (data, expiresAt) => { - await KV.put(`totp:${data.hash}`, JSON.stringify(data), { - expirationTtl: Math.max((expiresAt.getTime() - Date.now()) / 1000, 60), // >= 60 secs per Cloudflare KV - }) - }, - readTOTP: async (hash) => { - const totpJson = await KV.get(`totp:${hash}`) - return totpJson ? JSON.parse(totpJson) : null - }, - updateTOTP: async (hash, data, expiresAt) => { - const totpJson = await KV.get(`totp:${hash}`) - if (!totpJson) throw new Error('TOTP not found') - const totp = JSON.parse(totpJson) - await KV.put(`totp:${hash}`, JSON.stringify({ ...totp, ...data }), { - expirationTtl: Math.max((expiresAt.getTime() - Date.now()) / 1000, 60), // >= 60 secs per Cloudflare KV - }) - }, sendTOTP: async ({ email, code, magicLink }) => {}, }, async ({ email }) => {}, From 492e2184f91496ddb46c0713865060e0aa2a9c5a Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 19:21:09 -0500 Subject: [PATCH 24/44] update migration doc --- docs/migration.md | 42 ++++-------------------------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/docs/migration.md b/docs/migration.md index 9dd1177..e88f8a8 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,27 +1,14 @@ ## Migration -This document aims to assist you in migrating your `remix-auth-totp` implementation from `v1` to `v2`. +This document aims to assist you in migrating your `remix-auth-totp` implementation from `v2` to `v3`. ### Database -Add `expiresAt` field to `Totp` model if it's not already there. - -```ts -model Totp { - hash String @unique - active Boolean - attempts Int - - // Add `expiresAt` field and index. - expiresAt DateTime - @@index([expiresAt]) -} -``` +Remove `Totp` model from database if one exists. ### Implement `remix-auth-totp` API -- Remove `storeTOTP` and `handleTOTP` from `TOTPStrategy` options. -- Add `createTOTP`, `readTOTP` and `updateTOTP` to `TOTPStrategy` options. +- Remove `createTOTP`, `readTOTP` and `updateTOTP` from `TOTPStrategy` options. ```ts authenticator.use( @@ -29,28 +16,7 @@ authenticator.use( { secret: process.env.ENCRYPTION_SECRET, - // ❗`storeTOTP` and `handleTOTP` are no longer needed (removed). - - createTOTP: async (data, expiresAt) => { - await db.totp.create({ data: { ...data, expiresAt } }) - - try { - // Delete expired TOTP records. - // Better if this were in scheduled task. - await db.totp.deleteMany({ where: { expiresAt: { lt: new Date() } } }) - } catch (error) { - console.warn('Error deleting expired TOTP records', error) - } - }, - readTOTP: async (hash) => { - // Get the TOTP data from the database. - return await db.totp.findUnique({ where: { hash } }) - }, - updateTOTP: async (hash, data, expiresAt) => { - // Update the TOTP data in the database. - // No need to update `expiresAt` since it does not change after createTOTP() is called. - await db.totp.update({ where: { hash }, data }) - }, + // ❗`createTOTP`, `readTOTP` and `updateTOTP` are no longer needed (removed). // Unchanged. sendTOTP: async ({ email, code, magicLink }) => {}, From 7e1ad2218524e155559962b4afd5ed5dd7f6f85a Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 20:38:14 -0500 Subject: [PATCH 25/44] Remove MagicLinkGenerationOptions --- src/index.ts | 57 +++++++++++++--------------------------------- src/utils.ts | 25 +++++++------------- test/index.spec.ts | 13 +++++++---- test/utils.ts | 13 ++++------- 4 files changed, 36 insertions(+), 72 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0af861f..fcaeeb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,23 +78,6 @@ export interface TOTPGenerationOptions { maxAttempts?: number } -/** - * The magic-link configuration. - */ -export interface MagicLinkGenerationOptions { - /** - * Whether to enable the Magic Link generation. - * @default true - */ - enabled?: boolean - - /** - * The callback URL path for the Magic Link. - * @default '/magic-link' - */ - callbackPath?: string -} - /** * The send TOTP configuration. */ @@ -188,11 +171,6 @@ export interface TOTPStrategyOptions { */ totpGeneration?: TOTPGenerationOptions - /** - * The Magic Link configuration. - */ - magicLinkGeneration?: MagicLinkGenerationOptions - /** * The send TOTP method. */ @@ -231,6 +209,12 @@ export interface TOTPStrategyOptions { * @default "auth:totp" */ sessionTotpKey?: string + + /** + * The URL path for the Magic Link. + * @default '/magic-link' + */ + magicLinkPath?: string } /** @@ -270,7 +254,6 @@ export class TOTPStrategy extends Strategy { private readonly secret: string private readonly maxAge: number | undefined private readonly totpGeneration: Required - private readonly magicLinkGeneration: Required private readonly sendTOTP: SendTOTP private readonly validateEmail: ValidateEmail private readonly customErrors: Required @@ -278,6 +261,7 @@ export class TOTPStrategy extends Strategy { private readonly codeFieldKey: string private readonly sessionEmailKey: string private readonly sessionTotpKey: string + private readonly magicLinkPath: string private readonly _totpGenerationDefaults: Required = { secret: generateSecret(), @@ -287,10 +271,6 @@ export class TOTPStrategy extends Strategy { period: 60, maxAttempts: 3, } - private readonly _magicLinkGenerationDefaults: Required = { - enabled: true, - callbackPath: '/magic-link', - } private readonly _customErrorsDefaults: Required = { requiredEmail: ERRORS.REQUIRED_EMAIL, invalidEmail: ERRORS.INVALID_EMAIL, @@ -311,15 +291,12 @@ export class TOTPStrategy extends Strategy { this.codeFieldKey = options.codeFieldKey ?? FORM_FIELDS.CODE this.sessionEmailKey = options.sessionEmailKey ?? SESSION_KEYS.EMAIL this.sessionTotpKey = options.sessionTotpKey ?? SESSION_KEYS.TOTP + this.magicLinkPath = options.magicLinkPath ?? '/magic-link' this.totpGeneration = { ...this._totpGenerationDefaults, ...options.totpGeneration, } - this.magicLinkGeneration = { - ...this._magicLinkGenerationDefaults, - ...options.magicLinkGeneration, - } this.customErrors = { ...this._customErrorsDefaults, ...options.customErrors, @@ -448,8 +425,8 @@ export class TOTPStrategy extends Strategy { secretKey: this.secret, }) const magicLink = generateMagicLink({ - ...this.magicLinkGeneration, code, + magicLinkPath: this.magicLinkPath, param: this.codeFieldKey, request, }) @@ -479,17 +456,15 @@ export class TOTPStrategy extends Strategy { private _getMagicLinkCode(request: Request) { if (request.method === 'GET') { - if (this.magicLinkGeneration.enabled) { - const url = new URL(request.url) - if (url.pathname !== this.magicLinkGeneration.callbackPath) { - throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) - } - if (url.searchParams.has(this.codeFieldKey)) { - return decodeURIComponent(url.searchParams.get(this.codeFieldKey) ?? '') - } + const url = new URL(request.url) + if (url.pathname !== this.magicLinkPath) { + throw new Error(ERRORS.INVALID_MAGIC_LINK_PATH) + } + if (url.searchParams.has(this.codeFieldKey)) { + return decodeURIComponent(url.searchParams.get(this.codeFieldKey) ?? '') } } - return null + return undefined } private async _validateEmailDefault(email: string) { diff --git a/src/utils.ts b/src/utils.ts index 16bd1d1..317dbf4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,4 @@ -import type { - TOTPGenerationOptions, - MagicLinkGenerationOptions, - TOTPData, -} from './index.js' +import type { TOTPGenerationOptions, TOTPData } from './index.js' import { AuthenticateOptions } from 'remix-auth' import { SignJWT, jwtVerify } from 'jose' import { generateTOTP as _generateTOTP } from '@epic-web/totp' @@ -23,18 +19,13 @@ export function generateTOTP(options: TOTPGenerationOptions) { return _generateTOTP(options) } -export function generateMagicLink( - options: MagicLinkGenerationOptions & { - code: string - param: string - request: Request - }, -) { - if (!options.enabled) { - return undefined - } - - const url = new URL(options.callbackPath ?? '/', new URL(options.request.url).origin) +export function generateMagicLink(options: { + code: string + magicLinkPath: string + param: string + request: Request +}) { + const url = new URL(options.magicLinkPath ?? '/', new URL(options.request.url).origin) url.searchParams.set(options.param, options.code) return url.toString() diff --git a/test/index.spec.ts b/test/index.spec.ts index 28f2f7c..85f54c3 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -13,9 +13,9 @@ import { HOST_URL, AUTH_OPTIONS, TOTP_GENERATION_DEFAULTS, - MAGIC_LINK_GENERATION_DEFAULTS, DEFAULT_EMAIL, sessionStorage, + MAGIC_LINK_PATH, } from './utils' /** @@ -27,6 +27,7 @@ const sendTOTP = vi.fn() const TOTP_STRATEGY_OPTIONS: TOTPStrategyOptions = { secret: SECRET_ENV, sendTOTP, + magicLinkPath: MAGIC_LINK_PATH, } beforeEach(() => { @@ -416,7 +417,9 @@ describe('[ TOTP ]', () => { sendTOTPOptions = options expect(options.email).toBe(DEFAULT_EMAIL) expect(options.code).to.not.equal('') - expect(options.magicLink).toBe(`${HOST_URL}/magic-link?code=${options.code}`) + expect(options.magicLink).toBe( + `${HOST_URL}${MAGIC_LINK_PATH}?code=${options.code}`, + ) }) const strategy = new TOTPStrategy( @@ -762,9 +765,9 @@ describe('[ TOTP ]', () => { const { strategy, sendTOTPOptions, session } = await setupGenerateSendTOTP() expect(sendTOTPOptions.magicLink).toBeDefined() invariant(sendTOTPOptions.magicLink, 'Magic link is undefined.') - expect(sendTOTPOptions.magicLink).toMatch(/\/magic-link/) + expect(sendTOTPOptions.magicLink).toMatch(MAGIC_LINK_PATH) const request = new Request( - sendTOTPOptions.magicLink.replace(/\/magic-link/, '/invalid-magic-link'), + sendTOTPOptions.magicLink.replace(MAGIC_LINK_PATH, '/invalid-magic-link'), { method: 'GET', headers: { @@ -870,7 +873,7 @@ describe('[ Utils ]', () => { const request = new Request(requestUrl) expect( generateMagicLink({ - ...MAGIC_LINK_GENERATION_DEFAULTS, + magicLinkPath: '/magic-link', param: 'code', code: 'U2N2EY', request, diff --git a/test/utils.ts b/test/utils.ts index 2e76b66..7c2d180 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,5 +1,5 @@ import type { AuthenticateOptions } from 'remix-auth' -import type { TOTPGenerationOptions, MagicLinkGenerationOptions } from '../src' +import type { TOTPGenerationOptions } from '../src' import { createCookieSessionStorage } from '@remix-run/node' @@ -12,6 +12,7 @@ import * as crypto from 'crypto' export const SECRET_ENV = 'SECRET_ENV' export const HOST_URL = 'https://prodserver.com' export const DEFAULT_EMAIL = 'user@gmail.com' +export const MAGIC_LINK_PATH = '/magic-link' /** * Strategy Defaults. @@ -21,22 +22,16 @@ export const AUTH_OPTIONS = { sessionKey: 'user', sessionErrorKey: 'error', sessionStrategyKey: 'strategy', - throwOnError: true, } satisfies AuthenticateOptions -export const TOTP_GENERATION_DEFAULTS = { +export const TOTP_GENERATION_DEFAULTS: Required = { secret: base32.encode(crypto.randomBytes(10)).toString() as string, algorithm: 'SHA1', charSet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', digits: 6, period: 60, maxAttempts: 3, -} satisfies TOTPGenerationOptions - -export const MAGIC_LINK_GENERATION_DEFAULTS = { - enabled: true, - callbackPath: '/magic-link', -} satisfies MagicLinkGenerationOptions +} /** * Session Storage. From a83360c6074440321b5551dd9521bd17ca540d3b Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 20:47:05 -0500 Subject: [PATCH 26/44] validateEmail() returns boolean --- src/index.ts | 8 +++++--- test/index.spec.ts | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index fcaeeb9..e33ceb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -123,7 +123,7 @@ export interface SendTOTP { * @param email The email address to validate. */ export interface ValidateEmail { - (email: string): Promise + (email: string): Promise } /** @@ -414,7 +414,9 @@ export class TOTPStrategy extends Strategy { formData: FormData options: RequiredAuthenticateOptions }) { - await this.validateEmail(email) + if (!(await this.validateEmail(email))) { + throw new Error(this.customErrors.invalidEmail) + } const { otp: code, ...totpPayload } = generateTOTP({ ...this.totpGeneration, secret: generateSecret(), @@ -469,7 +471,7 @@ export class TOTPStrategy extends Strategy { private async _validateEmailDefault(email: string) { const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/gm - if (!regexEmail.test(email)) throw new Error(this.customErrors.invalidEmail) + return regexEmail.test(email) } private async _validateTOTP({ diff --git a/test/index.spec.ts b/test/index.spec.ts index 85f54c3..31b1da6 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -341,14 +341,15 @@ describe('[ TOTP ]', () => { }) }) - test('Should failure redirect when custom validateEmail throws Error.', async () => { + test('Should failure redirect when custom validateEmail returns false.', async () => { const ERROR_MESSAGE = 'TEST: Invalid email.' const strategy = new TOTPStrategy( { ...TOTP_STRATEGY_OPTIONS, - validateEmail: () => { - throw new Error(ERROR_MESSAGE) + customErrors: { + invalidEmail: ERROR_MESSAGE, }, + validateEmail: async () => false, }, verify, ) From aa0c58f9d52ee273c92083b974e4329a901c7cf5 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 20:49:30 -0500 Subject: [PATCH 27/44] Remove form and request from SendTOTPOptions --- src/index.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index e33ceb6..5b7c5a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,17 +95,7 @@ export interface SendTOTPOptions { /** * The Magic Link URL. */ - magicLink?: string - - /** - * The formData object. - */ - form?: FormData - - /** - * The Request object. - */ - request: Request + magicLink: string } /** @@ -436,8 +426,6 @@ export class TOTPStrategy extends Strategy { email, code, magicLink, - form: formData, - request, }) const totpData: TOTPData = { From fc8b41dfddfcd9756eb9df8ddabe8b0ebba31d3f Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 21:02:21 -0500 Subject: [PATCH 28/44] Remove code, magicLink, form, and request from TOTPVerifyParams --- src/index.ts | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5b7c5a8..93a2bee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -209,33 +209,13 @@ export interface TOTPStrategyOptions { /** * The verify method callback. - * Returns required data to verify the user and handle additional logic. + * Returns the user for the email to be stored in the session. */ export interface TOTPVerifyParams { /** * The email address provided by the user. */ email: string - - /** - * The TOTP code. - */ - code?: string - - /** - * The Magic Link URL. - */ - magicLink?: string - - /** - * The formData object from the Request. - */ - form?: FormData - - /** - * The Request object. - */ - request: Request } export class TOTPStrategy extends Strategy { @@ -298,11 +278,11 @@ export class TOTPStrategy extends Strategy { * * If the user is already authenticated, simply returns the user. * - * | Method | Email | TOTP | Sess. Email | Sess. TOTP | Action/Logic | + * | Method | Email | Code | Sess. Email | Sess. TOTP | Action/Logic | * |--------|-------|------|-------------|------------|------------------------------------------| * | POST | ✓ | - | - | - | Generate/send TOTP using form email. | - * | POST | ✗ | ✗ | ✓ | - | Generate/send TOTP for session email. | - * | POST | ✗ | ✓ | ✓ | ✓ | Validate form TOTP. | + * | POST | ✗ | ✗ | ✓ | - | Generate/send TOTP using session email. | + * | POST | ✗ | ✓ | ✓ | ✓ | Validate form TOTP code. | * | GET | - | - | ✓ | ✓ | Validate magic link TOTP. | * * @param {Request} request - The request object. @@ -356,8 +336,6 @@ export class TOTPStrategy extends Strategy { // Allow developer to handle user validation. const user = await this.verify({ email: sessionEmail, - form: formData, - request, }) session.set(options.sessionKey, user) From 77bd12b7592089a475f10c2a76f47bdc28ea6817 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 21:09:35 -0500 Subject: [PATCH 29/44] Update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a7f4ee2..f79928e 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,9 @@ authenticator.use( new TOTPStrategy( { secret: process.env.ENCRYPTION_SECRET || 'NOT_A_STRONG_SECRET', - sendTOTP: async ({ email, code, magicLink, user, form, request }) => {}, + sendTOTP: async ({ email, code, magicLink }) => {}, }, - async ({ email, code, form, magicLink, request }) => {}, + async ({ email }) => {}, ), ) ``` @@ -165,7 +165,7 @@ authenticator.use( await sendEmail({ email, code, magicLink }) }, }, - async ({ email, code, magicLink, form, request }) => {}, + async ({ email }) => {}, ), ) ``` @@ -184,7 +184,7 @@ authenticator.use( // createTOTP: async (data) => {}, // ... }, - async ({ email, code, magicLink, form, request }) => { + async ({ email }) => { // You can determine whether the user is authenticating // via OTP code submission or Magic-Link URL and run your own logic. if (form) console.log('Optional form submission logic.') From 90e9fec4f4b0fd99dff10e7624f9e1035002b97d Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 4 Jan 2024 21:17:20 -0500 Subject: [PATCH 30/44] Update docs --- docs/customization.md | 29 +++++------------------------ docs/migration.md | 7 ++++--- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index c4de584..2426aff 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -70,30 +70,6 @@ authenticator.use( ) ``` -### Magic Link Generation - -The Magic Link is optional and enabled by default. You can decide to opt-out by setting the `enabled` option to `false`. - -Furthermore, the Magic Link can be customized via the `magicLinkGeneration` object in the TOTPStrategy Instance. -The URL link generated will be in the format of `{request url origin}{callbackPath}?{codeField}=`. - -```ts -export interface MagicLinkGenerationOptions { - /** - * Whether to enable the Magic Link generation. - * @default true - */ - enabled?: boolean - /** - * The callback path for the Magic Link. - * @default '/magic-link' - */ - callbackPath?: string -} -``` - -> **Note:** Enabling the Magic Link feature will require to create a [magic-link.tsx](#magic-linktsx) route. - ### Custom Error Messages The Strategy includes a few default error messages that can be customized by passing an object called `customErrors` to the TOTPStrategy Instance. @@ -162,6 +138,11 @@ export interface TOTPStrategyOptions { * @default "auth:totp" */ sessionTotpKey?: string + /** + * The URL path for the Magic Link. + * @default '/magic-link' + */ + magicLinkPath?: string } ``` diff --git a/docs/migration.md b/docs/migration.md index e88f8a8..8d4f079 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -9,6 +9,7 @@ Remove `Totp` model from database if one exists. ### Implement `remix-auth-totp` API - Remove `createTOTP`, `readTOTP` and `updateTOTP` from `TOTPStrategy` options. +- Remove unneeded parameters from `sendTOTP` and `verify` functions ```ts authenticator.use( @@ -18,11 +19,11 @@ authenticator.use( // ❗`createTOTP`, `readTOTP` and `updateTOTP` are no longer needed (removed). - // Unchanged. + // Only email, code, and magicLink. Remove any other parameters. sendTOTP: async ({ email, code, magicLink }) => {}, }, - // Unchanged. - async ({ email, code, magicLink, form, request }) => {}, + // Only email. Remove any other parameters. + async ({ email }) => {}, ), ) ``` From a5dbc75af58330e56bad4510e669255dd232747b Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Fri, 5 Jan 2024 11:04:49 -0500 Subject: [PATCH 31/44] refactor !(await ) --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 93a2bee..76215e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -382,7 +382,8 @@ export class TOTPStrategy extends Strategy { formData: FormData options: RequiredAuthenticateOptions }) { - if (!(await this.validateEmail(email))) { + const isValidEmail = await this.validateEmail(email) + if (!isValidEmail) { throw new Error(this.customErrors.invalidEmail) } const { otp: code, ...totpPayload } = generateTOTP({ From e49ea05e49d0a55a3641c214cd582634546d0187 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Fri, 5 Jan 2024 11:17:14 -0500 Subject: [PATCH 32/44] Refactor _generateAndSendTOTP() --- src/index.ts | 66 +++++++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/src/index.ts b/src/index.ts index 76215e8..cd03108 100644 --- a/src/index.ts +++ b/src/index.ts @@ -313,17 +313,32 @@ export class TOTPStrategy extends Strategy { : null try { if (email) { - await this._generateAndSendTOTP({ + // Generate and send TOTP. + const { code, hash, magicLink } = await this._generateTOTP({ email, request }) + await this.sendTOTP({ email, - session, - sessionStorage, - request, - formData, - options, + code, + magicLink, + }) + + const totpData: TOTPData = { + hash, + attempts: 0, + } + session.set(this.sessionEmailKey, email) + session.set(this.sessionTotpKey, totpData) + session.unset(options.sessionErrorKey) + throw redirect(options.successRedirect, { + headers: { + 'set-cookie': await sessionStorage.commitSession(session, { + maxAge: this.maxAge, + }), + }, }) } const code = formDataTotp ?? this._getMagicLinkCode(request) if (code) { + // Validate TOTP. if (!sessionEmail || !sessionTotp) throw new Error(this.customErrors.expiredTotp) await this._validateTOTP({ code, @@ -333,7 +348,6 @@ export class TOTPStrategy extends Strategy { options, }) - // Allow developer to handle user validation. const user = await this.verify({ email: sessionEmail, }) @@ -367,22 +381,8 @@ export class TOTPStrategy extends Strategy { } } - private async _generateAndSendTOTP({ - email, - session, - sessionStorage, - request, - formData, - options, - }: { - email: string - session: Session - sessionStorage: SessionStorage - request: Request - formData: FormData - options: RequiredAuthenticateOptions - }) { - const isValidEmail = await this.validateEmail(email) + private async _generateTOTP({ email, request }: { email: string; request: Request }) { + const isValidEmail = await this.validateEmail(email) if (!isValidEmail) { throw new Error(this.customErrors.invalidEmail) } @@ -401,26 +401,8 @@ export class TOTPStrategy extends Strategy { param: this.codeFieldKey, request, }) - await this.sendTOTP({ - email, - code, - magicLink, - }) - const totpData: TOTPData = { - hash, - attempts: 0, - } - session.set(this.sessionEmailKey, email) - session.set(this.sessionTotpKey, totpData) - session.unset(options.sessionErrorKey) - throw redirect(options.successRedirect, { - headers: { - 'set-cookie': await sessionStorage.commitSession(session, { - maxAge: this.maxAge, - }), - }, - }) + return { code, hash, magicLink } } private _getMagicLinkCode(request: Request) { From a208a32bb2691e612392450a58258f458f0aeffa Mon Sep 17 00:00:00 2001 From: Dev XO Date: Sat, 6 Jan 2024 14:57:58 +0100 Subject: [PATCH 33/44] minor tweaks --- src/index.ts | 71 ++++++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/src/index.ts b/src/index.ts index cd03108..86d244b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ import type { Session, SessionStorage } from '@remix-run/server-runtime' import type { AuthenticateOptions, StrategyVerifyCallback } from 'remix-auth' -import { errors } from 'jose' import { redirect } from '@remix-run/server-runtime' import { Strategy } from 'remix-auth' import { verifyTOTP } from '@epic-web/totp' +import { errors as JoseErrors } from 'jose' import { generateSecret, generateTOTP, @@ -162,14 +162,10 @@ export interface TOTPStrategyOptions { totpGeneration?: TOTPGenerationOptions /** - * The send TOTP method. - */ - sendTOTP: SendTOTP - - /** - * The validate email method. + * The URL path for the Magic Link. + * @default '/magic-link' */ - validateEmail?: ValidateEmail + magicLinkPath?: string /** * The custom errors configuration. @@ -201,10 +197,14 @@ export interface TOTPStrategyOptions { sessionTotpKey?: string /** - * The URL path for the Magic Link. - * @default '/magic-link' + * The send TOTP method. */ - magicLinkPath?: string + sendTOTP: SendTOTP + + /** + * The validate email method. + */ + validateEmail?: ValidateEmail } /** @@ -224,14 +224,14 @@ export class TOTPStrategy extends Strategy { private readonly secret: string private readonly maxAge: number | undefined private readonly totpGeneration: Required - private readonly sendTOTP: SendTOTP - private readonly validateEmail: ValidateEmail + private readonly magicLinkPath: string private readonly customErrors: Required private readonly emailFieldKey: string private readonly codeFieldKey: string private readonly sessionEmailKey: string private readonly sessionTotpKey: string - private readonly magicLinkPath: string + private readonly sendTOTP: SendTOTP + private readonly validateEmail: ValidateEmail private readonly _totpGenerationDefaults: Required = { secret: generateSecret(), @@ -255,13 +255,13 @@ export class TOTPStrategy extends Strategy { super(verify) this.secret = options.secret this.maxAge = options.maxAge - this.sendTOTP = options.sendTOTP - this.validateEmail = options.validateEmail ?? this._validateEmailDefault + this.magicLinkPath = options.magicLinkPath ?? '/magic-link' this.emailFieldKey = options.emailFieldKey ?? FORM_FIELDS.EMAIL this.codeFieldKey = options.codeFieldKey ?? FORM_FIELDS.CODE this.sessionEmailKey = options.sessionEmailKey ?? SESSION_KEYS.EMAIL this.sessionTotpKey = options.sessionTotpKey ?? SESSION_KEYS.TOTP - this.magicLinkPath = options.magicLinkPath ?? '/magic-link' + this.sendTOTP = options.sendTOTP + this.validateEmail = options.validateEmail ?? this._validateEmailDefault this.totpGeneration = { ...this._totpGenerationDefaults, @@ -304,30 +304,25 @@ export class TOTPStrategy extends Strategy { const formData = request.method === 'POST' ? await request.formData() : new FormData() const formDataEmail = coerceToOptionalNonEmptyString(formData.get(this.emailFieldKey)) - const formDataTotp = coerceToOptionalNonEmptyString(formData.get(this.codeFieldKey)) + const formDataCode = coerceToOptionalNonEmptyString(formData.get(this.codeFieldKey)) const sessionEmail = coerceToOptionalString(session.get(this.sessionEmailKey)) const sessionTotp = coerceToOptionalTotpData(session.get(this.sessionTotpKey)) const email = request.method === 'POST' - ? formDataEmail ?? (!formDataTotp ? sessionEmail : null) + ? formDataEmail ?? (!formDataCode ? sessionEmail : null) : null + try { if (email) { - // Generate and send TOTP. + // Generate and Send TOTP. const { code, hash, magicLink } = await this._generateTOTP({ email, request }) - await this.sendTOTP({ - email, - code, - magicLink, - }) + await this.sendTOTP({ email, code, magicLink }) - const totpData: TOTPData = { - hash, - attempts: 0, - } + const totpData: TOTPData = { hash, attempts: 0 } session.set(this.sessionEmailKey, email) session.set(this.sessionTotpKey, totpData) session.unset(options.sessionErrorKey) + throw redirect(options.successRedirect, { headers: { 'set-cookie': await sessionStorage.commitSession(session, { @@ -336,17 +331,12 @@ export class TOTPStrategy extends Strategy { }, }) } - const code = formDataTotp ?? this._getMagicLinkCode(request) + + const code = formDataCode ?? this._getMagicLinkCode(request) if (code) { // Validate TOTP. if (!sessionEmail || !sessionTotp) throw new Error(this.customErrors.expiredTotp) - await this._validateTOTP({ - code, - sessionTotp: sessionTotp, - session, - sessionStorage, - options, - }) + await this._validateTOTP({ code, sessionTotp, session, sessionStorage, options }) const user = await this.verify({ email: sessionEmail, @@ -383,9 +373,8 @@ export class TOTPStrategy extends Strategy { private async _generateTOTP({ email, request }: { email: string; request: Request }) { const isValidEmail = await this.validateEmail(email) - if (!isValidEmail) { - throw new Error(this.customErrors.invalidEmail) - } + if (!isValidEmail) throw new Error(this.customErrors.invalidEmail) + const { otp: code, ...totpPayload } = generateTOTP({ ...this.totpGeneration, secret: generateSecret(), @@ -448,7 +437,7 @@ export class TOTPStrategy extends Strategy { throw new Error(this.customErrors.invalidTotp) } } catch (error) { - if (error instanceof errors.JWTExpired) { + if (error instanceof JoseErrors.JWTExpired) { session.unset(this.sessionTotpKey) session.flash(options.sessionErrorKey, { message: this.customErrors.expiredTotp }) } else { From 5320a463cda54b53d6c2a1f168f42d597bdbaa3f Mon Sep 17 00:00:00 2001 From: Dev XO Date: Sat, 6 Jan 2024 15:06:05 +0100 Subject: [PATCH 34/44] revert JoseErrors to errors It was good as it was. --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 86d244b..a010040 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import type { AuthenticateOptions, StrategyVerifyCallback } from 'remix-auth' import { redirect } from '@remix-run/server-runtime' import { Strategy } from 'remix-auth' import { verifyTOTP } from '@epic-web/totp' -import { errors as JoseErrors } from 'jose' +import { errors } from 'jose' import { generateSecret, generateTOTP, @@ -437,7 +437,7 @@ export class TOTPStrategy extends Strategy { throw new Error(this.customErrors.invalidTotp) } } catch (error) { - if (error instanceof JoseErrors.JWTExpired) { + if (error instanceof errors.JWTExpired) { session.unset(this.sessionTotpKey) session.flash(options.sessionErrorKey, { message: this.customErrors.expiredTotp }) } else { From e99159ece6deae14ff9fe3ed264d60f5ee7bc58a Mon Sep 17 00:00:00 2001 From: Dev XO Date: Sat, 6 Jan 2024 18:39:32 +0100 Subject: [PATCH 35/44] chore: update documentation Changed to as it was incorrect - Mentioned in #46 --- docs/customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 2426aff..8652c37 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -22,7 +22,7 @@ authenticator.use( ### TOTP Generation -The TOTP generation can customized by passing an object called `codeGeneration` to the TOTPStrategy Instance. +The TOTP generation can customized by passing an object called `totpGeneration` to the TOTPStrategy Instance. ```ts export interface TOTPGenerationOptions { @@ -61,7 +61,7 @@ export interface TOTPGenerationOptions { authenticator.use( new TOTPStrategy({ - codeGeneration: { + totpGeneration: { digits: 6, period: 60, // ... From 0fc32d312cdf1ed6b46557471241f09bda53dca3 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 5 Feb 2024 08:30:47 -0500 Subject: [PATCH 36/44] use node import --- package.json | 2 +- src/utils.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7f56cbc..c8ff99f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "/public/dist" ], "dependencies": { - "@epic-web/totp": "^1.1.1", + "@epic-web/totp": "github:mw10013/totp#types", "jose": "^5.2.0", "thirty-two": "^1.0.2" }, diff --git a/src/utils.ts b/src/utils.ts index 317dbf4..52b4269 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,14 +5,14 @@ import { generateTOTP as _generateTOTP } from '@epic-web/totp' import { ERRORS } from './constants.js' // @ts-expect-error - `thirty-two` is not typed. -import * as base32 from 'thirty-two' -import * as crypto from 'crypto' +import { encode } from 'thirty-two' +import { randomBytes } from 'node:crypto' /** * TOTP Generation. */ export function generateSecret() { - return base32.encode(crypto.randomBytes(10)).toString() as string + return encode(randomBytes(10)).toString() as string } export function generateTOTP(options: TOTPGenerationOptions) { From f65003eb105821fdd53e9eec3a9119f519d65da7 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 5 Feb 2024 10:14:02 -0500 Subject: [PATCH 37/44] add build to prepare script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8ff99f..be2cd67 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test": "vitest --reporter verbose", "test:cov": "vitest run --coverage", "validate": "npm run lint && npm run typecheck && npx vitest --watch=false", - "prepare": "husky install", + "prepare": "husky install && npm run build", "prepublishOnly": "npm run validate && npm run build" }, "files": [ From 0b8a52bc7613f7919f0ebcbf6e6c11668e51e2c2 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 5 Feb 2024 12:47:04 -0500 Subject: [PATCH 38/44] import * --- src/utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 52b4269..1e40f47 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,14 +5,13 @@ import { generateTOTP as _generateTOTP } from '@epic-web/totp' import { ERRORS } from './constants.js' // @ts-expect-error - `thirty-two` is not typed. -import { encode } from 'thirty-two' -import { randomBytes } from 'node:crypto' - +import * as base32 from 'thirty-two' +import * as crypto from 'node:crypto' /** * TOTP Generation. */ export function generateSecret() { - return encode(randomBytes(10)).toString() as string + return base32.encode(crypto.randomBytes(10)).toString() as string } export function generateTOTP(options: TOTPGenerationOptions) { From 859b50053d7a3d37f30073ff810305cf1a050448 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 5 Feb 2024 15:23:12 -0500 Subject: [PATCH 39/44] add pnpm-lock.yaml per https://pnpm.io/git#lockfiles --- .gitignore | 2 - package.json | 2 +- pnpm-lock.yaml | 2748 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2749 insertions(+), 3 deletions(-) create mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 2989237..743d2b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ # Package Managers. package-lock.json yarn.lock -pnpm-lock.yaml -pnpm-lock.yml node_modules # Editor Configs. diff --git a/package.json b/package.json index be2cd67..d12c18c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "/public/dist" ], "dependencies": { - "@epic-web/totp": "github:mw10013/totp#types", + "@epic-web/totp": "^1.1.2", "jose": "^5.2.0", "thirty-two": "^1.0.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3edf8c3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2748 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@epic-web/totp': + specifier: ^1.1.2 + version: 1.1.2 + jose: + specifier: ^5.2.0 + version: 5.2.1 + remix-auth: + specifier: ^3.6.0 + version: 3.6.0(@remix-run/react@2.6.0)(@remix-run/server-runtime@2.6.0) + thirty-two: + specifier: ^1.0.2 + version: 1.0.2 + +devDependencies: + '@remix-run/node': + specifier: ^2.0.0 + version: 2.6.0(typescript@5.3.3) + '@remix-run/server-runtime': + specifier: ^2.0.0 + version: 2.6.0(typescript@5.3.3) + '@types/node': + specifier: ^20.10.5 + version: 20.11.16 + '@typescript-eslint/eslint-plugin': + specifier: ^5.62.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^5.62.0 + version: 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@vitest/coverage-v8': + specifier: 1.0.0-beta.3 + version: 1.0.0-beta.3(vitest@1.2.2) + eslint: + specifier: ^8.56.0 + version: 8.56.0 + eslint-config-prettier: + specifier: ^8.10.0 + version: 8.10.0(eslint@8.56.0) + eslint-plugin-prettier: + specifier: ^4.2.1 + version: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.56.0)(prettier@2.8.8) + husky: + specifier: ^8.0.3 + version: 8.0.3 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + tiny-invariant: + specifier: ^1.3.1 + version: 1.3.1 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + vite: + specifier: ^4.5.1 + version: 4.5.2(@types/node@20.11.16) + vite-tsconfig-paths: + specifier: ^4.2.2 + version: 4.3.1(typescript@5.3.3)(vite@4.5.2) + vitest: + specifier: ^1.1.0 + version: 1.2.2(@types/node@20.11.16) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@epic-web/totp@1.1.2: + resolution: {integrity: sha512-ngw0+NjNgZ1ikmascnQWfut7dj0cuWqKwDM7m2w7JoOXHZCR0C5cx2aUPIl8oHOO2DQzkI6yaJLoc1bMDOn/oQ==} + engines: {node: '>=18'} + dependencies: + thirty-two: 1.0.2 + dev: false + + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.56.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.56.0: + resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@remix-run/node@2.6.0(typescript@5.3.3): + resolution: {integrity: sha512-bWemy3g258Kdqi+4OxIEZ7QS64T96jNK6a7NdlPXGJZqeLpxM5NmlCl/slSdx52oTi9r5Xoz1Tm4uR37nD1/Xw==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@remix-run/server-runtime': 2.6.0(typescript@5.3.3) + '@remix-run/web-fetch': 4.4.2 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.1 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + typescript: 5.3.3 + dev: true + + /@remix-run/react@2.6.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-m/Ph6bryny7wrmrQyXQMvIiW+cBLrU/MepcLGFPvTVVwvfeiGBgXRiYZJ6yPNsfrmHFaS83d+Ja/Mx4N4zUWcg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@remix-run/router': 1.15.0 + '@remix-run/server-runtime': 2.6.0(typescript@5.3.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.22.0(react@18.2.0) + react-router-dom: 6.22.0(react-dom@18.2.0)(react@18.2.0) + typescript: 5.3.3 + dev: false + + /@remix-run/router@1.15.0: + resolution: {integrity: sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==} + engines: {node: '>=14.0.0'} + + /@remix-run/server-runtime@2.6.0(typescript@5.3.3): + resolution: {integrity: sha512-qFXDl4pK55njBLuvyRn5AkI/hu8fEU3t1XFKv0Syivx0nmUVpWMW25Uzi1pkX/chF1VIxCVrZ8KuQ1rcrKj+DQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@remix-run/router': 1.15.0 + '@types/cookie': 0.6.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie: 0.6.0 + set-cookie-parser: 2.6.0 + source-map: 0.7.4 + typescript: 5.3.3 + + /@remix-run/web-blob@3.1.0: + resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + dependencies: + '@remix-run/web-stream': 1.1.0 + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-fetch@4.4.2: + resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} + engines: {node: ^10.17 || >=12.3} + dependencies: + '@remix-run/web-blob': 3.1.0 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-form-data': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + dev: true + + /@remix-run/web-file@3.1.0: + resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + dependencies: + '@remix-run/web-blob': 3.1.0 + dev: true + + /@remix-run/web-form-data@3.1.0: + resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + dependencies: + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-stream@1.1.0: + resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + dependencies: + web-streams-polyfill: 3.3.2 + dev: true + + /@rollup/rollup-android-arm-eabi@4.9.6: + resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.9.6: + resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.9.6: + resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.9.6: + resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.9.6: + resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.6: + resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.9.6: + resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.9.6: + resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.6: + resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.9.6: + resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.9.6: + resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.9.6: + resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.9.6: + resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/node@20.11.16: + resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare-lite: 1.4.0 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + eslint: 8.56.0 + eslint-scope: 5.1.1 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitest/coverage-v8@1.0.0-beta.3(vitest@1.2.2): + resolution: {integrity: sha512-ewUkoelUzU3lBxWiv53lMzfgAZzz9OSyveb4ngIIP+I2/zqNA2IGhhHrFgFL4AqSoOBD1W2mUoUO4noaUrP2Wg==} + peerDependencies: + vitest: ^1.0.0-0 + dependencies: + '@ampproject/remapping': 2.2.1 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + magic-string: 0.30.7 + picocolors: 1.0.0 + std-env: 3.7.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + vitest: 1.2.2(@types/node@20.11.16) + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@1.2.2: + resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} + dependencies: + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.2.2: + resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==} + dependencies: + '@vitest/utils': 1.2.2 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.2.2: + resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} + dependencies: + magic-string: 0.30.7 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.2.2: + resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==} + dependencies: + tinyspy: 2.2.0 + dev: true + + /@vitest/utils@1.2.2: + resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + + /@web3-storage/multipart-parser@1.0.0: + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: true + optional: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /available-typed-arrays@1.0.6: + resolution: {integrity: sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==} + engines: {node: '>= 0.4'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.3 + set-function-length: 1.2.0 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + dev: true + + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-prettier@8.10.0(eslint@8.56.0): + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.56.0 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.56.0)(prettier@2.8.8): + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.56.0 + eslint-config-prettier: 8.10.0(eslint@8.56.0) + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.56.0: + resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.56.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.2.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-intrinsic@1.2.3: + resolution: {integrity: sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.3 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.3 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + + /husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.2 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.14 + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /jose@5.2.1: + resolution: {integrity: sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.5.0 + pkg-types: 1.0.3 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /magic-string@0.30.7: + resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /mlly@1.5.0: + resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.3.2 + dev: true + + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /npm-run-path@5.2.0: + resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.1 + mlly: 1.5.0 + pathe: 1.1.2 + dev: true + + /postcss@8.4.34: + resolution: {integrity: sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /react-router-dom@6.22.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.15.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.22.0(react@18.2.0) + dev: false + + /react-router@6.22.0(react@18.2.0): + resolution: {integrity: sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.15.0 + react: 18.2.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /remix-auth@3.6.0(@remix-run/react@2.6.0)(@remix-run/server-runtime@2.6.0): + resolution: {integrity: sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==} + peerDependencies: + '@remix-run/react': ^1.0.0 || ^2.0.0 + '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 + dependencies: + '@remix-run/react': 2.6.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@remix-run/server-runtime': 2.6.0(typescript@5.3.3) + uuid: 8.3.2 + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /rollup@4.9.6: + resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.9.6 + '@rollup/rollup-android-arm64': 4.9.6 + '@rollup/rollup-darwin-arm64': 4.9.6 + '@rollup/rollup-darwin-x64': 4.9.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.6 + '@rollup/rollup-linux-arm64-gnu': 4.9.6 + '@rollup/rollup-linux-arm64-musl': 4.9.6 + '@rollup/rollup-linux-riscv64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-musl': 4.9.6 + '@rollup/rollup-win32-arm64-msvc': 4.9.6 + '@rollup/rollup-win32-ia32-msvc': 4.9.6 + '@rollup/rollup-win32-x64-msvc': 4.9.6 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + + /set-function-length@1.2.0: + resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + function-bind: 1.1.2 + get-intrinsic: 1.2.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + + /stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + dependencies: + acorn: 8.11.3 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + dev: false + + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: true + + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.0: + resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} + engines: {node: '>=14.0.0'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tsconfck@3.0.1(typescript@5.3.3): + resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.3.3 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + /ufo@1.3.2: + resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.14 + dev: true + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + + /vite-node@1.2.2(@types/node@20.11.16): + resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.0.12(@types/node@20.11.16) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@4.5.2): + resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + debug: 4.3.4 + globrex: 0.1.2 + tsconfck: 3.0.1(typescript@5.3.3) + vite: 4.5.2(@types/node@20.11.16) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /vite@4.5.2(@types/node@20.11.16): + resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.16 + esbuild: 0.18.20 + postcss: 8.4.34 + rollup: 3.29.4 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vite@5.0.12(@types/node@20.11.16): + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.16 + esbuild: 0.19.12 + postcss: 8.4.34 + rollup: 4.9.6 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.2.2(@types/node@20.11.16): + resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': ^1.0.0 + '@vitest/ui': ^1.0.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.11.16 + '@vitest/expect': 1.2.2 + '@vitest/runner': 1.2.2 + '@vitest/snapshot': 1.2.2 + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + acorn-walk: 8.3.2 + cac: 6.7.14 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.7 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 1.3.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.0.12(@types/node@20.11.16) + vite-node: 1.2.2(@types/node@20.11.16) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: true + + /web-streams-polyfill@3.3.2: + resolution: {integrity: sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==} + engines: {node: '>= 8'} + dev: true + + /which-typed-array@1.1.14: + resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.6 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true From c9c2d0dcdc4c04be5f505bb4c601f2126da57d0e Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 12 Feb 2024 16:28:00 -0500 Subject: [PATCH 40/44] add request and formData to SendTOTPOptions --- pnpm-lock.yaml | 2162 ++++++++++++++++++++++++++++++++------------ src/index.ts | 12 +- test/index.spec.ts | 114 +++ 3 files changed, 1720 insertions(+), 568 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3edf8c3..5ebf78b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,34 +69,48 @@ devDependencies: version: 1.2.2(@types/node@20.11.16) packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==, + } + engines: { node: '>=0.10.0' } dev: true /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==, + } + engines: { node: '>=6.0.0' } dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 dev: true /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + resolution: + { + integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, + } dev: true /@epic-web/totp@1.1.2: - resolution: {integrity: sha512-ngw0+NjNgZ1ikmascnQWfut7dj0cuWqKwDM7m2w7JoOXHZCR0C5cx2aUPIl8oHOO2DQzkI6yaJLoc1bMDOn/oQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-ngw0+NjNgZ1ikmascnQWfut7dj0cuWqKwDM7m2w7JoOXHZCR0C5cx2aUPIl8oHOO2DQzkI6yaJLoc1bMDOn/oQ==, + } + engines: { node: '>=18' } dependencies: thirty-two: 1.0.2 dev: false /@esbuild/aix-ppc64@0.19.12: - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==, + } + engines: { node: '>=12' } cpu: [ppc64] os: [aix] requiresBuild: true @@ -104,8 +118,11 @@ packages: optional: true /@esbuild/android-arm64@0.18.20: - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==, + } + engines: { node: '>=12' } cpu: [arm64] os: [android] requiresBuild: true @@ -113,8 +130,11 @@ packages: optional: true /@esbuild/android-arm64@0.19.12: - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==, + } + engines: { node: '>=12' } cpu: [arm64] os: [android] requiresBuild: true @@ -122,8 +142,11 @@ packages: optional: true /@esbuild/android-arm@0.18.20: - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==, + } + engines: { node: '>=12' } cpu: [arm] os: [android] requiresBuild: true @@ -131,8 +154,11 @@ packages: optional: true /@esbuild/android-arm@0.19.12: - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==, + } + engines: { node: '>=12' } cpu: [arm] os: [android] requiresBuild: true @@ -140,8 +166,11 @@ packages: optional: true /@esbuild/android-x64@0.18.20: - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==, + } + engines: { node: '>=12' } cpu: [x64] os: [android] requiresBuild: true @@ -149,8 +178,11 @@ packages: optional: true /@esbuild/android-x64@0.19.12: - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==, + } + engines: { node: '>=12' } cpu: [x64] os: [android] requiresBuild: true @@ -158,8 +190,11 @@ packages: optional: true /@esbuild/darwin-arm64@0.18.20: - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==, + } + engines: { node: '>=12' } cpu: [arm64] os: [darwin] requiresBuild: true @@ -167,8 +202,11 @@ packages: optional: true /@esbuild/darwin-arm64@0.19.12: - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==, + } + engines: { node: '>=12' } cpu: [arm64] os: [darwin] requiresBuild: true @@ -176,8 +214,11 @@ packages: optional: true /@esbuild/darwin-x64@0.18.20: - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [darwin] requiresBuild: true @@ -185,8 +226,11 @@ packages: optional: true /@esbuild/darwin-x64@0.19.12: - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==, + } + engines: { node: '>=12' } cpu: [x64] os: [darwin] requiresBuild: true @@ -194,8 +238,11 @@ packages: optional: true /@esbuild/freebsd-arm64@0.18.20: - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==, + } + engines: { node: '>=12' } cpu: [arm64] os: [freebsd] requiresBuild: true @@ -203,8 +250,11 @@ packages: optional: true /@esbuild/freebsd-arm64@0.19.12: - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==, + } + engines: { node: '>=12' } cpu: [arm64] os: [freebsd] requiresBuild: true @@ -212,8 +262,11 @@ packages: optional: true /@esbuild/freebsd-x64@0.18.20: - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [freebsd] requiresBuild: true @@ -221,8 +274,11 @@ packages: optional: true /@esbuild/freebsd-x64@0.19.12: - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==, + } + engines: { node: '>=12' } cpu: [x64] os: [freebsd] requiresBuild: true @@ -230,8 +286,11 @@ packages: optional: true /@esbuild/linux-arm64@0.18.20: - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==, + } + engines: { node: '>=12' } cpu: [arm64] os: [linux] requiresBuild: true @@ -239,8 +298,11 @@ packages: optional: true /@esbuild/linux-arm64@0.19.12: - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==, + } + engines: { node: '>=12' } cpu: [arm64] os: [linux] requiresBuild: true @@ -248,8 +310,11 @@ packages: optional: true /@esbuild/linux-arm@0.18.20: - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==, + } + engines: { node: '>=12' } cpu: [arm] os: [linux] requiresBuild: true @@ -257,8 +322,11 @@ packages: optional: true /@esbuild/linux-arm@0.19.12: - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==, + } + engines: { node: '>=12' } cpu: [arm] os: [linux] requiresBuild: true @@ -266,8 +334,11 @@ packages: optional: true /@esbuild/linux-ia32@0.18.20: - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==, + } + engines: { node: '>=12' } cpu: [ia32] os: [linux] requiresBuild: true @@ -275,8 +346,11 @@ packages: optional: true /@esbuild/linux-ia32@0.19.12: - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==, + } + engines: { node: '>=12' } cpu: [ia32] os: [linux] requiresBuild: true @@ -284,8 +358,11 @@ packages: optional: true /@esbuild/linux-loong64@0.18.20: - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==, + } + engines: { node: '>=12' } cpu: [loong64] os: [linux] requiresBuild: true @@ -293,8 +370,11 @@ packages: optional: true /@esbuild/linux-loong64@0.19.12: - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==, + } + engines: { node: '>=12' } cpu: [loong64] os: [linux] requiresBuild: true @@ -302,8 +382,11 @@ packages: optional: true /@esbuild/linux-mips64el@0.18.20: - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==, + } + engines: { node: '>=12' } cpu: [mips64el] os: [linux] requiresBuild: true @@ -311,8 +394,11 @@ packages: optional: true /@esbuild/linux-mips64el@0.19.12: - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==, + } + engines: { node: '>=12' } cpu: [mips64el] os: [linux] requiresBuild: true @@ -320,8 +406,11 @@ packages: optional: true /@esbuild/linux-ppc64@0.18.20: - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==, + } + engines: { node: '>=12' } cpu: [ppc64] os: [linux] requiresBuild: true @@ -329,8 +418,11 @@ packages: optional: true /@esbuild/linux-ppc64@0.19.12: - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==, + } + engines: { node: '>=12' } cpu: [ppc64] os: [linux] requiresBuild: true @@ -338,8 +430,11 @@ packages: optional: true /@esbuild/linux-riscv64@0.18.20: - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==, + } + engines: { node: '>=12' } cpu: [riscv64] os: [linux] requiresBuild: true @@ -347,8 +442,11 @@ packages: optional: true /@esbuild/linux-riscv64@0.19.12: - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==, + } + engines: { node: '>=12' } cpu: [riscv64] os: [linux] requiresBuild: true @@ -356,8 +454,11 @@ packages: optional: true /@esbuild/linux-s390x@0.18.20: - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==, + } + engines: { node: '>=12' } cpu: [s390x] os: [linux] requiresBuild: true @@ -365,8 +466,11 @@ packages: optional: true /@esbuild/linux-s390x@0.19.12: - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==, + } + engines: { node: '>=12' } cpu: [s390x] os: [linux] requiresBuild: true @@ -374,8 +478,11 @@ packages: optional: true /@esbuild/linux-x64@0.18.20: - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==, + } + engines: { node: '>=12' } cpu: [x64] os: [linux] requiresBuild: true @@ -383,8 +490,11 @@ packages: optional: true /@esbuild/linux-x64@0.19.12: - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==, + } + engines: { node: '>=12' } cpu: [x64] os: [linux] requiresBuild: true @@ -392,8 +502,11 @@ packages: optional: true /@esbuild/netbsd-x64@0.18.20: - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==, + } + engines: { node: '>=12' } cpu: [x64] os: [netbsd] requiresBuild: true @@ -401,8 +514,11 @@ packages: optional: true /@esbuild/netbsd-x64@0.19.12: - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==, + } + engines: { node: '>=12' } cpu: [x64] os: [netbsd] requiresBuild: true @@ -410,8 +526,11 @@ packages: optional: true /@esbuild/openbsd-x64@0.18.20: - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==, + } + engines: { node: '>=12' } cpu: [x64] os: [openbsd] requiresBuild: true @@ -419,8 +538,11 @@ packages: optional: true /@esbuild/openbsd-x64@0.19.12: - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==, + } + engines: { node: '>=12' } cpu: [x64] os: [openbsd] requiresBuild: true @@ -428,8 +550,11 @@ packages: optional: true /@esbuild/sunos-x64@0.18.20: - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [sunos] requiresBuild: true @@ -437,8 +562,11 @@ packages: optional: true /@esbuild/sunos-x64@0.19.12: - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==, + } + engines: { node: '>=12' } cpu: [x64] os: [sunos] requiresBuild: true @@ -446,8 +574,11 @@ packages: optional: true /@esbuild/win32-arm64@0.18.20: - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==, + } + engines: { node: '>=12' } cpu: [arm64] os: [win32] requiresBuild: true @@ -455,8 +586,11 @@ packages: optional: true /@esbuild/win32-arm64@0.19.12: - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==, + } + engines: { node: '>=12' } cpu: [arm64] os: [win32] requiresBuild: true @@ -464,8 +598,11 @@ packages: optional: true /@esbuild/win32-ia32@0.18.20: - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==, + } + engines: { node: '>=12' } cpu: [ia32] os: [win32] requiresBuild: true @@ -473,8 +610,11 @@ packages: optional: true /@esbuild/win32-ia32@0.19.12: - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==, + } + engines: { node: '>=12' } cpu: [ia32] os: [win32] requiresBuild: true @@ -482,8 +622,11 @@ packages: optional: true /@esbuild/win32-x64@0.18.20: - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [win32] requiresBuild: true @@ -491,8 +634,11 @@ packages: optional: true /@esbuild/win32-x64@0.19.12: - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==, + } + engines: { node: '>=12' } cpu: [x64] os: [win32] requiresBuild: true @@ -500,8 +646,11 @@ packages: optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: @@ -510,13 +659,19 @@ packages: dev: true /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + resolution: + { + integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } dev: true /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: ajv: 6.12.6 debug: 4.3.4 @@ -532,13 +687,19 @@ packages: dev: true /@eslint/js@8.56.0: - resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dev: true /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + resolution: + { + integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==, + } + engines: { node: '>=10.10.0' } dependencies: '@humanwhocodes/object-schema': 2.0.2 debug: 4.3.4 @@ -548,29 +709,44 @@ packages: dev: true /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: '>=12.22' } dev: true /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + resolution: + { + integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==, + } dev: true /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==, + } + engines: { node: '>=8' } dev: true /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==, + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } dependencies: '@sinclair/typebox': 0.27.8 dev: true /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==, + } + engines: { node: '>=6.0.0' } dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 @@ -578,50 +754,74 @@ packages: dev: true /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==, + } + engines: { node: '>=6.0.0' } dev: true /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==, + } + engines: { node: '>=6.0.0' } dev: true /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + resolution: + { + integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==, + } dev: true /@jridgewell/trace-mapping@0.3.22: - resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + resolution: + { + integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==, + } dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 dev: true /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: '>= 8' } dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 dev: true /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: '>= 8' } dev: true /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: '>= 8' } dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 dev: true /@remix-run/node@2.6.0(typescript@5.3.3): - resolution: {integrity: sha512-bWemy3g258Kdqi+4OxIEZ7QS64T96jNK6a7NdlPXGJZqeLpxM5NmlCl/slSdx52oTi9r5Xoz1Tm4uR37nD1/Xw==} - engines: {node: '>=18.0.0'} + resolution: + { + integrity: sha512-bWemy3g258Kdqi+4OxIEZ7QS64T96jNK6a7NdlPXGJZqeLpxM5NmlCl/slSdx52oTi9r5Xoz1Tm4uR37nD1/Xw==, + } + engines: { node: '>=18.0.0' } peerDependencies: typescript: ^5.1.0 peerDependenciesMeta: @@ -640,8 +840,11 @@ packages: dev: true /@remix-run/react@2.6.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): - resolution: {integrity: sha512-m/Ph6bryny7wrmrQyXQMvIiW+cBLrU/MepcLGFPvTVVwvfeiGBgXRiYZJ6yPNsfrmHFaS83d+Ja/Mx4N4zUWcg==} - engines: {node: '>=18.0.0'} + resolution: + { + integrity: sha512-m/Ph6bryny7wrmrQyXQMvIiW+cBLrU/MepcLGFPvTVVwvfeiGBgXRiYZJ6yPNsfrmHFaS83d+Ja/Mx4N4zUWcg==, + } + engines: { node: '>=18.0.0' } peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -660,12 +863,18 @@ packages: dev: false /@remix-run/router@1.15.0: - resolution: {integrity: sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==, + } + engines: { node: '>=14.0.0' } /@remix-run/server-runtime@2.6.0(typescript@5.3.3): - resolution: {integrity: sha512-qFXDl4pK55njBLuvyRn5AkI/hu8fEU3t1XFKv0Syivx0nmUVpWMW25Uzi1pkX/chF1VIxCVrZ8KuQ1rcrKj+DQ==} - engines: {node: '>=18.0.0'} + resolution: + { + integrity: sha512-qFXDl4pK55njBLuvyRn5AkI/hu8fEU3t1XFKv0Syivx0nmUVpWMW25Uzi1pkX/chF1VIxCVrZ8KuQ1rcrKj+DQ==, + } + engines: { node: '>=18.0.0' } peerDependencies: typescript: ^5.1.0 peerDependenciesMeta: @@ -681,15 +890,21 @@ packages: typescript: 5.3.3 /@remix-run/web-blob@3.1.0: - resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + resolution: + { + integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==, + } dependencies: '@remix-run/web-stream': 1.1.0 web-encoding: 1.1.5 dev: true /@remix-run/web-fetch@4.4.2: - resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} - engines: {node: ^10.17 || >=12.3} + resolution: + { + integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==, + } + engines: { node: ^10.17 || >=12.3 } dependencies: '@remix-run/web-blob': 3.1.0 '@remix-run/web-file': 3.1.0 @@ -702,25 +917,37 @@ packages: dev: true /@remix-run/web-file@3.1.0: - resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + resolution: + { + integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==, + } dependencies: '@remix-run/web-blob': 3.1.0 dev: true /@remix-run/web-form-data@3.1.0: - resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + resolution: + { + integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==, + } dependencies: web-encoding: 1.1.5 dev: true /@remix-run/web-stream@1.1.0: - resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + resolution: + { + integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==, + } dependencies: web-streams-polyfill: 3.3.2 dev: true /@rollup/rollup-android-arm-eabi@4.9.6: - resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} + resolution: + { + integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==, + } cpu: [arm] os: [android] requiresBuild: true @@ -728,7 +955,10 @@ packages: optional: true /@rollup/rollup-android-arm64@4.9.6: - resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==} + resolution: + { + integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==, + } cpu: [arm64] os: [android] requiresBuild: true @@ -736,7 +966,10 @@ packages: optional: true /@rollup/rollup-darwin-arm64@4.9.6: - resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==} + resolution: + { + integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==, + } cpu: [arm64] os: [darwin] requiresBuild: true @@ -744,7 +977,10 @@ packages: optional: true /@rollup/rollup-darwin-x64@4.9.6: - resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==} + resolution: + { + integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==, + } cpu: [x64] os: [darwin] requiresBuild: true @@ -752,7 +988,10 @@ packages: optional: true /@rollup/rollup-linux-arm-gnueabihf@4.9.6: - resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==} + resolution: + { + integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==, + } cpu: [arm] os: [linux] requiresBuild: true @@ -760,7 +999,10 @@ packages: optional: true /@rollup/rollup-linux-arm64-gnu@4.9.6: - resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==} + resolution: + { + integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==, + } cpu: [arm64] os: [linux] requiresBuild: true @@ -768,7 +1010,10 @@ packages: optional: true /@rollup/rollup-linux-arm64-musl@4.9.6: - resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==} + resolution: + { + integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==, + } cpu: [arm64] os: [linux] requiresBuild: true @@ -776,7 +1021,10 @@ packages: optional: true /@rollup/rollup-linux-riscv64-gnu@4.9.6: - resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==} + resolution: + { + integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==, + } cpu: [riscv64] os: [linux] requiresBuild: true @@ -784,7 +1032,10 @@ packages: optional: true /@rollup/rollup-linux-x64-gnu@4.9.6: - resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==} + resolution: + { + integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==, + } cpu: [x64] os: [linux] requiresBuild: true @@ -792,7 +1043,10 @@ packages: optional: true /@rollup/rollup-linux-x64-musl@4.9.6: - resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==} + resolution: + { + integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==, + } cpu: [x64] os: [linux] requiresBuild: true @@ -800,7 +1054,10 @@ packages: optional: true /@rollup/rollup-win32-arm64-msvc@4.9.6: - resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==} + resolution: + { + integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==, + } cpu: [arm64] os: [win32] requiresBuild: true @@ -808,7 +1065,10 @@ packages: optional: true /@rollup/rollup-win32-ia32-msvc@4.9.6: - resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==} + resolution: + { + integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==, + } cpu: [ia32] os: [win32] requiresBuild: true @@ -816,7 +1076,10 @@ packages: optional: true /@rollup/rollup-win32-x64-msvc@4.9.6: - resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==} + resolution: + { + integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==, + } cpu: [x64] os: [win32] requiresBuild: true @@ -824,37 +1087,61 @@ packages: optional: true /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + resolution: + { + integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, + } dev: true /@types/cookie@0.6.0: - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + resolution: + { + integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, + } /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + resolution: + { + integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, + } dev: true /@types/istanbul-lib-coverage@2.0.6: - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + resolution: + { + integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==, + } dev: true /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } dev: true /@types/node@20.11.16: - resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==} + resolution: + { + integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==, + } dependencies: undici-types: 5.26.5 dev: true /@types/semver@7.5.6: - resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + resolution: + { + integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==, + } dev: true /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: '@typescript-eslint/parser': ^5.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -881,8 +1168,11 @@ packages: dev: true /@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' @@ -901,16 +1191,22 @@ packages: dev: true /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 dev: true /@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: '*' typescript: '*' @@ -929,13 +1225,19 @@ packages: dev: true /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dev: true /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -955,8 +1257,11 @@ packages: dev: true /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: @@ -975,19 +1280,28 @@ packages: dev: true /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: '@typescript-eslint/types': 5.62.0 eslint-visitor-keys: 3.4.3 dev: true /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + resolution: + { + integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==, + } dev: true /@vitest/coverage-v8@1.0.0-beta.3(vitest@1.2.2): - resolution: {integrity: sha512-ewUkoelUzU3lBxWiv53lMzfgAZzz9OSyveb4ngIIP+I2/zqNA2IGhhHrFgFL4AqSoOBD1W2mUoUO4noaUrP2Wg==} + resolution: + { + integrity: sha512-ewUkoelUzU3lBxWiv53lMzfgAZzz9OSyveb4ngIIP+I2/zqNA2IGhhHrFgFL4AqSoOBD1W2mUoUO4noaUrP2Wg==, + } peerDependencies: vitest: ^1.0.0-0 dependencies: @@ -1008,7 +1322,10 @@ packages: dev: true /@vitest/expect@1.2.2: - resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} + resolution: + { + integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==, + } dependencies: '@vitest/spy': 1.2.2 '@vitest/utils': 1.2.2 @@ -1016,7 +1333,10 @@ packages: dev: true /@vitest/runner@1.2.2: - resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==} + resolution: + { + integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==, + } dependencies: '@vitest/utils': 1.2.2 p-limit: 5.0.0 @@ -1024,7 +1344,10 @@ packages: dev: true /@vitest/snapshot@1.2.2: - resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} + resolution: + { + integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==, + } dependencies: magic-string: 0.30.7 pathe: 1.1.2 @@ -1032,13 +1355,19 @@ packages: dev: true /@vitest/spy@1.2.2: - resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==} + resolution: + { + integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==, + } dependencies: tinyspy: 2.2.0 dev: true /@vitest/utils@1.2.2: - resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==} + resolution: + { + integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==, + } dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -1047,23 +1376,35 @@ packages: dev: true /@web3-storage/multipart-parser@1.0.0: - resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + resolution: + { + integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==, + } /@zxing/text-encoding@0.9.0: - resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + resolution: + { + integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==, + } requiresBuild: true dev: true optional: true /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} + resolution: + { + integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, + } + engines: { node: '>=6.5' } dependencies: event-target-shim: 5.0.1 dev: true /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: @@ -1071,18 +1412,27 @@ packages: dev: true /acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==, + } + engines: { node: '>=0.4.0' } dev: true /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==, + } + engines: { node: '>=0.4.0' } hasBin: true dev: true /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + resolution: + { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, + } dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -1091,69 +1441,108 @@ packages: dev: true /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: '>=8' } dev: true /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: '>=8' } dependencies: color-convert: 2.0.1 dev: true /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, + } + engines: { node: '>=10' } dev: true /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } dev: true /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, + } + engines: { node: '>=8' } dev: true /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + resolution: + { + integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==, + } dev: true /available-typed-arrays@1.0.6: - resolution: {integrity: sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==, + } + engines: { node: '>= 0.4' } dev: true /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } dev: true /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + resolution: + { + integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, + } dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 dev: true /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==, + } + engines: { node: '>=8' } dependencies: fill-range: 7.0.1 dev: true /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, + } dev: true /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, + } + engines: { node: '>=8' } dev: true /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + resolution: + { + integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==, + } dependencies: function-bind: 1.1.2 get-intrinsic: 1.2.3 @@ -1161,13 +1550,19 @@ packages: dev: true /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: '>=6' } dev: true /chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==, + } + engines: { node: '>=4' } dependencies: assertion-error: 1.1.0 check-error: 1.0.3 @@ -1179,50 +1574,77 @@ packages: dev: true /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: '>=10' } dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 dev: true /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + resolution: + { + integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==, + } dependencies: get-func-name: 2.0.2 dev: true /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: '>=7.0.0' } dependencies: color-name: 1.1.4 dev: true /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } dev: true /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } dev: true /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } dev: true /cookie-signature@1.2.1: - resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} - engines: {node: '>=6.6.0'} + resolution: + { + integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==, + } + engines: { node: '>=6.6.0' } dev: true /cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, + } + engines: { node: '>= 0.6' } /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, + } + engines: { node: '>= 8' } dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -1230,13 +1652,19 @@ packages: dev: true /data-uri-to-buffer@3.0.1: - resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==, + } + engines: { node: '>= 6' } dev: true /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, + } + engines: { node: '>=6.0' } peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -1247,19 +1675,28 @@ packages: dev: true /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==, + } + engines: { node: '>=6' } dependencies: type-detect: 4.0.8 dev: true /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } dev: true /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==, + } + engines: { node: '>= 0.4' } dependencies: get-intrinsic: 1.2.3 gopd: 1.0.1 @@ -1267,32 +1704,47 @@ packages: dev: true /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==, + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } dev: true /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, + } + engines: { node: '>=8' } dependencies: path-type: 4.0.0 dev: true /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, + } + engines: { node: '>=6.0.0' } dependencies: esutils: 2.0.3 dev: true /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: '>= 0.4' } dev: true /esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==, + } + engines: { node: '>=12' } hasBin: true requiresBuild: true optionalDependencies: @@ -1321,8 +1773,11 @@ packages: dev: true /esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==, + } + engines: { node: '>=12' } hasBin: true requiresBuild: true optionalDependencies: @@ -1352,12 +1807,18 @@ packages: dev: true /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: '>=10' } dev: true /eslint-config-prettier@8.10.0(eslint@8.56.0): - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + resolution: + { + integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==, + } hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -1366,8 +1827,11 @@ packages: dev: true /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.56.0)(prettier@2.8.8): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==, + } + engines: { node: '>=12.0.0' } peerDependencies: eslint: '>=7.28.0' eslint-config-prettier: '*' @@ -1383,29 +1847,41 @@ packages: dev: true /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==, + } + engines: { node: '>=8.0.0' } dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 dev: true /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 dev: true /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dev: true /eslint@8.56.0: - resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) @@ -1451,8 +1927,11 @@ packages: dev: true /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: acorn: 8.11.3 acorn-jsx: 5.3.2(acorn@8.11.3) @@ -1460,48 +1939,72 @@ packages: dev: true /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==, + } + engines: { node: '>=0.10' } dependencies: estraverse: 5.3.0 dev: true /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: '>=4.0' } dependencies: estraverse: 5.3.0 dev: true /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==, + } + engines: { node: '>=4.0' } dev: true /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: '>=4.0' } dev: true /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } dependencies: '@types/estree': 1.0.5 dev: true /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: '>=0.10.0' } dev: true /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, + } + engines: { node: '>=6' } dev: true /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + resolution: + { + integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==, + } + engines: { node: '>=16.17' } dependencies: cross-spawn: 7.0.3 get-stream: 8.0.1 @@ -1515,16 +2018,25 @@ packages: dev: true /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } dev: true /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + resolution: + { + integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==, + } dev: true /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + resolution: + { + integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, + } + engines: { node: '>=8.6.0' } dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -1534,44 +2046,65 @@ packages: dev: true /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } dev: true /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } dev: true /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + resolution: + { + integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, + } dependencies: reusify: 1.0.4 dev: true /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + resolution: + { + integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==, + } + engines: { node: ^10.12.0 || >=12.0.0 } dependencies: flat-cache: 3.2.0 dev: true /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, + } + engines: { node: '>=8' } dependencies: to-regex-range: 5.0.1 dev: true /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: '>=10' } dependencies: locate-path: 6.0.0 path-exists: 4.0.0 dev: true /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + resolution: + { + integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==, + } + engines: { node: ^10.12.0 || >=12.0.0 } dependencies: flatted: 3.2.9 keyv: 4.5.4 @@ -1579,38 +2112,59 @@ packages: dev: true /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + resolution: + { + integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==, + } dev: true /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + resolution: + { + integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==, + } dependencies: is-callable: 1.2.7 dev: true /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, + } dev: true /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] requiresBuild: true dev: true optional: true /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } dev: true /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + resolution: + { + integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==, + } dev: true /get-intrinsic@1.2.3: - resolution: {integrity: sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==, + } + engines: { node: '>= 0.4' } dependencies: es-errors: 1.3.0 function-bind: 1.1.2 @@ -1620,26 +2174,38 @@ packages: dev: true /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==, + } + engines: { node: '>=16' } dev: true /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: '>= 6' } dependencies: is-glob: 4.0.3 dev: true /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: '>=10.13.0' } dependencies: is-glob: 4.0.3 dev: true /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + resolution: + { + integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, + } dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1650,15 +2216,21 @@ packages: dev: true /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==, + } + engines: { node: '>=8' } dependencies: type-fest: 0.20.2 dev: true /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, + } + engines: { node: '>=10' } dependencies: array-union: 2.1.0 dir-glob: 3.0.1 @@ -1669,164 +2241,251 @@ packages: dev: true /globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + resolution: + { + integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==, + } dev: true /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + resolution: + { + integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==, + } dependencies: get-intrinsic: 1.2.3 dev: true /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + resolution: + { + integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, + } dev: true /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: '>=8' } dev: true /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + resolution: + { + integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==, + } dependencies: get-intrinsic: 1.2.3 dev: true /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==, + } + engines: { node: '>= 0.4' } dev: true /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==, + } + engines: { node: '>= 0.4' } dev: true /has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, + } + engines: { node: '>= 0.4' } dependencies: has-symbols: 1.0.3 dev: true /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==, + } + engines: { node: '>= 0.4' } dependencies: function-bind: 1.1.2 dev: true /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, + } dev: true /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + resolution: + { + integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==, + } + engines: { node: '>=16.17.0' } dev: true /husky@8.0.3: - resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==, + } + engines: { node: '>=14' } hasBin: true dev: true /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==, + } + engines: { node: '>= 4' } dev: true /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==, + } + engines: { node: '>=6' } dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 dev: true /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: '>=0.8.19' } dev: true /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, + } dependencies: once: 1.4.0 wrappy: 1.0.2 dev: true /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } dev: true /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==, + } + engines: { node: '>= 0.4' } dependencies: call-bind: 1.0.5 has-tostringtag: 1.0.2 dev: true /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, + } + engines: { node: '>= 0.4' } dev: true /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: '>=0.10.0' } dev: true /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==, + } + engines: { node: '>= 0.4' } dependencies: has-tostringtag: 1.0.2 dev: true /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: '>=0.10.0' } dependencies: is-extglob: 2.1.1 dev: true /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: '>=0.12.0' } dev: true /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, + } + engines: { node: '>=8' } dev: true /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } dev: true /is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==, + } + engines: { node: '>= 0.4' } dependencies: which-typed-array: 1.1.14 dev: true /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } dev: true /istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, + } + engines: { node: '>=8' } dev: true /istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==, + } + engines: { node: '>=10' } dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 @@ -1834,8 +2493,11 @@ packages: dev: true /istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==, + } + engines: { node: '>=10' } dependencies: debug: 4.3.4 istanbul-lib-coverage: 3.2.2 @@ -1845,141 +2507,213 @@ packages: dev: true /istanbul-reports@3.1.6: - resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==, + } + engines: { node: '>=8' } dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 dev: true /jose@5.2.1: - resolution: {integrity: sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==} + resolution: + { + integrity: sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==, + } dev: false /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } dev: false /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + resolution: + { + integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, + } hasBin: true dependencies: argparse: 2.0.1 dev: true /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } dev: true /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } dev: true /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } dev: true /jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + resolution: + { + integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==, + } dev: true /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } dependencies: json-buffer: 3.0.1 dev: true /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: '>= 0.8.0' } dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 dev: true /local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==, + } + engines: { node: '>=14' } dependencies: mlly: 1.5.0 pkg-types: 1.0.3 dev: true /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: '>=10' } dependencies: p-locate: 5.0.0 dev: true /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } dev: true /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + resolution: + { + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, + } hasBin: true dependencies: js-tokens: 4.0.0 dev: false /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + resolution: + { + integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==, + } dependencies: get-func-name: 2.0.2 dev: true /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, + } + engines: { node: '>=10' } dependencies: yallist: 4.0.0 dev: true /magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==, + } + engines: { node: '>=12' } dependencies: '@jridgewell/sourcemap-codec': 1.4.15 dev: true /make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, + } + engines: { node: '>=10' } dependencies: semver: 7.6.0 dev: true /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + resolution: + { + integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, + } dev: true /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: '>= 8' } dev: true /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==, + } + engines: { node: '>=8.6' } dependencies: braces: 3.0.2 picomatch: 2.3.1 dev: true /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==, + } + engines: { node: '>=12' } dev: true /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } dependencies: brace-expansion: 1.1.11 dev: true /mlly@1.5.0: - resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} + resolution: + { + integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==, + } dependencies: acorn: 8.11.3 pathe: 1.1.2 @@ -1988,51 +2722,78 @@ packages: dev: true /mrmime@1.0.1: - resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==, + } + engines: { node: '>=10' } dev: true /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + resolution: + { + integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, + } dev: true /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true dev: true /natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + resolution: + { + integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==, + } dev: true /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } dev: true /npm-run-path@5.2.0: - resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } dependencies: path-key: 4.0.0 dev: true /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } dependencies: wrappy: 1.0.2 dev: true /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==, + } + engines: { node: '>=12' } dependencies: mimic-fn: 4.0.0 dev: true /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==, + } + engines: { node: '>= 0.8.0' } dependencies: '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 @@ -2043,77 +2804,119 @@ packages: dev: true /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: '>=10' } dependencies: yocto-queue: 0.1.0 dev: true /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==, + } + engines: { node: '>=18' } dependencies: yocto-queue: 1.0.0 dev: true /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: '>=10' } dependencies: p-limit: 3.1.0 dev: true /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: '>=6' } dependencies: callsites: 3.1.0 dev: true /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: '>=8' } dev: true /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, + } + engines: { node: '>=0.10.0' } dev: true /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: '>=8' } dev: true /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==, + } + engines: { node: '>=12' } dev: true /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, + } + engines: { node: '>=8' } dev: true /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + resolution: + { + integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, + } dev: true /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + resolution: + { + integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==, + } dev: true /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + resolution: + { + integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, + } dev: true /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, + } + engines: { node: '>=8.6' } dev: true /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + resolution: + { + integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==, + } dependencies: jsonc-parser: 3.2.1 mlly: 1.5.0 @@ -2121,8 +2924,11 @@ packages: dev: true /postcss@8.4.34: - resolution: {integrity: sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==, + } + engines: { node: ^10 || ^12 || >=14 } dependencies: nanoid: 3.3.7 picocolors: 1.0.0 @@ -2130,26 +2936,38 @@ packages: dev: true /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: '>= 0.8.0' } dev: true /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==, + } + engines: { node: '>=6.0.0' } dependencies: fast-diff: 1.3.0 dev: true /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, + } + engines: { node: '>=10.13.0' } hasBin: true dev: true /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==, + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 @@ -2157,16 +2975,25 @@ packages: dev: true /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: '>=6' } dev: true /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } dev: true /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + resolution: + { + integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==, + } peerDependencies: react: ^18.2.0 dependencies: @@ -2176,12 +3003,18 @@ packages: dev: false /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + resolution: + { + integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==, + } dev: true /react-router-dom@6.22.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==, + } + engines: { node: '>=14.0.0' } peerDependencies: react: '>=16.8' react-dom: '>=16.8' @@ -2193,8 +3026,11 @@ packages: dev: false /react-router@6.22.0(react@18.2.0): - resolution: {integrity: sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==, + } + engines: { node: '>=14.0.0' } peerDependencies: react: '>=16.8' dependencies: @@ -2203,14 +3039,20 @@ packages: dev: false /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==, + } + engines: { node: '>=0.10.0' } dependencies: loose-envify: 1.4.0 dev: false /remix-auth@3.6.0(@remix-run/react@2.6.0)(@remix-run/server-runtime@2.6.0): - resolution: {integrity: sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==} + resolution: + { + integrity: sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==, + } peerDependencies: '@remix-run/react': ^1.0.0 || ^2.0.0 '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 @@ -2221,33 +3063,48 @@ packages: dev: false /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: '>=4' } dev: true /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolution: + { + integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==, + } + engines: { iojs: '>=1.0.0', node: '>=0.10.0' } dev: true /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + resolution: + { + integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, + } hasBin: true dependencies: glob: 7.2.3 dev: true /rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==, + } + engines: { node: '>=14.18.0', npm: '>=8.0.0' } hasBin: true optionalDependencies: fsevents: 2.3.3 dev: true /rollup@4.9.6: - resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==, + } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true dependencies: '@types/estree': 1.0.5 @@ -2269,31 +3126,46 @@ packages: dev: true /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } dependencies: queue-microtask: 1.2.3 dev: true /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + resolution: + { + integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==, + } dependencies: loose-envify: 1.4.0 dev: false /semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==, + } + engines: { node: '>=10' } hasBin: true dependencies: lru-cache: 6.0.0 dev: true /set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + resolution: + { + integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==, + } /set-function-length@1.2.0: - resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==, + } + engines: { node: '>= 0.4' } dependencies: define-data-property: 1.1.1 function-bind: 1.1.2 @@ -2303,97 +3175,151 @@ packages: dev: true /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: '>=8' } dependencies: shebang-regex: 3.0.0 dev: true /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: '>=8' } dev: true /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, + } dev: true /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, + } + engines: { node: '>=14' } dev: true /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, + } + engines: { node: '>=8' } dev: true /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, + } + engines: { node: '>=0.10.0' } dev: true /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + resolution: + { + integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, + } dependencies: buffer-from: 1.1.2 source-map: 0.6.1 dev: true /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, + } + engines: { node: '>=0.10.0' } dev: true /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==, + } + engines: { node: '>= 8' } /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, + } dev: true /std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + resolution: + { + integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==, + } dev: true /stream-slice@0.1.2: - resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + resolution: + { + integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==, + } dev: true /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: '>=8' } dependencies: ansi-regex: 5.0.1 dev: true /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==, + } + engines: { node: '>=12' } dev: true /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: '>=8' } dev: true /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + resolution: + { + integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==, + } dependencies: acorn: 8.11.3 dev: true /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: '>=8' } dependencies: has-flag: 4.0.0 dev: true /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, + } + engines: { node: '>=8' } dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 @@ -2401,42 +3327,66 @@ packages: dev: true /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + resolution: + { + integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==, + } dev: true /thirty-two@1.0.2: - resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} - engines: {node: '>=0.2.6'} + resolution: + { + integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==, + } + engines: { node: '>=0.2.6' } dev: false /tiny-invariant@1.3.1: - resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + resolution: + { + integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==, + } dev: true /tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + resolution: + { + integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==, + } dev: true /tinypool@0.8.2: - resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==, + } + engines: { node: '>=14.0.0' } dev: true /tinyspy@2.2.0: - resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==, + } + engines: { node: '>=14.0.0' } dev: true /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: '>=8.0' } dependencies: is-number: 7.0.0 dev: true /tsconfck@3.0.1(typescript@5.3.3): - resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} - engines: {node: ^18 || >=20} + resolution: + { + integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==, + } + engines: { node: ^18 || >=20 } hasBin: true peerDependencies: typescript: ^5.0.0 @@ -2448,12 +3398,18 @@ packages: dev: true /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + resolution: + { + integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==, + } dev: true /tsutils@3.21.0(typescript@5.3.3): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==, + } + engines: { node: '>= 6' } peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: @@ -2462,43 +3418,67 @@ packages: dev: true /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: '>= 0.8.0' } dependencies: prelude-ls: 1.2.1 dev: true /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, + } + engines: { node: '>=4' } dev: true /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==, + } + engines: { node: '>=10' } dev: true /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==, + } + engines: { node: '>=14.17' } hasBin: true /ufo@1.3.2: - resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + resolution: + { + integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==, + } dev: true /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + resolution: + { + integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, + } dev: true /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } dependencies: punycode: 2.3.1 dev: true /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + resolution: + { + integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==, + } dependencies: inherits: 2.0.4 is-arguments: 1.1.1 @@ -2508,13 +3488,19 @@ packages: dev: true /uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + resolution: + { + integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==, + } hasBin: true dev: false /v8-to-istanbul@9.2.0: - resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} - engines: {node: '>=10.12.0'} + resolution: + { + integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==, + } + engines: { node: '>=10.12.0' } dependencies: '@jridgewell/trace-mapping': 0.3.22 '@types/istanbul-lib-coverage': 2.0.6 @@ -2522,8 +3508,11 @@ packages: dev: true /vite-node@1.2.2(@types/node@20.11.16): - resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true dependencies: cac: 6.7.14 @@ -2543,7 +3532,10 @@ packages: dev: true /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@4.5.2): - resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} + resolution: + { + integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==, + } peerDependencies: vite: '*' peerDependenciesMeta: @@ -2560,8 +3552,11 @@ packages: dev: true /vite@4.5.2(@types/node@20.11.16): - resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} - engines: {node: ^14.18.0 || >=16.0.0} + resolution: + { + integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==, + } + engines: { node: ^14.18.0 || >=16.0.0 } hasBin: true peerDependencies: '@types/node': '>= 14' @@ -2596,8 +3591,11 @@ packages: dev: true /vite@5.0.12(@types/node@20.11.16): - resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@types/node': ^18.0.0 || >=20.0.0 @@ -2632,8 +3630,11 @@ packages: dev: true /vitest@1.2.2(@types/node@20.11.16): - resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -2689,7 +3690,10 @@ packages: dev: true /web-encoding@1.1.5: - resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + resolution: + { + integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==, + } dependencies: util: 0.12.5 optionalDependencies: @@ -2697,13 +3701,19 @@ packages: dev: true /web-streams-polyfill@3.3.2: - resolution: {integrity: sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==, + } + engines: { node: '>= 8' } dev: true /which-typed-array@1.1.14: - resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==, + } + engines: { node: '>= 0.4' } dependencies: available-typed-arrays: 1.0.6 call-bind: 1.0.5 @@ -2713,16 +3723,22 @@ packages: dev: true /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: '>= 8' } hasBin: true dependencies: isexe: 2.0.0 dev: true /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==, + } + engines: { node: '>=8' } hasBin: true dependencies: siginfo: 2.0.0 @@ -2730,19 +3746,31 @@ packages: dev: true /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } dev: true /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, + } dev: true /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: '>=10' } dev: true /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==, + } + engines: { node: '>=12.20' } dev: true diff --git a/src/index.ts b/src/index.ts index a010040..860fe5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,6 +96,16 @@ export interface SendTOTPOptions { * The Magic Link URL. */ magicLink: string + + /** + * The request to generate the TOTP. + */ + request: Request + + /** + * The form data of the request. + */ + formData: FormData } /** @@ -316,7 +326,7 @@ export class TOTPStrategy extends Strategy { if (email) { // Generate and Send TOTP. const { code, hash, magicLink } = await this._generateTOTP({ email, request }) - await this.sendTOTP({ email, code, magicLink }) + await this.sendTOTP({ email, code, magicLink, request, formData }) const totpData: TOTPData = { hash, attempts: 0 } session.set(this.sessionEmailKey, email) diff --git a/test/index.spec.ts b/test/index.spec.ts index 31b1da6..2dfcc72 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -89,6 +89,8 @@ describe('[ TOTP ]', () => { sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { expect(options.email).toBe(DEFAULT_EMAIL) expect(options.code).to.not.equal('') + expect(options.request).toBeInstanceOf(Request) + expect(options.formData).toBeInstanceOf(FormData) }) const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() @@ -118,10 +120,51 @@ describe('[ TOTP ]', () => { expect(sendTOTP).toHaveBeenCalledTimes(1) }) + test('Should generate/send TOTP for form email with application form data.', async () => { + const APP_FORM_FIELD = 'via' + const APP_FORM_VALUE = 'whatsapp' + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + expect(options.request).toBeInstanceOf(Request) + expect(options.formData).toBeInstanceOf(FormData) + expect(options.formData.get(APP_FORM_FIELD)).toBe(APP_FORM_VALUE) + }) + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + formData.append(APP_FORM_FIELD, APP_FORM_VALUE) + const request = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(request, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + } else throw reason + }) + + expect(sendTOTP).toHaveBeenCalledTimes(1) + }) + test('Should generate/send TOTP for form email ignoring form totp code.', async () => { sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { expect(options.email).toBe(DEFAULT_EMAIL) expect(options.code).to.not.equal('') + expect(options.request).toBeInstanceOf(Request) + expect(options.formData).toBeInstanceOf(FormData) }) const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() @@ -219,6 +262,8 @@ describe('[ TOTP ]', () => { sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { expect(options.email).toBe(DEFAULT_EMAIL) expect(options.code).to.not.equal('') + expect(options.request).toBeInstanceOf(Request) + expect(options.formData).toBeInstanceOf(FormData) }) let session: Session | undefined let sessionTotp: unknown @@ -276,6 +321,75 @@ describe('[ TOTP ]', () => { expect(sendTOTP).toHaveBeenCalledTimes(2) }) + test('Should generate/send TOTP for application form data with session email.', async () => { + const APP_FORM_FIELD = 'via' + const APP_FORM_VALUE = 'whatsapp' + sendTOTP.mockImplementation(async (options: SendTOTPOptions) => { + expect(options.email).toBe(DEFAULT_EMAIL) + expect(options.code).to.not.equal('') + expect(options.request).toBeInstanceOf(Request) + expect(options.formData).toBeInstanceOf(FormData) + expect(options.formData.get(APP_FORM_FIELD)).toBe(APP_FORM_VALUE) + }) + let session: Session | undefined + let sessionTotp: unknown + const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) + const formData = new FormData() + formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL) + formData.append(APP_FORM_FIELD, APP_FORM_VALUE) + const requestToPopulateSessionEmail = new Request(`${HOST_URL}/login`, { + method: 'POST', + body: formData, + }) + await strategy + .authenticate(requestToPopulateSessionEmail, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/verify') + session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + sessionTotp = session.get(SESSION_KEYS.TOTP) + } else throw reason + }) + if (!session) throw new Error('Undefined session.') + const appFormData = new FormData() + appFormData.append(APP_FORM_FIELD, APP_FORM_VALUE) + const appFormRequest = new Request(`${HOST_URL}/login`, { + method: 'POST', + headers: { + cookie: await sessionStorage.commitSession(session), + }, + body: appFormData, + }) + await strategy + .authenticate(appFormRequest, sessionStorage, { + ...AUTH_OPTIONS, + successRedirect: '/verify', + failureRedirect: '/login', + }) + .catch(async (reason) => { + if (reason instanceof Response) { + expect(reason.status).toBe(302) + expect(reason.headers.get('location')).toBe('/verify') + const session = await sessionStorage.getSession( + reason.headers.get('set-cookie') ?? '', + ) + expect(session.get(SESSION_KEYS.EMAIL)).toBe(DEFAULT_EMAIL) + expect(session.get(SESSION_KEYS.TOTP)).toBeDefined() + expect(session.get(SESSION_KEYS.TOTP)).not.toEqual(sessionTotp) + } else throw reason + }) + expect(sendTOTP).toHaveBeenCalledTimes(2) + }) + test('Should failure redirect on invalid email.', async () => { const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify) const formData = new FormData() From ea3752dbfc7596c3b32ae8ccbb3d743dad4b7b77 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 12 Feb 2024 18:58:48 -0500 Subject: [PATCH 41/44] docs --- README.md | 5 ----- docs/customization.md | 24 +++++++++++++++++++++++- docs/migration.md | 5 ++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f79928e..2533826 100644 --- a/README.md +++ b/README.md @@ -185,11 +185,6 @@ authenticator.use( // ... }, async ({ email }) => { - // You can determine whether the user is authenticating - // via OTP code submission or Magic-Link URL and run your own logic. - if (form) console.log('Optional form submission logic.') - if (magicLink) console.log('Optional magic-link submission logic.') - // Get user from database. let user = await db.user.findFirst({ where: { email }, diff --git a/docs/customization.md b/docs/customization.md index 8652c37..849626d 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -148,6 +148,8 @@ export interface TOTPStrategyOptions { ## Cloudflare +### Remix v2 Compiler + To use on the Cloudflare runtime, you'll need to add the following to your `remix.config.js` file to specify the polyfills for a couple of node builtin modules. See the remix docs on [supportNodeBuiltinsPolyfill](https://remix.run/docs/en/main/file-conventions/remix-config#servernodebuiltinspolyfill). ### `remix.config.js` @@ -163,6 +165,26 @@ export default { } ``` +### Vite + +Enable [nodejs compatiblity](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) for Cloudflare in [wrangler.toml](https://developers.cloudflare.com/workers/runtime-apis/nodejs/#enable-nodejs-with-workers), [Cloudflare dashboard](https://developers.cloudflare.com/workers/runtime-apis/nodejs/#enable-nodejs-from-the-cloudflare-dashboard), and in the start script inside package.json + +```json +"scripts": { + "start": "wrangler pages dev ./build/client --compatibility-flags=nodejs_compat" +} +``` + +Ensure the `Buffer` global is set up before using `remix-auth-totp`. + +```ts +import { Buffer } from "node:buffer"; + +function setUpGlobals() { + globalThis.Buffer = Buffer +} +``` + ### Using Cloudflare KV for session storage ```ts @@ -187,7 +209,7 @@ authenticator.use( async ({ email }) => {}, ), ) -``` +```` ## Contributing diff --git a/docs/migration.md b/docs/migration.md index 8d4f079..649efb7 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -9,7 +9,7 @@ Remove `Totp` model from database if one exists. ### Implement `remix-auth-totp` API - Remove `createTOTP`, `readTOTP` and `updateTOTP` from `TOTPStrategy` options. -- Remove unneeded parameters from `sendTOTP` and `verify` functions +- Remove unneeded parameters from `verify` function ```ts authenticator.use( @@ -19,8 +19,7 @@ authenticator.use( // ❗`createTOTP`, `readTOTP` and `updateTOTP` are no longer needed (removed). - // Only email, code, and magicLink. Remove any other parameters. - sendTOTP: async ({ email, code, magicLink }) => {}, + sendTOTP: async ({ email }) => {}, }, // Only email. Remove any other parameters. async ({ email }) => {}, From 52caeee54a0d81d52288963bc7c636a224606f00 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 12 Feb 2024 19:08:28 -0500 Subject: [PATCH 42/44] docs --- docs/customization.md | 4 ++-- docs/migration.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 849626d..a7a1084 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -178,7 +178,7 @@ Enable [nodejs compatiblity](https://developers.cloudflare.com/workers/runtime-a Ensure the `Buffer` global is set up before using `remix-auth-totp`. ```ts -import { Buffer } from "node:buffer"; +import { Buffer } from 'node:buffer' function setUpGlobals() { globalThis.Buffer = Buffer @@ -209,7 +209,7 @@ authenticator.use( async ({ email }) => {}, ), ) -```` +``` ## Contributing diff --git a/docs/migration.md b/docs/migration.md index 649efb7..821882a 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -9,6 +9,7 @@ Remove `Totp` model from database if one exists. ### Implement `remix-auth-totp` API - Remove `createTOTP`, `readTOTP` and `updateTOTP` from `TOTPStrategy` options. +- Change `form` to `formData` if you are using it in `sendTOTP` - Remove unneeded parameters from `verify` function ```ts @@ -19,7 +20,8 @@ authenticator.use( // ❗`createTOTP`, `readTOTP` and `updateTOTP` are no longer needed (removed). - sendTOTP: async ({ email }) => {}, + // Change `form` to `formData` if you are using it. + sendTOTP: async ({ email, formData }) => {}, }, // Only email. Remove any other parameters. async ({ email }) => {}, From c7304ff56ecb555a1d0d88f1d715b7ba4c495b2c Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Tue, 13 Feb 2024 19:09:46 -0500 Subject: [PATCH 43/44] add formData and request to TOTPVerifyParams --- src/index.ts | 20 +++++++++++++++++++- test/index.spec.ts | 9 ++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 860fe5f..4f7a7a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -226,6 +226,16 @@ export interface TOTPVerifyParams { * The email address provided by the user. */ email: string + + /** + * The formData object from the Request. + */ + formData?: FormData + + /** + * The Request object. + */ + request: Request } export class TOTPStrategy extends Strategy { @@ -326,7 +336,13 @@ export class TOTPStrategy extends Strategy { if (email) { // Generate and Send TOTP. const { code, hash, magicLink } = await this._generateTOTP({ email, request }) - await this.sendTOTP({ email, code, magicLink, request, formData }) + await this.sendTOTP({ + email, + code, + magicLink, + formData, + request, + }) const totpData: TOTPData = { hash, attempts: 0 } session.set(this.sessionEmailKey, email) @@ -350,6 +366,8 @@ export class TOTPStrategy extends Strategy { const user = await this.verify({ email: sessionEmail, + formData: request.method === 'POST' ? formData : undefined, + request, }) session.set(options.sessionKey, user) diff --git a/test/index.spec.ts b/test/index.spec.ts index 2dfcc72..ce63b9e 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -539,7 +539,14 @@ describe('[ TOTP ]', () => { const strategy = new TOTPStrategy( { ...TOTP_STRATEGY_OPTIONS, ...totpStrategyOptions }, - async () => { + async ({ email, formData, request }) => { + expect(email).toBe(DEFAULT_EMAIL) + expect(request).toBeInstanceOf(Request) + if (request.method === 'POST') { + expect(formData).toBeInstanceOf(FormData) + } else { + expect(formData).not.toBeDefined() + } return Promise.resolve(user) }, ) From dc2d5148dc63a2a02af76ddb2cb269b73d858c59 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Tue, 13 Feb 2024 19:16:05 -0500 Subject: [PATCH 44/44] docs --- docs/migration.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/migration.md b/docs/migration.md index 821882a..358e8a1 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -9,8 +9,8 @@ Remove `Totp` model from database if one exists. ### Implement `remix-auth-totp` API - Remove `createTOTP`, `readTOTP` and `updateTOTP` from `TOTPStrategy` options. -- Change `form` to `formData` if you are using it in `sendTOTP` -- Remove unneeded parameters from `verify` function +- Change `form` to `formData` if you are using it in `sendTOTP` and `verify` functions +- Remove deprecated parameters from `verify` function ```ts authenticator.use( @@ -23,8 +23,9 @@ authenticator.use( // Change `form` to `formData` if you are using it. sendTOTP: async ({ email, formData }) => {}, }, - // Only email. Remove any other parameters. - async ({ email }) => {}, + // Remove deprecated parameters. + // Change `form` to `formData` if you are using it. + async ({ email, formData, request }) => {}, ), ) ```