From 6b909f033fcd3a5e02cf7d841bb298ec1666a5e4 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 24 Nov 2022 17:15:16 +0800 Subject: [PATCH] refactor(core): update the interaction guard (#2521) --- packages/core/package.json | 2 +- .../koa-interaction-body-guard.test.ts | 8 +- .../koa-session-sign-in-experience-guard.ts | 36 +-- ...oa-session-sign-inexperience-guard.test.ts | 83 ++---- .../src/routes/interaction/types/guard.ts | 47 +--- .../src/routes/interaction/types/index.ts | 5 +- .../sign-in-experience-valiation.test.ts | 257 ++++++++++++++++++ .../utils/sign-in-experience-validation.ts | 133 +++++++++ .../utils/verify-user-by-password.test.ts | 71 +---- .../utils/verify-user-by-password.ts | 28 +- .../identifier-verification.test.ts | 31 +-- .../verifications/identifier-verification.ts | 34 +-- packages/schemas/src/types/interactions.ts | 17 +- pnpm-lock.yaml | 251 ++++++++++++++++- 14 files changed, 713 insertions(+), 290 deletions(-) create mode 100644 packages/core/src/routes/interaction/utils/sign-in-experience-valiation.test.ts create mode 100644 packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts diff --git a/packages/core/package.json b/packages/core/package.json index 0cf613337..2eeff22e6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -99,7 +99,7 @@ "openapi-types": "^12.0.0", "prettier": "^2.7.1", "supertest": "^6.2.2", - "typescript": "^4.7.4" + "typescript": "^4.9.3" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts index d95917407..22bb956f8 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts @@ -56,7 +56,7 @@ describe('koaInteractionBodyGuard', () => { }); describe('identifier', () => { - it('invalid identifier should not parsed', async () => { + it('invalid identifier should throw', async () => { const ctx: WithGuardedIdentifierPayloadContext = { ...baseCtx, request: { @@ -64,15 +64,15 @@ describe('koaInteractionBodyGuard', () => { body: { event: 'sign-in', identifier: { - google: 'username', + username: 'username', + passcode: 'passcode', }, }, }, interactionPayload: {}, }; - await expect(koaInteractionBodyGuard()(ctx, next)).resolves.not.toThrow(); - expect(ctx.interactionPayload.identifier).not.toContain({ google: 'username' }); + await expect(koaInteractionBodyGuard()(ctx, next)).rejects.toThrow(); }); it.each(interactionMocks)('interaction methods should parse successfully', async (input) => { diff --git a/packages/core/src/routes/interaction/middleware/koa-session-sign-in-experience-guard.ts b/packages/core/src/routes/interaction/middleware/koa-session-sign-in-experience-guard.ts index 23525efcc..1ad26daa0 100644 --- a/packages/core/src/routes/interaction/middleware/koa-session-sign-in-experience-guard.ts +++ b/packages/core/src/routes/interaction/middleware/koa-session-sign-in-experience-guard.ts @@ -1,48 +1,40 @@ -import type { SignInExperience } from '@logto/schemas'; -import { SignInMode } from '@logto/schemas'; import type { MiddlewareType } from 'koa'; import type { IRouterParamContext } from 'koa-router'; import type { Provider } from 'oidc-provider'; -import RequestError from '#src/errors/RequestError/index.js'; import { getSignInExperienceForApplication } from '#src/lib/sign-in-experience/index.js'; -import assertThat from '#src/utils/assert-that.js'; +import { + signInModeValidation, + identifierValidation, + profileValidation, +} from '../utils/sign-in-experience-validation.js'; import type { WithGuardedIdentifierPayloadContext } from './koa-interaction-body-guard.js'; -const forbiddenEventError = new RequestError({ code: 'auth.forbidden', status: 403 }); - -export type WithSignInExperienceContext< - ContextT extends WithGuardedIdentifierPayloadContext -> = ContextT & { - signInExperience: SignInExperience; -}; - export default function koaSessionSignInExperienceGuard< StateT, ContextT extends WithGuardedIdentifierPayloadContext, ResponseBodyT ->( - provider: Provider -): MiddlewareType, ResponseBodyT> { +>(provider: Provider): MiddlewareType { return async (ctx, next) => { const interaction = await provider.interactionDetails(ctx.req, ctx.res); - const { event } = ctx.interactionPayload; + const { event, identifier, profile } = ctx.interactionPayload; const signInExperience = await getSignInExperienceForApplication( typeof interaction.params.client_id === 'string' ? interaction.params.client_id : undefined ); - // SignInMode validation - if (event === 'sign-in') { - assertThat(signInExperience.signInMode !== SignInMode.Register, forbiddenEventError); + if (event) { + signInModeValidation(event, signInExperience); } - if (event === 'register') { - assertThat(signInExperience.signInMode !== SignInMode.SignIn, forbiddenEventError); + if (identifier) { + identifierValidation(identifier, signInExperience); } - ctx.signInExperience = signInExperience; + if (profile) { + profileValidation(profile, signInExperience); + } return next(); }; diff --git a/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts b/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts index 633db6c19..7f80e7a27 100644 --- a/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts +++ b/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts @@ -1,14 +1,23 @@ -import { SignInMode } from '@logto/schemas'; import { Provider } from 'oidc-provider'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; -import { getSignInExperienceForApplication } from '#src/lib/sign-in-experience/index.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; +import { + signInModeValidation, + identifierValidation, + profileValidation, +} from '../utils/sign-in-experience-validation.js'; import koaSessionSignInExperienceGuard from './koa-session-sign-in-experience-guard.js'; jest.mock('#src/lib/sign-in-experience/index.js', () => ({ - getSignInExperienceForApplication: jest.fn(), + getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), +})); + +jest.mock('../utils/sign-in-experience-validation.js', () => ({ + signInModeValidation: jest.fn(), + identifierValidation: jest.fn(), + profileValidation: jest.fn(), })); jest.mock('oidc-provider', () => ({ @@ -18,62 +27,26 @@ jest.mock('oidc-provider', () => ({ })); describe('koaSessionSignInExperienceGuard', () => { - const getSignInExperienceForApplicationMock = getSignInExperienceForApplication as jest.Mock; const baseCtx = createContextWithRouteParameters(); const next = jest.fn(); - describe('sign-in mode guard', () => { - it('should reject register', async () => { - getSignInExperienceForApplicationMock.mockImplementationOnce(() => ({ - signInMode: SignInMode.SignIn, - })); + it('should call validation method properly', async () => { + const ctx = { + ...baseCtx, + interactionPayload: Object.freeze({ + event: 'register', + identifier: { username: 'username', password: 'password' }, + profile: { email: 'email' }, + }), + }; - const ctx = { - ...baseCtx, - interactionPayload: Object.freeze({ - event: 'register', - }), - signInExperience: mockSignInExperience, - }; + await koaSessionSignInExperienceGuard(new Provider(''))(ctx, next); - await expect(koaSessionSignInExperienceGuard(new Provider(''))(ctx, next)).rejects.toThrow(); - }); - - it('should reject sign-in', async () => { - getSignInExperienceForApplicationMock.mockImplementationOnce(() => ({ - signInMode: SignInMode.Register, - })); - - const ctx = { - ...baseCtx, - interactionPayload: Object.freeze({ - event: 'sign-in', - }), - signInExperience: mockSignInExperience, - }; - - await expect(koaSessionSignInExperienceGuard(new Provider(''))(ctx, next)).rejects.toThrow(); - }); - - it('should allow register', async () => { - getSignInExperienceForApplicationMock.mockImplementationOnce(() => ({ - signInMode: SignInMode.SignInAndRegister, - })); - - const ctx = { - ...baseCtx, - interactionPayload: Object.freeze({ - event: 'register', - }), - signInExperience: mockSignInExperience, - }; - - await expect( - koaSessionSignInExperienceGuard(new Provider(''))(ctx, next) - ).resolves.not.toThrow(); - expect(ctx.signInExperience).toEqual({ - signInMode: SignInMode.SignInAndRegister, - }); - }); + expect(signInModeValidation).toBeCalledWith('register', mockSignInExperience); + expect(identifierValidation).toBeCalledWith( + { username: 'username', password: 'password' }, + mockSignInExperience + ); + expect(profileValidation).toBeCalledWith({ email: 'email' }, mockSignInExperience); }); }); diff --git a/packages/core/src/routes/interaction/types/guard.ts b/packages/core/src/routes/interaction/types/guard.ts index d338d95a5..d413c187c 100644 --- a/packages/core/src/routes/interaction/types/guard.ts +++ b/packages/core/src/routes/interaction/types/guard.ts @@ -1,22 +1,4 @@ -import type { - UsernamePasswordPayload, - EmailPasscodePayload, - PhonePasswordPayload, - EmailPasswordPayload, - PhonePasscodePayload, - SocialConnectorPayload, -} from '@logto/schemas'; -import { - eventGuard, - profileGuard, - identifierGuard, - usernamePasswordPayloadGuard, - emailPasswordPayloadGuard, - phonePasswordPayloadGuard, - emailPasscodePayloadGuard, - phonePasscodePayloadGuard, - socialConnectorPayloadGuard, -} from '@logto/schemas'; +import { eventGuard, profileGuard, identifierGuard } from '@logto/schemas'; import { z } from 'zod'; export const interactionPayloadGuard = z.object({ @@ -26,29 +8,4 @@ export const interactionPayloadGuard = z.object({ }); export type InteractionPayload = z.infer; - -export const isUsernamePassword = ( - identifier: InteractionPayload['identifier'] -): identifier is UsernamePasswordPayload => - usernamePasswordPayloadGuard.safeParse(identifier).success; - -export const isEmailPassword = ( - identifier: InteractionPayload['identifier'] -): identifier is EmailPasswordPayload => emailPasswordPayloadGuard.safeParse(identifier).success; - -export const isPhonePassword = ( - identifier: InteractionPayload['identifier'] -): identifier is PhonePasswordPayload => phonePasswordPayloadGuard.safeParse(identifier).success; - -export const isEmailPasscode = ( - identifier: InteractionPayload['identifier'] -): identifier is EmailPasscodePayload => emailPasscodePayloadGuard.safeParse(identifier).success; - -export const isPhonePasscode = ( - identifier: InteractionPayload['identifier'] -): identifier is PhonePasscodePayload => phonePasscodePayloadGuard.safeParse(identifier).success; - -export const isSocialConnector = ( - identifier: InteractionPayload['identifier'] -): identifier is SocialConnectorPayload => - socialConnectorPayloadGuard.safeParse(identifier).success; +export type IdentifierPayload = z.infer; diff --git a/packages/core/src/routes/interaction/types/index.ts b/packages/core/src/routes/interaction/types/index.ts index 3cd2f8cd2..d04d3c0a9 100644 --- a/packages/core/src/routes/interaction/types/index.ts +++ b/packages/core/src/routes/interaction/types/index.ts @@ -2,7 +2,6 @@ import type { Context } from 'koa'; import type { IRouterParamContext } from 'koa-router'; import type { WithGuardedIdentifierPayloadContext } from '../middleware/koa-interaction-body-guard.js'; -import type { WithSignInExperienceContext } from '../middleware/koa-session-sign-in-experience-guard.js'; export type Identifier = | AccountIdIdentifier @@ -26,6 +25,4 @@ type UseInfo = { id: string; }; -export type InteractionContext = WithSignInExperienceContext< - WithGuardedIdentifierPayloadContext ->; +export type InteractionContext = WithGuardedIdentifierPayloadContext; diff --git a/packages/core/src/routes/interaction/utils/sign-in-experience-valiation.test.ts b/packages/core/src/routes/interaction/utils/sign-in-experience-valiation.test.ts new file mode 100644 index 000000000..12c739516 --- /dev/null +++ b/packages/core/src/routes/interaction/utils/sign-in-experience-valiation.test.ts @@ -0,0 +1,257 @@ +import type { SignInExperience } from '@logto/schemas'; +import { SignUpIdentifier, SignInIdentifier, SignInMode } from '@logto/schemas'; + +import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; + +import { signInModeValidation, identifierValidation } from './sign-in-experience-validation.js'; + +describe('signInModeValidation', () => { + it('register', () => { + expect(() => { + signInModeValidation('register', { signInMode: SignInMode.SignIn } as SignInExperience); + }).toThrow(); + expect(() => { + signInModeValidation('register', { signInMode: SignInMode.Register } as SignInExperience); + }).not.toThrow(); + expect(() => { + signInModeValidation('register', { + signInMode: SignInMode.SignInAndRegister, + } as SignInExperience); + }).not.toThrow(); + }); + + it('SignIn', () => { + expect(() => { + signInModeValidation('sign-in', { signInMode: SignInMode.SignIn } as SignInExperience); + }).not.toThrow(); + expect(() => { + signInModeValidation('sign-in', { signInMode: SignInMode.Register } as SignInExperience); + }).toThrow(); + expect(() => { + signInModeValidation('sign-in', { + signInMode: SignInMode.SignInAndRegister, + } as SignInExperience); + }).not.toThrow(); + }); + + it('forgot-password', () => { + expect(() => { + signInModeValidation('forgot-password', { + signInMode: SignInMode.SignIn, + } as SignInExperience); + }).not.toThrow(); + expect(() => { + signInModeValidation('forgot-password', { + signInMode: SignInMode.Register, + } as SignInExperience); + }).not.toThrow(); + expect(() => { + signInModeValidation('forgot-password', { + signInMode: SignInMode.SignInAndRegister, + } as SignInExperience); + }).not.toThrow(); + }); +}); + +describe('identifier validation', () => { + it('username-password', () => { + const identifier = { username: 'username', password: 'password' }; + + expect(() => { + identifierValidation(identifier, mockSignInExperience); + }).not.toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.filter( + ({ identifier }) => identifier !== SignInIdentifier.Username + ), + }, + }); + }).toThrow(); + }); + + it('email password', () => { + const identifier = { email: 'email', password: 'password' }; + + expect(() => { + identifierValidation(identifier, mockSignInExperience); + }).not.toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.filter( + ({ identifier }) => identifier !== SignInIdentifier.Email + ), + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Email, + password: false, + verificationCode: true, + isPasswordPrimary: true, + }, + ], + }, + }); + }).toThrow(); + }); + + it('email passcode', () => { + const identifier = { email: 'email', passcode: 'passcode' }; + + expect(() => { + identifierValidation(identifier, mockSignInExperience); + }).not.toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.filter( + ({ identifier }) => identifier !== SignInIdentifier.Email + ), + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Email, + password: true, + verificationCode: false, + isPasswordPrimary: true, + }, + ], + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signUp: { + identifier: SignUpIdentifier.Email, + password: false, + verify: true, + }, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Email, + password: true, + verificationCode: false, + isPasswordPrimary: true, + }, + ], + }, + }); + }).not.toThrow(); + }); + + it('phone password', () => { + const identifier = { phone: '123', password: 'password' }; + + expect(() => { + identifierValidation(identifier, mockSignInExperience); + }).not.toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.filter( + ({ identifier }) => identifier !== SignInIdentifier.Sms + ), + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Sms, + password: false, + verificationCode: true, + isPasswordPrimary: true, + }, + ], + }, + }); + }).toThrow(); + }); + + it('phone passcode', () => { + const identifier = { phone: '123456', passcode: 'passcode' }; + + expect(() => { + identifierValidation(identifier, mockSignInExperience); + }).not.toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: mockSignInExperience.signIn.methods.filter( + ({ identifier }) => identifier !== SignInIdentifier.Sms + ), + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Sms, + password: true, + verificationCode: false, + isPasswordPrimary: true, + }, + ], + }, + }); + }).toThrow(); + + expect(() => { + identifierValidation(identifier, { + ...mockSignInExperience, + signUp: { + identifier: SignUpIdentifier.Sms, + password: false, + verify: true, + }, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Sms, + password: true, + verificationCode: false, + isPasswordPrimary: true, + }, + ], + }, + }); + }).not.toThrow(); + }); +}); diff --git a/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts b/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts new file mode 100644 index 000000000..e0d507d35 --- /dev/null +++ b/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts @@ -0,0 +1,133 @@ +import type { Event, SignInExperience, Profile } from '@logto/schemas'; +import { SignUpIdentifier, SignInMode, SignInIdentifier } from '@logto/schemas'; + +import RequestError from '#src/errors/RequestError/index.js'; +import assertThat from '#src/utils/assert-that.js'; + +import type { IdentifierPayload } from '../types/guard.js'; + +const forbiddenEventError = new RequestError({ code: 'auth.forbidden', status: 403 }); + +const forbiddenIdentifierError = new RequestError({ + code: 'user.sign_in_method_not_enabled', + status: 422, +}); + +export const signInModeValidation = (event: Event, { signInMode }: SignInExperience) => { + if (event === 'sign-in') { + assertThat(signInMode !== SignInMode.Register, forbiddenEventError); + } + + if (event === 'register') { + assertThat(signInMode !== SignInMode.SignIn, forbiddenEventError); + } +}; + +export const identifierValidation = ( + identifier: IdentifierPayload, + signInExperience: SignInExperience +) => { + const { signIn, signUp } = signInExperience; + + // Username Password Identifier + if ('username' in identifier) { + assertThat( + signIn.methods.some( + ({ identifier: method, password }) => method === SignInIdentifier.Username && password + ), + forbiddenIdentifierError + ); + + return; + } + + // Email Identifier + if ('email' in identifier) { + assertThat( + // eslint-disable-next-line complexity + signIn.methods.some(({ identifier: method, password, verificationCode }) => { + if (method !== SignInIdentifier.Email) { + return false; + } + + // Email Password Verification + if ('password' in identifier && !password) { + return false; + } + + // Email Passcode Verification: SignIn verificationCode enabled or SignUp Email verify enabled + if ( + 'passcode' in identifier && + !verificationCode && + ![SignUpIdentifier.Email, SignUpIdentifier.EmailOrSms].includes(signUp.identifier) && + !signUp.verify + ) { + return false; + } + + return true; + }), + forbiddenIdentifierError + ); + + return; + } + + // Phone Identifier + if ('phone' in identifier) { + assertThat( + // eslint-disable-next-line complexity + signIn.methods.some(({ identifier: method, password, verificationCode }) => { + if (method !== SignInIdentifier.Sms) { + return false; + } + + // Phone Password Verification + if ('password' in identifier && !password) { + return false; + } + + // Phone Passcode Verification: SignIn verificationCode enabled or SignUp Email verify enabled + if ( + 'passcode' in identifier && + !verificationCode && + ![SignUpIdentifier.Sms, SignUpIdentifier.EmailOrSms].includes(signUp.identifier) && + !signUp.verify + ) { + return false; + } + + return true; + }), + forbiddenIdentifierError + ); + } + + // Social Identifier TODO: @darcy, @sijie +}; + +export const profileValidation = (profile: Profile, { signUp }: SignInExperience) => { + if (profile.phone) { + assertThat( + signUp.identifier === SignUpIdentifier.Sms || + signUp.identifier === SignUpIdentifier.EmailOrSms, + forbiddenIdentifierError + ); + } + + if (profile.email) { + assertThat( + signUp.identifier === SignUpIdentifier.Email || + signUp.identifier === SignUpIdentifier.EmailOrSms, + forbiddenIdentifierError + ); + } + + if (profile.username) { + assertThat(signUp.identifier === SignUpIdentifier.Username, forbiddenIdentifierError); + } + + if (profile.password) { + assertThat(signUp.password, forbiddenIdentifierError); + } +}; diff --git a/packages/core/src/routes/interaction/utils/verify-user-by-password.test.ts b/packages/core/src/routes/interaction/utils/verify-user-by-password.test.ts index 9938d2e65..417365325 100644 --- a/packages/core/src/routes/interaction/utils/verify-user-by-password.test.ts +++ b/packages/core/src/routes/interaction/utils/verify-user-by-password.test.ts @@ -1,8 +1,4 @@ -import { SignInIdentifier } from '@logto/schemas'; - -import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import { verifyUserPassword } from '#src/lib/user.js'; -import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import verifyUserByPassword from './verify-user-by-password.js'; @@ -12,57 +8,14 @@ jest.mock('#src/lib/user.js', () => ({ describe('verifyUserByPassword', () => { const findUser = jest.fn(); - const baseCtx = createContextWithRouteParameters(); const verifyUserPasswordMock = verifyUserPassword as jest.Mock; const mockUser = { id: 'mock_user', isSuspended: false }; - it('should throw if target sign-in method is not enabled', async () => { - const ctx = { - ...baseCtx, - interactionPayload: {}, - signInExperience: { - ...mockSignInExperience, - signIn: { - methods: mockSignInExperience.signIn.methods.filter( - ({ identifier }) => identifier === SignInIdentifier.Username - ), - }, - }, - }; - - await expect( - verifyUserByPassword(ctx, { - identifier: 'foo', - password: 'password', - findUser, - identifierType: SignInIdentifier.Email, - }) - ).rejects.toThrow(); - }); - it('should return userId', async () => { findUser.mockResolvedValueOnce(mockUser); verifyUserPasswordMock.mockResolvedValueOnce(mockUser); - const ctx = { - ...baseCtx, - interactionPayload: {}, - signInExperience: { - ...mockSignInExperience, - signIn: { - methods: mockSignInExperience.signIn.methods.filter( - ({ identifier }) => identifier === SignInIdentifier.Username - ), - }, - }, - }; - - const userId = await verifyUserByPassword(ctx, { - identifier: 'foo', - password: 'password', - findUser, - identifierType: SignInIdentifier.Username, - }); + const userId = await verifyUserByPassword('foo', 'password', findUser); expect(findUser).toBeCalledWith('foo'); expect(verifyUserPasswordMock).toBeCalledWith(mockUser, 'password'); @@ -76,27 +29,7 @@ describe('verifyUserByPassword', () => { isSuspended: true, }); - const ctx = { - ...baseCtx, - interactionPayload: {}, - signInExperience: { - ...mockSignInExperience, - signIn: { - methods: mockSignInExperience.signIn.methods.filter( - ({ identifier }) => identifier === SignInIdentifier.Username - ), - }, - }, - }; - - await expect( - verifyUserByPassword(ctx, { - identifier: 'foo', - password: 'password', - findUser, - identifierType: SignInIdentifier.Username, - }) - ).rejects.toThrow(); + await expect(verifyUserByPassword('foo', 'password', findUser)).rejects.toThrow(); expect(findUser).toBeCalledWith('foo'); expect(verifyUserPasswordMock).toBeCalledWith(mockUser, 'password'); diff --git a/packages/core/src/routes/interaction/utils/verify-user-by-password.ts b/packages/core/src/routes/interaction/utils/verify-user-by-password.ts index 2a5cc7b49..9b77cd714 100644 --- a/packages/core/src/routes/interaction/utils/verify-user-by-password.ts +++ b/packages/core/src/routes/interaction/utils/verify-user-by-password.ts @@ -1,35 +1,15 @@ -import type { SignInIdentifier, User } from '@logto/schemas'; +import type { User } from '@logto/schemas'; import type { Nullable } from '@silverhand/essentials'; import RequestError from '#src/errors/RequestError/index.js'; import { verifyUserPassword } from '#src/lib/user.js'; import assertThat from '#src/utils/assert-that.js'; -import type { InteractionContext } from '../types/index.js'; - -type Parameters = { - identifier: string; - password: string; - findUser: (identifier: string) => Promise>; - identifierType: SignInIdentifier; -}; - export default async function verifyUserByPassword( - ctx: InteractionContext, - { identifier, password, findUser, identifierType }: Parameters + identifier: string, + password: string, + findUser: (identifier: string) => Promise> ) { - const { signIn } = ctx.signInExperience; - - assertThat( - signIn.methods.some( - ({ identifier: method, password }) => method === identifierType && password - ), - new RequestError({ - code: 'user.sign_in_method_not_enabled', - status: 422, - }) - ); - const user = await findUser(identifier); const verifiedUser = await verifyUserPassword(user, password); const { isSuspended, id } = verifiedUser; diff --git a/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts b/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts index 5f980d560..a488f67c0 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts @@ -1,6 +1,3 @@ -import { SignInIdentifier } from '@logto/schemas'; - -import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import { findUserByUsername, findUserByEmail, findUserByPhone } from '#src/queries/user.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; @@ -15,12 +12,15 @@ describe('identifier verification', () => { const baseCtx = createContextWithRouteParameters(); const verifyUserByPasswordMock = verifyUserByPassword as jest.Mock; + afterEach(() => { + jest.clearAllMocks(); + }); + it('username password', async () => { verifyUserByPasswordMock.mockResolvedValueOnce('userId'); const ctx = { ...baseCtx, - signInExperience: mockSignInExperience, interactionPayload: Object.freeze({ event: 'sign-in', identifier: { @@ -32,12 +32,7 @@ describe('identifier verification', () => { const result = await identifierVerification(ctx); - expect(verifyUserByPasswordMock).toBeCalledWith(ctx, { - findUser: findUserByUsername, - identifier: 'username', - identifierType: SignInIdentifier.Username, - password: 'password', - }); + expect(verifyUserByPasswordMock).toBeCalledWith('username', 'password', findUserByUsername); expect(result).toEqual([{ key: 'accountId', value: 'userId' }]); }); @@ -46,7 +41,6 @@ describe('identifier verification', () => { const ctx = { ...baseCtx, - signInExperience: mockSignInExperience, interactionPayload: Object.freeze({ event: 'sign-in', identifier: { @@ -58,12 +52,7 @@ describe('identifier verification', () => { const result = await identifierVerification(ctx); - expect(verifyUserByPasswordMock).toBeCalledWith(ctx, { - findUser: findUserByEmail, - identifier: 'email', - identifierType: SignInIdentifier.Email, - password: 'password', - }); + expect(verifyUserByPasswordMock).toBeCalledWith('email', 'password', findUserByEmail); expect(result).toEqual([{ key: 'accountId', value: 'userId' }]); }); @@ -72,7 +61,6 @@ describe('identifier verification', () => { const ctx = { ...baseCtx, - signInExperience: mockSignInExperience, interactionPayload: Object.freeze({ event: 'sign-in', identifier: { @@ -84,12 +72,7 @@ describe('identifier verification', () => { const result = await identifierVerification(ctx); - expect(verifyUserByPasswordMock).toBeCalledWith(ctx, { - findUser: findUserByPhone, - identifier: '123456', - identifierType: SignInIdentifier.Sms, - password: 'password', - }); + expect(verifyUserByPasswordMock).toBeCalledWith('123456', 'password', findUserByPhone); expect(result).toEqual([{ key: 'accountId', value: 'userId' }]); }); }); diff --git a/packages/core/src/routes/interaction/verifications/identifier-verification.ts b/packages/core/src/routes/interaction/verifications/identifier-verification.ts index 6f088d6c1..bb3bd991be 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-verification.ts @@ -1,9 +1,6 @@ -import { SignInIdentifier } from '@logto/schemas'; - import RequestError from '#src/errors/RequestError/index.js'; import { findUserByEmail, findUserByPhone, findUserByUsername } from '#src/queries/user.js'; -import { isUsernamePassword, isPhonePassword, isEmailPassword } from '../types/guard.js'; import type { InteractionContext, Identifier } from '../types/index.js'; import { verifyUserByPassword } from '../utils/index.js'; @@ -12,41 +9,30 @@ export default async function identifierVerification( ): Promise { const { identifier } = ctx.interactionPayload; - if (isUsernamePassword(identifier)) { + if (!identifier) { + return []; + } + + if ('username' in identifier) { const { username, password } = identifier; - const accountId = await verifyUserByPassword(ctx, { - identifier: username, - password, - findUser: findUserByUsername, - identifierType: SignInIdentifier.Username, - }); + const accountId = await verifyUserByPassword(username, password, findUserByUsername); return [{ key: 'accountId', value: accountId }]; } - if (isPhonePassword(identifier)) { + if ('phone' in identifier && 'password' in identifier) { const { phone, password } = identifier; - const accountId = await verifyUserByPassword(ctx, { - identifier: phone, - password, - findUser: findUserByPhone, - identifierType: SignInIdentifier.Sms, - }); + const accountId = await verifyUserByPassword(phone, password, findUserByPhone); return [{ key: 'accountId', value: accountId }]; } - if (isEmailPassword(identifier)) { + if ('email' in identifier && 'password' in identifier) { const { email, password } = identifier; - const accountId = await verifyUserByPassword(ctx, { - identifier: email, - password, - findUser: findUserByEmail, - identifierType: SignInIdentifier.Email, - }); + const accountId = await verifyUserByPassword(email, password, findUserByEmail); return [{ key: 'accountId', value: accountId }]; } diff --git a/packages/schemas/src/types/interactions.ts b/packages/schemas/src/types/interactions.ts index fddbb2d6a..39375a117 100644 --- a/packages/schemas/src/types/interactions.ts +++ b/packages/schemas/src/types/interactions.ts @@ -52,15 +52,14 @@ export const eventGuard = z.union([ export type Event = z.infer; -export const identifierGuard = z.object({ - username: z.string().min(1).optional(), - email: z.string().min(1).optional(), - phone: z.string().min(1).optional(), - connectorId: z.string().min(1).optional(), - password: z.string().min(1).optional(), - passcode: z.string().min(1).optional(), - connectorData: z.unknown().optional(), -}); +export const identifierGuard = z.union([ + usernamePasswordPayloadGuard, + emailPasswordPayloadGuard, + phonePasswordPayloadGuard, + emailPasscodePayloadGuard, + phonePasscodePayloadGuard, + socialConnectorPayloadGuard, +]); export const profileGuard = z.object({ username: z.string().regex(usernameRegEx).optional(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52075baf6..011b1e826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -323,7 +323,7 @@ importers: snake-case: ^3.0.4 snakecase-keys: ^5.4.4 supertest: ^6.2.2 - typescript: ^4.7.4 + typescript: ^4.9.3 zod: ^3.19.1 dependencies: '@logto/cli': link:../cli @@ -372,9 +372,9 @@ importers: zod: 3.19.1 devDependencies: '@shopify/jest-koa-mocks': 5.0.1 - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/jest-config': 1.2.2_zapogttls25djihwjkusccjjym - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_xygfz6avl43ipur7dlp2av7gnm + '@silverhand/jest-config': 1.2.2_gxkpbehbojmgu22invxph4jlwq + '@silverhand/ts-config': 1.2.1_typescript@4.9.3 '@types/debug': 4.1.7 '@types/etag': 1.8.1 '@types/fs-extra': 9.0.13 @@ -401,7 +401,7 @@ importers: openapi-types: 12.0.0 prettier: 2.7.1 supertest: 6.2.2 - typescript: 4.7.4 + typescript: 4.9.3 packages/create: specifiers: @@ -3569,6 +3569,38 @@ packages: - typescript dev: true + /@silverhand/eslint-config/1.3.0_xygfz6avl43ipur7dlp2av7gnm: + resolution: {integrity: sha512-0+SXJXAkUe1pg2DNn3JCEo99Weev07chQsL2iSCramXeMKjEk1R1UKjgQJM9saUGF7ovY4hlE/JjFD3PFId4DQ==} + engines: {node: ^16.0.0 || ^18.0.0} + peerDependencies: + eslint: ^8.21.0 + prettier: ^2.7.1 + dependencies: + '@silverhand/eslint-plugin-fp': 2.5.0_eslint@8.21.0 + '@typescript-eslint/eslint-plugin': 5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq + '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + eslint: 8.21.0 + eslint-config-prettier: 8.5.0_eslint@8.21.0 + eslint-config-xo: 0.42.0_eslint@8.21.0 + eslint-config-xo-typescript: 0.53.0_6262kjopfp2ssqpmwkpdbrlzgu + eslint-import-resolver-typescript: 3.5.1_jatgrcxl4x7ywe7ak6cnjca2ae + eslint-plugin-consistent-default-export-name: 0.0.15 + eslint-plugin-eslint-comments: 3.2.0_eslint@8.21.0 + eslint-plugin-import: 2.26.0_7tkpoacjify653e7qftl64vwym + eslint-plugin-no-use-extend-native: 0.5.0 + eslint-plugin-node: 11.1.0_eslint@8.21.0 + eslint-plugin-prettier: 4.2.1_h62lvancfh4b7r6zn2dgodrh5e + eslint-plugin-promise: 6.1.0_eslint@8.21.0 + eslint-plugin-sql: 2.1.0_eslint@8.21.0 + eslint-plugin-unicorn: 43.0.2_eslint@8.21.0 + eslint-plugin-unused-imports: 2.0.0_kjyxfvacupbf4yx7sz4dzjz4we + prettier: 2.7.1 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - supports-color + - typescript + dev: true + /@silverhand/eslint-plugin-fp/2.5.0_eslint@8.21.0: resolution: {integrity: sha512-/oLO2Rs9nkhOk+rmC3PsWDvrDKrOfKuRtbSAwH4Scawn5GqAjo7ZXIZXj7RWa4nxLsCGc3ULvaVs1e1m4n6G/A==} engines: {node: '>=14.15.0'} @@ -3589,6 +3621,26 @@ packages: lodash.orderby: 4.6.0 lodash.pick: 4.4.0 + /@silverhand/jest-config/1.2.2_gxkpbehbojmgu22invxph4jlwq: + resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} + engines: {node: ^16.0.0 || ^18.0.0} + peerDependencies: + jest: ^29.0.0 || ^29.1.2 + dependencies: + '@jest/types': 29.1.2 + deepmerge: 4.2.2 + identity-obj-proxy: 3.0.0 + jest: 29.1.2_@types+node@16.11.12 + jest-matcher-specific-error: 1.0.0 + jest-transform-stub: 2.0.0 + ts-jest: 29.0.3_lr7fqxhx6o7ex6ma5v5npbw6ae + transitivePeerDependencies: + - '@babel/core' + - babel-jest + - esbuild + - typescript + dev: true + /@silverhand/jest-config/1.2.2_wkdujqsgbnfnnp5xidismkcn6e: resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} engines: {node: ^16.0.0 || ^18.0.0} @@ -3648,6 +3700,15 @@ packages: typescript: 4.7.4 dev: true + /@silverhand/ts-config/1.2.1_typescript@4.9.3: + resolution: {integrity: sha512-Lm5Ydb45qKmXvlOfQfSb+1WHrdL5IBtzt+AMOR5h528H073FLzaazLiaDo4noBVT9PAVtO7kG9qjwSPzHf0k9Q==} + engines: {node: ^16.0.0 || ^18.0.0} + peerDependencies: + typescript: ^4.7.4 + dependencies: + typescript: 4.9.3 + dev: true + /@sinclair/typebox/0.24.46: resolution: {integrity: sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==} dev: true @@ -4447,6 +4508,52 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin/5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq: + resolution: {integrity: sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==} + 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: + '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + '@typescript-eslint/scope-manager': 5.40.0 + '@typescript-eslint/type-utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + '@typescript-eslint/utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + debug: 4.3.4 + eslint: 8.21.0 + ignore: 5.2.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + resolution: {integrity: sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==} + 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.40.0 + '@typescript-eslint/types': 5.40.0 + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 + debug: 4.3.4 + eslint: 8.21.0 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: resolution: {integrity: sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4475,6 +4582,26 @@ packages: '@typescript-eslint/visitor-keys': 5.40.0 dev: true + /@typescript-eslint/type-utils/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + resolution: {integrity: sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 + '@typescript-eslint/utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + debug: 4.3.4 + eslint: 8.21.0 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: resolution: {integrity: sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4521,6 +4648,46 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree/5.40.0_typescript@4.9.3: + resolution: {integrity: sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.40.0 + '@typescript-eslint/visitor-keys': 5.40.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + resolution: {integrity: sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@typescript-eslint/scope-manager': 5.40.0 + '@typescript-eslint/types': 5.40.0 + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 + eslint: 8.21.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0_eslint@8.21.0 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: resolution: {integrity: sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6447,6 +6614,21 @@ packages: typescript: 4.7.4 dev: true + /eslint-config-xo-typescript/0.53.0_6262kjopfp2ssqpmwkpdbrlzgu: + resolution: {integrity: sha512-IJ1n70egMPTou/41HoGGFbLf/2WCsVW5lSUxOSklrR8T1221fMRPVJxIVZ3evr8R+N5wR6uzg/0uzSymwWA5Bg==} + engines: {node: '>=12'} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>=5.31.0' + '@typescript-eslint/parser': '>=5.31.0' + eslint: '>=8.0.0' + typescript: '>=4.4' + dependencies: + '@typescript-eslint/eslint-plugin': 5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq + '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + eslint: 8.21.0 + typescript: 4.9.3 + dev: true + /eslint-config-xo/0.42.0_eslint@8.21.0: resolution: {integrity: sha512-HIfd+AM6tHFoaZ/NXYDV3Mr/CJrAj/DoP6IOYt1/v+90XtCwVYOfW7LXbRDYDmhQMzT16h7eqPRcex72waRqdA==} engines: {node: '>=12'} @@ -6507,7 +6689,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i debug: 3.2.7 eslint: 8.21.0 eslint-import-resolver-node: 0.3.6 @@ -6556,7 +6738,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i array-includes: 3.1.5 array.prototype.flat: 1.3.0 debug: 2.6.9 @@ -6731,7 +6913,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.40.0_bomoubwgcm5gub6ncofkqpat4u + '@typescript-eslint/eslint-plugin': 5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq eslint: 8.21.0 eslint-rule-composer: 0.3.0 dev: true @@ -7584,6 +7766,7 @@ packages: /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} @@ -9939,7 +10122,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.9 /jsonparse/1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} @@ -14308,6 +14491,40 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest/29.0.3_lr7fqxhx6o7ex6ma5v5npbw6ae: + resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 || ^29.1.2 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@jest/types': 29.1.2 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.1.2_@types+node@16.11.12 + jest-util: 29.2.1 + json5: 2.2.1 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.3.8 + typescript: 4.9.3 + yargs-parser: 21.1.1 + dev: true + /ts-jest/29.0.3_o3wtcjdhyxuv43bggxcaucanwu: resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14442,6 +14659,16 @@ packages: typescript: 4.7.4 dev: true + /tsutils/3.21.0_typescript@4.9.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: 4.9.3 + dev: true + /tty-table/4.1.6: resolution: {integrity: sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==} engines: {node: '>=8.0.0'} @@ -14533,6 +14760,12 @@ packages: hasBin: true dev: true + /typescript/4.9.3: + resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /ua-parser-js/1.0.2: resolution: {integrity: sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==} dev: true