From 220ba58364c52ce21fce9332f8535e9e75bd6de5 Mon Sep 17 00:00:00 2001
From: Xiao Yijun <xiaoyijun@silverhand.io>
Date: Mon, 25 Jul 2022 17:39:09 +0800
Subject: [PATCH] refactor(test): user sign in (#1657)

---
 packages/integration-tests/package.json       |   2 +-
 .../src/client/dummy-storage.ts               |  23 ++
 .../src/client/logto-client.ts                |  20 ++
 .../integration-tests/src/logto-context.ts    | 111 ----------
 packages/integration-tests/src/ui-actions.ts  | 139 ++++++++++++
 .../tests/username-password-flow.test.ts      | 201 +++---------------
 pnpm-lock.yaml                                |  61 +++---
 7 files changed, 249 insertions(+), 308 deletions(-)
 create mode 100644 packages/integration-tests/src/client/dummy-storage.ts
 create mode 100644 packages/integration-tests/src/client/logto-client.ts
 delete mode 100644 packages/integration-tests/src/logto-context.ts
 create mode 100644 packages/integration-tests/src/ui-actions.ts

diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json
index fa56776c8..bb6f9b8b8 100644
--- a/packages/integration-tests/package.json
+++ b/packages/integration-tests/package.json
@@ -13,7 +13,7 @@
   },
   "devDependencies": {
     "@jest/types": "^27.5.1",
-    "@logto/js": "^0.2.0",
+    "@logto/node": "1.0.0-beta.0",
     "@logto/schemas": "^1.0.0-beta.1",
     "@peculiar/webcrypto": "^1.3.3",
     "@silverhand/eslint-config": "^0.17.0",
diff --git a/packages/integration-tests/src/client/dummy-storage.ts b/packages/integration-tests/src/client/dummy-storage.ts
new file mode 100644
index 000000000..87adf2cb1
--- /dev/null
+++ b/packages/integration-tests/src/client/dummy-storage.ts
@@ -0,0 +1,23 @@
+import { Storage, StorageKey } from '@logto/node';
+import { Nullable } from '@silverhand/essentials';
+
+export class DummyStorage implements Storage {
+  private storage: { [key in StorageKey]: Nullable<string> } = {
+    idToken: null,
+    refreshToken: null,
+    accessToken: null,
+    signInSession: null,
+  };
+
+  getItem(key: StorageKey): Nullable<string> {
+    return this.storage[key];
+  }
+
+  setItem(key: StorageKey, value: string): void {
+    this.storage[key] = value;
+  }
+
+  removeItem(key: StorageKey): void {
+    this.storage[key] = null;
+  }
+}
diff --git a/packages/integration-tests/src/client/logto-client.ts b/packages/integration-tests/src/client/logto-client.ts
new file mode 100644
index 000000000..c1d7a2344
--- /dev/null
+++ b/packages/integration-tests/src/client/logto-client.ts
@@ -0,0 +1,20 @@
+import BaseClient, { LogtoConfig } from '@logto/node';
+
+import { DummyStorage } from './dummy-storage';
+
+export default class LogtoClient extends BaseClient {
+  public navigateUrl = '';
+
+  constructor(config: LogtoConfig) {
+    super(
+      // Note: Disable persisting access token in integration tests
+      { ...config, persistAccessToken: false },
+      {
+        navigate: (url: string) => {
+          this.navigateUrl = url;
+        },
+        storage: new DummyStorage(),
+      }
+    );
+  }
+}
diff --git a/packages/integration-tests/src/logto-context.ts b/packages/integration-tests/src/logto-context.ts
deleted file mode 100644
index 4016d8f31..000000000
--- a/packages/integration-tests/src/logto-context.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { generateCodeVerifier, generateState, generateCodeChallenge } from '@logto/js';
-
-import { generatePassword, generateUsername } from './utils';
-
-type Account = {
-  username: string;
-  password: string;
-};
-
-type ContextData = {
-  account: Account;
-  codeVerifier: string;
-  codeChallenge: string;
-  state: string;
-  authorizationEndpoint: string;
-  tokenEndpoint: string;
-  authorizationCode: string;
-  interactionCookie: string;
-  nextRedirectTo: string;
-};
-
-type ContextDataKey = keyof ContextData;
-
-type ContextStore = {
-  getData: <T extends ContextDataKey>(key: T) => ContextData[T];
-  setData: <T extends ContextDataKey>(key: T, value: ContextData[T]) => void;
-};
-
-const createContextStore = (): ContextStore => {
-  const data: ContextData = {
-    account: { username: '', password: '' },
-    codeVerifier: '',
-    codeChallenge: '',
-    state: '',
-    interactionCookie: '',
-    authorizationCode: '',
-    authorizationEndpoint: '',
-    tokenEndpoint: '',
-    nextRedirectTo: '',
-  };
-
-  return {
-    getData: <T extends ContextDataKey>(key: T) => data[key],
-    setData: <T extends ContextDataKey>(key: T, value: ContextData[T]) => {
-      // eslint-disable-next-line @silverhand/fp/no-mutation
-      data[key] = value;
-    },
-  };
-};
-
-export class LogtoContext {
-  private readonly contextData: ContextStore = createContextStore();
-
-  public async init() {
-    const account = {
-      username: generateUsername(),
-      password: generatePassword(),
-    };
-    const codeVerifier = generateCodeVerifier();
-    const codeChallenge = await generateCodeChallenge(codeVerifier);
-
-    this.setData('account', account);
-    this.setData('codeVerifier', codeVerifier);
-    this.setData('codeChallenge', codeChallenge);
-    this.setData('state', generateState());
-  }
-
-  public get account(): Account {
-    return this.getData('account');
-  }
-
-  public get codeVerifier(): string {
-    return this.getData('codeVerifier');
-  }
-
-  public get codeChallenge(): string {
-    return this.getData('codeChallenge');
-  }
-
-  public get state(): string {
-    return this.getData('state');
-  }
-
-  public get authorizationCode(): string {
-    return this.getData('authorizationCode');
-  }
-
-  public get interactionCookie(): string {
-    return this.getData('interactionCookie');
-  }
-
-  public get authorizationEndpoint(): string {
-    return this.getData('authorizationEndpoint');
-  }
-
-  public get tokenEndpoint(): string {
-    return this.getData('tokenEndpoint');
-  }
-
-  public get nextRedirectTo(): string {
-    return this.getData('nextRedirectTo');
-  }
-
-  public setData<T extends ContextDataKey>(key: T, value: ContextData[T]): void {
-    this.contextData.setData(key, value);
-  }
-
-  private getData<T extends ContextDataKey>(key: T): ContextData[T] {
-    return this.contextData.getData(key);
-  }
-}
diff --git a/packages/integration-tests/src/ui-actions.ts b/packages/integration-tests/src/ui-actions.ts
new file mode 100644
index 000000000..92258d0cb
--- /dev/null
+++ b/packages/integration-tests/src/ui-actions.ts
@@ -0,0 +1,139 @@
+import { assert } from '@silverhand/essentials';
+import got from 'got/dist/source';
+
+import api from './api';
+import { logtoUrl } from './constants';
+import { extractCookie } from './utils';
+
+type RegisterResponse = {
+  redirectTo: string;
+};
+
+type SignInResponse = {
+  redirectTo: string;
+};
+
+type ConsentResponse = {
+  redirectTo: string;
+};
+
+export const visitSignInUri = async (signInUri: string) => {
+  const response = await got(signInUri, {
+    followRedirect: false,
+  });
+
+  // Note: After visit the sign in uri successfully, it will redirect the user to the ui sign in page.
+  assert(
+    response.statusCode === 303 && response.headers.location === '/sign-in',
+    new Error('Visit sign in uri failed')
+  );
+
+  const cookie = extractCookie(response);
+  assert(cookie, new Error('Get cookie from authorization endpoint failed'));
+
+  return cookie;
+};
+
+export const registerUserWithUsernameAndPassword = async (
+  username: string,
+  password: string,
+  interactionCookie: string
+) => {
+  const { redirectTo } = await api
+    .post('session/register/username-password', {
+      headers: {
+        cookie: interactionCookie,
+      },
+      json: {
+        username,
+        password,
+      },
+    })
+    .json<RegisterResponse>();
+
+  // Note: If register successfully, it will redirect the user to the auth endpoint.
+  assert(
+    redirectTo.startsWith(`${logtoUrl}/oidc/auth`),
+    new Error('Register with username and password failed')
+  );
+};
+
+export const signInWithUsernameAndPassword = async (
+  username: string,
+  password: string,
+  interactionCookie: string
+) => {
+  const { redirectTo: completeSignInActionUri } = await api
+    .post('session/sign-in/username-password', {
+      headers: {
+        cookie: interactionCookie,
+      },
+      json: {
+        username,
+        password,
+      },
+      followRedirect: false,
+    })
+    .json<SignInResponse>();
+
+  // Note: If sign in successfully, it will redirect the user to the auth endpoint
+  assert(
+    completeSignInActionUri.startsWith(`${logtoUrl}/oidc/auth`),
+    new Error('Sign in with username and password failed')
+  );
+
+  // Note: visit the completeSignInActionUri to get a new interaction cookie with session.
+  const completeSignInActionResponse = await got.get(completeSignInActionUri, {
+    headers: {
+      cookie: interactionCookie,
+    },
+    followRedirect: false,
+  });
+
+  // Note: If sign in action completed successfully, it will redirect the user to the consent page.
+  assert(
+    completeSignInActionResponse.statusCode === 303 &&
+      completeSignInActionResponse.headers.location === '/sign-in/consent',
+    new Error('Invoke auth before consent failed')
+  );
+
+  const cookieWithSession = extractCookie(completeSignInActionResponse);
+
+  // Note: If sign in action completed successfully, we will get `_session.sig` in the cookie.
+  assert(
+    Boolean(cookieWithSession) && cookieWithSession.includes('_session.sig'),
+    new Error('Invoke auth before consent failed')
+  );
+
+  return cookieWithSession;
+};
+
+export const consentUserAndGetSignInCallbackUri = async (interactionCookie: string) => {
+  const { redirectTo: completeAuthUri } = await api
+    .post('session/consent', {
+      headers: {
+        cookie: interactionCookie,
+      },
+      followRedirect: false,
+    })
+    .json<ConsentResponse>();
+
+  // Note: If consent successfully, it will redirect the user to the auth endpoint.
+  assert(completeAuthUri.startsWith(`${logtoUrl}/oidc/auth`), new Error('Consent failed'));
+
+  // Note: complete the auth process to get the sign in callback uri.
+  const authCodeResponse = await got.get(completeAuthUri, {
+    headers: {
+      cookie: interactionCookie,
+    },
+    followRedirect: false,
+  });
+
+  // Note: If complete auth successfully, it will redirect the user to the redirect uri.
+  assert(authCodeResponse.statusCode === 303, new Error('Complete auth failed'));
+
+  const signInCallbackUri = authCodeResponse.headers.location;
+  assert(signInCallbackUri, new Error('Get sign in callback uri failed'));
+
+  return signInCallbackUri;
+};
diff --git a/packages/integration-tests/tests/username-password-flow.test.ts b/packages/integration-tests/tests/username-password-flow.test.ts
index caebf10cc..d9de7243f 100644
--- a/packages/integration-tests/tests/username-password-flow.test.ts
+++ b/packages/integration-tests/tests/username-password-flow.test.ts
@@ -1,184 +1,49 @@
-import {
-  createRequester,
-  fetchOidcConfig,
-  fetchTokenByAuthorizationCode,
-  generateSignInUri,
-  verifyAndParseCodeFromCallbackUri,
-} from '@logto/js';
+import { LogtoConfig } from '@logto/node';
 import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
-import got from 'got/dist/source';
 
-import api from '@/api';
-
-import { discoveryUrl, logtoUrl, demoAppRedirectUri } from '../src/constants';
-import { LogtoContext } from '../src/logto-context';
-import { extractCookie } from '../src/utils';
+import LogtoClient from '@/client/logto-client';
+import { demoAppRedirectUri, logtoUrl } from '@/constants';
+import {
+  consentUserAndGetSignInCallbackUri,
+  registerUserWithUsernameAndPassword,
+  signInWithUsernameAndPassword,
+  visitSignInUri,
+} from '@/ui-actions';
+import { generatePassword, generateUsername } from '@/utils';
 
 describe('username and password flow', () => {
-  const logtoContext = new LogtoContext();
-
-  beforeAll(async () => {
-    await logtoContext.init();
-  });
-
-  it('should fetch OIDC configuration', async () => {
-    const oidcConfig = await fetchOidcConfig(discoveryUrl, createRequester());
-    const { authorizationEndpoint, tokenEndpoint } = oidcConfig;
-    expect(authorizationEndpoint).toBeTruthy();
-    expect(tokenEndpoint).toBeTruthy();
-
-    logtoContext.setData('authorizationEndpoint', authorizationEndpoint);
-    logtoContext.setData('tokenEndpoint', tokenEndpoint);
-  });
-
-  it('should visit authorization endpoint and get interaction cookie', async () => {
-    const signInUri = generateSignInUri({
-      authorizationEndpoint: logtoContext.authorizationEndpoint,
-      clientId: demoAppApplicationId,
-      redirectUri: demoAppRedirectUri,
-      codeChallenge: logtoContext.codeChallenge,
-      state: logtoContext.state,
-    });
-
-    const response = await got(signInUri, {
-      followRedirect: false,
-    });
-
-    // Note: this will redirect to the ui sign in page
-    expect(response.statusCode).toBe(303);
-    expect(response.headers.location).toBe('/sign-in');
-
-    const cookie = extractCookie(response);
-    expect(cookie).toBeTruthy();
-
-    logtoContext.setData('interactionCookie', cookie);
-  });
-
-  it('should register with username and password and redirect to oidc/auth endpoint to start an auth process', async () => {
-    type RegisterResponse = {
-      redirectTo: string;
+  it('should register and sign in with username and password successfully', async () => {
+    const logtoConfig: LogtoConfig = {
+      endpoint: logtoUrl,
+      appId: demoAppApplicationId,
+      persistAccessToken: false,
     };
 
-    const registerResponse = await api
-      .post('session/register/username-password', {
-        headers: {
-          cookie: logtoContext.interactionCookie,
-        },
-        json: logtoContext.account,
-      })
-      .json<RegisterResponse>();
+    const logtoClient = new LogtoClient(logtoConfig);
 
-    const { redirectTo: invokeAuthUrl } = registerResponse;
+    await logtoClient.signIn(demoAppRedirectUri);
 
-    expect(invokeAuthUrl.startsWith(`${logtoUrl}/oidc/auth`)).toBeTruthy();
-  });
+    expect(logtoClient.navigateUrl).toBeTruthy();
 
-  it('should sign in with username and password and redirect to oidc/auth endpoint to start an auth process', async () => {
-    type SignInResponse = {
-      redirectTo: string;
-    };
+    const interactionCookie = await visitSignInUri(logtoClient.navigateUrl);
 
-    const signInResponse = await api
-      .post('session/sign-in/username-password', {
-        headers: {
-          cookie: logtoContext.interactionCookie,
-        },
-        json: logtoContext.account,
-        followRedirect: false,
-      })
-      .json<SignInResponse>();
+    const username = generateUsername();
+    const password = generatePassword();
 
-    const { redirectTo: invokeAuthUrl } = signInResponse;
+    await registerUserWithUsernameAndPassword(username, password, interactionCookie);
 
-    expect(invokeAuthUrl.startsWith(`${logtoUrl}/oidc/auth`)).toBeTruthy();
-
-    logtoContext.setData('nextRedirectTo', invokeAuthUrl);
-  });
-
-  it('should invoke the auth process and redirect to the consent page with session cookie', async () => {
-    const invokeAuthUrl = logtoContext.nextRedirectTo;
-    const invokeAuthResponse = await got.get(invokeAuthUrl, {
-      headers: {
-        cookie: logtoContext.interactionCookie,
-      },
-      followRedirect: false,
-    });
-
-    // Note: Redirect to consent page
-    expect(invokeAuthResponse).toHaveProperty('statusCode', 303);
-    expect(invokeAuthResponse.headers.location).toBe('/sign-in/consent');
-
-    const cookie = extractCookie(invokeAuthResponse);
-    expect(cookie).toBeTruthy();
-    expect(cookie.includes('_session.sig')).toBeTruthy();
-
-    logtoContext.setData('interactionCookie', cookie);
-  });
-
-  it('should redirect to oidc/auth endpoint to complete the auth process after consent', async () => {
-    type ConsentResponse = {
-      redirectTo: string;
-    };
-
-    const consentResponse = await api
-      .post('session/consent', {
-        headers: {
-          cookie: logtoContext.interactionCookie,
-        },
-        followRedirect: false,
-      })
-      .json<ConsentResponse>();
-
-    const { redirectTo: completeAuthUrl } = consentResponse;
-
-    expect(completeAuthUrl.startsWith(`${logtoUrl}/oidc/auth`)).toBeTruthy();
-
-    logtoContext.setData('nextRedirectTo', completeAuthUrl);
-  });
-
-  it('should get the authorization code from the callback uri when the auth process is completed', async () => {
-    const completeAuthUrl = logtoContext.nextRedirectTo;
-    const authCodeResponse = await got.get(completeAuthUrl, {
-      headers: {
-        cookie: logtoContext.interactionCookie,
-      },
-    });
-
-    expect(authCodeResponse).toHaveProperty('statusCode', 200);
-    const callbackUri = authCodeResponse.redirectUrls[0];
-    expect(callbackUri).toBeTruthy();
-
-    if (!callbackUri) {
-      throw new Error('No redirect uri');
-    }
-
-    const authorizationCode = verifyAndParseCodeFromCallbackUri(
-      callbackUri,
-      demoAppRedirectUri,
-      logtoContext.state
-    );
-    expect(authorizationCode).toBeTruthy();
-
-    logtoContext.setData('authorizationCode', authorizationCode);
-  });
-
-  it('should fetch token by authorization code', async () => {
-    const token = await fetchTokenByAuthorizationCode(
-      {
-        clientId: demoAppApplicationId,
-        tokenEndpoint: logtoContext.tokenEndpoint,
-        redirectUri: demoAppRedirectUri,
-        codeVerifier: logtoContext.codeVerifier,
-        code: logtoContext.authorizationCode,
-      },
-      createRequester()
+    const interactionCookieWithSession = await signInWithUsernameAndPassword(
+      username,
+      password,
+      interactionCookie
     );
 
-    expect(token).toHaveProperty('accessToken');
-    expect(token).toHaveProperty('expiresIn');
-    expect(token).toHaveProperty('idToken');
-    expect(token).toHaveProperty('refreshToken');
-    expect(token).toHaveProperty('scope');
-    expect(token).toHaveProperty('tokenType');
+    const signInCallbackUri = await consentUserAndGetSignInCallbackUri(
+      interactionCookieWithSession
+    );
+
+    await logtoClient.handleSignInCallback(signInCallbackUri);
+
+    expect(logtoClient.isAuthenticated).toBeTruthy();
   });
 });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6f87e1e7c..0a04dec7e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1022,7 +1022,7 @@ importers:
   packages/integration-tests:
     specifiers:
       '@jest/types': ^27.5.1
-      '@logto/js': ^0.2.0
+      '@logto/node': 1.0.0-beta.0
       '@logto/schemas': ^1.0.0-beta.1
       '@peculiar/webcrypto': ^1.3.3
       '@silverhand/eslint-config': ^0.17.0
@@ -1043,7 +1043,7 @@ importers:
       typescript: ^4.6.4
     devDependencies:
       '@jest/types': 27.5.1
-      '@logto/js': 0.2.0
+      '@logto/node': 1.0.0-beta.0
       '@logto/schemas': link:../schemas
       '@peculiar/webcrypto': 1.3.3
       '@silverhand/eslint-config': 0.17.0_odhppvbqvvm7sc3xnvl7b6rwuy
@@ -1869,7 +1869,6 @@ packages:
     engines: {node: '>=12'}
     dependencies:
       '@jridgewell/trace-mapping': 0.3.9
-    dev: true
 
   /@eslint/eslintrc/1.3.0:
     resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
@@ -2032,7 +2031,6 @@ packages:
       - supports-color
       - ts-node
       - utf-8-validate
-    dev: true
 
   /@jest/environment/27.5.1:
     resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==}
@@ -2177,7 +2175,6 @@ packages:
     dependencies:
       '@jridgewell/resolve-uri': 3.0.5
       '@jridgewell/sourcemap-codec': 1.4.11
-    dev: true
 
   /@koa/cors/3.1.0:
     resolution: {integrity: sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==}
@@ -2969,14 +2966,15 @@ packages:
       superstruct: 0.16.0
     dev: true
 
-  /@logto/js/0.2.0:
-    resolution: {integrity: sha512-hKbx0pqN8hPGB/G4/OTnmcuKk+MlU9xkGTbXzc4pSCvuFq4eMEOeQ8uOaGxoetisD7dZ36lMkfAZ57cFlcwE8w==}
+  /@logto/client/1.0.0-beta.0:
+    resolution: {integrity: sha512-FHprVzEATJuRxdsSPxOD+1lpRcpgmnfA3JKHI4o1yce7yXrcqYx1nAw/4acW1vxLv67NQ0IQf/U2RAPWSpdj4g==}
     dependencies:
+      '@logto/js': 1.0.0-beta.0
       '@silverhand/essentials': 1.1.7
       camelcase-keys: 7.0.2
       jose: 4.6.0
-      js-base64: 3.7.2
       lodash.get: 4.4.2
+      lodash.once: 4.1.1
       superstruct: 0.16.0
     dev: true
 
@@ -2991,6 +2989,27 @@ packages:
       superstruct: 0.16.0
     dev: true
 
+  /@logto/js/1.0.0-beta.0:
+    resolution: {integrity: sha512-IYgfcs6Pc+fx2Y84kFyzAI1lNBT8NmJVZnP6Xuii6xBc1eTQFLpxffLPEB3/CtgXuyzlGyL5ZM9BO8mnhWYWAQ==}
+    dependencies:
+      '@silverhand/essentials': 1.1.7
+      camelcase-keys: 7.0.2
+      jose: 4.6.0
+      lodash.get: 4.4.2
+      superstruct: 0.16.0
+    dev: true
+
+  /@logto/node/1.0.0-beta.0:
+    resolution: {integrity: sha512-wQLBp8xslgqFMG7vm+/+2drkGlLql91hB2eElx9M6+iJj/OI2c8didSy6eltcYKgM5Wp4xH8peF2XX6RpOOyxQ==}
+    dependencies:
+      '@logto/client': 1.0.0-beta.0
+      '@silverhand/essentials': 1.1.7
+      js-base64: 3.7.2
+      node-fetch: 2.6.7
+    transitivePeerDependencies:
+      - encoding
+    dev: true
+
   /@logto/react/1.0.0-alpha.2_react@17.0.2:
     resolution: {integrity: sha512-52nFUrOrMuGJFY9eJG0BIawJ3pn72HvhEg4Of3/Jtrr3d1THYSIg1wEdKfNjfbuABJ85qxdi/H2HD2rn5Q6TdQ==}
     peerDependencies:
@@ -4497,7 +4516,7 @@ packages:
       '@jest/types': 27.5.1
       deepmerge: 4.2.2
       identity-obj-proxy: 3.0.0
-      jest: 27.5.1
+      jest: 27.5.1_ts-node@10.9.1
       jest-matcher-specific-error: 1.0.0
       jest-transform-stub: 2.0.0
       ts-jest: 27.1.1_53ggqi2i4rbcfjtktmjua6zili
@@ -4548,6 +4567,7 @@ packages:
       - babel-jest
       - esbuild
       - typescript
+    dev: false
 
   /@silverhand/jest-config/0.17.0_u4suh3umvg724wu2nufptihvny:
     resolution: {integrity: sha512-Syb9S2Hqbg3UcuGq+qByv+SJKWC6jrFSQp6JeGbsqLWlGTDjXX3QpHMaq5p9XYldlns6ZT6QQ7Q19CtbqUFU+A==}
@@ -4568,6 +4588,7 @@ packages:
       - babel-jest
       - esbuild
       - typescript
+    dev: false
 
   /@silverhand/ts-config-react/0.17.0_typescript@4.6.2:
     resolution: {integrity: sha512-vsZMcWVzaQT2Qb2b46jbmbcXPHxEKz3snQzQhHHV4IRJbLWnTtswOnNwfPRdXsW7/xcYfF+EM9Bw4Q7Cmib2nA==}
@@ -4844,19 +4865,15 @@ packages:
 
   /@tsconfig/node10/1.0.8:
     resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
-    dev: true
 
   /@tsconfig/node12/1.0.9:
     resolution: {integrity: sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==}
-    dev: true
 
   /@tsconfig/node14/1.0.1:
     resolution: {integrity: sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==}
-    dev: true
 
   /@tsconfig/node16/1.0.2:
     resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==}
-    dev: true
 
   /@types/accepts/1.3.5:
     resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
@@ -5160,7 +5177,6 @@ packages:
 
   /@types/node/16.11.12:
     resolution: {integrity: sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==}
-    dev: true
 
   /@types/node/17.0.23:
     resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==}
@@ -5801,7 +5817,6 @@ packages:
   /acorn-walk/8.2.0:
     resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
     engines: {node: '>=0.4.0'}
-    dev: true
 
   /acorn/7.4.1:
     resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
@@ -5817,7 +5832,6 @@ packages:
     resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==}
     engines: {node: '>=0.4.0'}
     hasBin: true
-    dev: true
 
   /add-stream/1.0.0:
     resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
@@ -5961,7 +5975,6 @@ packages:
 
   /arg/4.1.3:
     resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
-    dev: true
 
   /argon2/0.28.5:
     resolution: {integrity: sha512-kGFCctzc3VWmR1aCOYjNgvoTmVF5uVBUtWlXCKKO54d1K+31zRz45KAcDIqMo2746ozv/52d25nfEekitaXP0w==}
@@ -6976,7 +6989,6 @@ packages:
 
   /create-require/1.1.1:
     resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
-    dev: true
 
   /cross-env/7.0.3:
     resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
@@ -7324,7 +7336,6 @@ packages:
   /diff/4.0.2:
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
     engines: {node: '>=0.3.1'}
-    dev: true
 
   /diff/5.0.0:
     resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
@@ -7733,7 +7744,7 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.14.0_cm24urgmefwiki325v54og4qfa
+      '@typescript-eslint/parser': 5.14.0_g4cxuhevh5o54harssx6h7xjim
       debug: 3.2.7
       eslint-import-resolver-node: 0.3.6
       eslint-import-resolver-typescript: 2.5.0_p6hsegxeddyw6tkhd66xuhpt6y
@@ -7784,7 +7795,7 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.14.0_cm24urgmefwiki325v54og4qfa
+      '@typescript-eslint/parser': 5.14.0_g4cxuhevh5o54harssx6h7xjim
       array-includes: 3.1.4
       array.prototype.flat: 1.2.5
       debug: 2.6.9
@@ -9743,7 +9754,6 @@ packages:
       - supports-color
       - ts-node
       - utf-8-validate
-    dev: true
 
   /jest-config/27.5.1:
     resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==}
@@ -9823,7 +9833,6 @@ packages:
       - canvas
       - supports-color
       - utf-8-validate
-    dev: true
 
   /jest-diff/27.5.1:
     resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==}
@@ -10195,7 +10204,6 @@ packages:
       - supports-color
       - ts-node
       - utf-8-validate
-    dev: true
 
   /jose/4.6.0:
     resolution: {integrity: sha512-0hNAkhMBNi4soKSAX4zYOFV+aqJlEz/4j4fregvasJzEVtjDChvWqRjPvHwLqr5hx28Ayr6bsOs1Kuj87V0O8w==}
@@ -15040,7 +15048,7 @@ packages:
       '@types/jest': 27.4.1
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
-      jest: 27.5.1
+      jest: 27.5.1_ts-node@10.9.1
       jest-util: 27.5.1
       json5: 2.2.1
       lodash.memoize: 4.1.2
@@ -15209,7 +15217,6 @@ packages:
       typescript: 4.6.4
       v8-compile-cache-lib: 3.0.1
       yn: 3.1.1
-    dev: true
 
   /ts-node/10.9.1_mtczhn2fdutewshpiexgzmf2mq:
     resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
@@ -15703,7 +15710,6 @@ packages:
 
   /v8-compile-cache-lib/3.0.1:
     resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
-    dev: true
 
   /v8-compile-cache/2.3.0:
     resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
@@ -16075,7 +16081,6 @@ packages:
   /yn/3.1.1:
     resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
     engines: {node: '>=6'}
-    dev: true
 
   /yocto-queue/0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}