0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(connector): google one tap

This commit is contained in:
Gao Sun 2024-06-16 13:55:36 +08:00
parent dc6fbe212e
commit 6308ee1857
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
10 changed files with 269 additions and 83 deletions

View file

@ -0,0 +1,9 @@
---
"@logto/connector-kit": major
---
remove `.catchall()` for `connectorMetadataGuard`
`.catchall()` allows unknown keys to be parsed as metadata. This is troublesome when we want to strip out unknown keys (Zod provides `.strip()` for this purpose but somehow it doesn't work with `.catchall()`).
For data extensibility, we added `customData` field to `ConnectorMetadata` type to store unknown keys. For example, the `fromEmail` field in `connector-logto-email` is not part of the standard metadata, so it should be stored in `customData` in the future.

View file

@ -0,0 +1,9 @@
---
"@logto/connector-google": minor
"@logto/connector-kit": minor
---
support Google One Tap
- support parsing and validating Google One Tap data in `connector-google`
- add Google connector constants in `connector-kit` for reuse

View file

@ -7,6 +7,7 @@
"@logto/connector-kit": "workspace:^3.0.0", "@logto/connector-kit": "workspace:^3.0.0",
"@silverhand/essentials": "^2.9.1", "@silverhand/essentials": "^2.9.1",
"got": "^14.0.0", "got": "^14.0.0",
"jose": "^5.0.0",
"snakecase-keys": "^8.0.0", "snakecase-keys": "^8.0.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },

View file

@ -1,5 +1,9 @@
import type { ConnectorMetadata } from '@logto/connector-kit'; import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit'; import {
ConnectorConfigFormItemType,
ConnectorPlatform,
GoogleConnector,
} from '@logto/connector-kit';
export const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth'; export const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
export const accessTokenEndpoint = 'https://oauth2.googleapis.com/token'; export const accessTokenEndpoint = 'https://oauth2.googleapis.com/token';
@ -7,8 +11,8 @@ export const userInfoEndpoint = 'https://openidconnect.googleapis.com/v1/userinf
export const scope = 'openid profile email'; export const scope = 'openid profile email';
export const defaultMetadata: ConnectorMetadata = { export const defaultMetadata: ConnectorMetadata = {
id: 'google-universal', id: GoogleConnector.factoryId,
target: 'google', target: GoogleConnector.target,
platform: ConnectorPlatform.Universal, platform: ConnectorPlatform.Universal,
name: { name: {
en: 'Google', en: 'Google',
@ -53,3 +57,6 @@ export const defaultMetadata: ConnectorMetadata = {
}; };
export const defaultTimeout = 5000; export const defaultTimeout = 5000;
// https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
export const jwksUri = 'https://www.googleapis.com/oauth2/v3/certs';

View file

@ -11,6 +11,7 @@ import type {
GetConnectorConfig, GetConnectorConfig,
CreateConnector, CreateConnector,
SocialConnector, SocialConnector,
GoogleConnectorConfig,
} from '@logto/connector-kit'; } from '@logto/connector-kit';
import { import {
ConnectorError, ConnectorError,
@ -18,7 +19,9 @@ import {
validateConfig, validateConfig,
ConnectorType, ConnectorType,
parseJson, parseJson,
GoogleConnector,
} from '@logto/connector-kit'; } from '@logto/connector-kit';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { import {
accessTokenEndpoint, accessTokenEndpoint,
@ -27,20 +30,20 @@ import {
userInfoEndpoint, userInfoEndpoint,
defaultMetadata, defaultMetadata,
defaultTimeout, defaultTimeout,
jwksUri,
} from './constant.js'; } from './constant.js';
import type { GoogleConfig } from './types.js';
import { import {
googleConfigGuard,
accessTokenResponseGuard, accessTokenResponseGuard,
userInfoResponseGuard, userInfoResponseGuard,
authResponseGuard, authResponseGuard,
googleOneTapDataGuard,
} from './types.js'; } from './types.js';
const getAuthorizationUri = const getAuthorizationUri =
(getConfig: GetConnectorConfig): GetAuthorizationUri => (getConfig: GetConnectorConfig): GetAuthorizationUri =>
async ({ state, redirectUri }) => { async ({ state, redirectUri }) => {
const config = await getConfig(defaultMetadata.id); const config = await getConfig(defaultMetadata.id);
validateConfig(config, googleConfigGuard); validateConfig(config, GoogleConnector.configGuard);
const queryParameters = new URLSearchParams({ const queryParameters = new URLSearchParams({
client_id: config.clientId, client_id: config.clientId,
@ -54,7 +57,7 @@ const getAuthorizationUri =
}; };
export const getAccessToken = async ( export const getAccessToken = async (
config: GoogleConfig, config: GoogleConnectorConfig,
codeObject: { code: string; redirectUri: string } codeObject: { code: string; redirectUri: string }
) => { ) => {
const { code, redirectUri } = codeObject; const { code, redirectUri } = codeObject;
@ -86,22 +89,58 @@ export const getAccessToken = async (
return { accessToken }; return { accessToken };
}; };
const getUserInfo = type Json = ReturnType<typeof parseJson>;
(getConfig: GetConnectorConfig): GetUserInfo =>
async (data) => { /**
* Get user information JSON from Google Identity Platform. It will use the following order to
* retrieve user information:
*
* 1. Google One Tap: https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
* 2. Normal Google OAuth: https://developers.google.com/identity/protocols/oauth2/openid-connect
*
* @param data The data from the client.
* @param config The configuration of the connector.
* @returns A Promise that resolves to the user information JSON.
*/
const getUserInfoJson = async (data: unknown, config: GoogleConnectorConfig): Promise<Json> => {
// Google One Tap
const oneTapResult = googleOneTapDataGuard.safeParse(data);
if (oneTapResult.success) {
const { payload } = await jwtVerify<Json>(
oneTapResult.data.credential,
createRemoteJWKSet(new URL(jwksUri)),
{
// https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
issuer: ['https://accounts.google.com', 'accounts.google.com'],
audience: config.clientId,
clockTolerance: 10,
}
);
return payload;
}
// Normal Google OAuth
const { code, redirectUri } = await authorizationCallbackHandler(data); const { code, redirectUri } = await authorizationCallbackHandler(data);
const config = await getConfig(defaultMetadata.id);
validateConfig(config, googleConfigGuard);
const { accessToken } = await getAccessToken(config, { code, redirectUri }); const { accessToken } = await getAccessToken(config, { code, redirectUri });
try {
const httpResponse = await got.post(userInfoEndpoint, { const httpResponse = await got.post(userInfoEndpoint, {
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`,
}, },
timeout: { request: defaultTimeout }, timeout: { request: defaultTimeout },
}); });
const rawData = parseJson(httpResponse.body); return parseJson(httpResponse.body);
};
const getUserInfo =
(getConfig: GetConnectorConfig): GetUserInfo =>
async (data) => {
const config = await getConfig(defaultMetadata.id);
validateConfig(config, GoogleConnector.configGuard);
try {
const rawData = await getUserInfoJson(data, config);
const result = userInfoResponseGuard.safeParse(rawData); const result = userInfoResponseGuard.safeParse(rawData);
if (!result.success) { if (!result.success) {
@ -150,7 +189,7 @@ const createGoogleConnector: CreateConnector<SocialConnector> = async ({ getConf
return { return {
metadata: defaultMetadata, metadata: defaultMetadata,
type: ConnectorType.Social, type: ConnectorType.Social,
configGuard: googleConfigGuard, configGuard: GoogleConnector.configGuard,
getAuthorizationUri: getAuthorizationUri(getConfig), getAuthorizationUri: getAuthorizationUri(getConfig),
getUserInfo: getUserInfo(getConfig), getUserInfo: getUserInfo(getConfig),
}; };

View file

@ -1,12 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
export const googleConfigGuard = z.object({ import { GoogleConnector } from '@logto/connector-kit';
clientId: z.string(),
clientSecret: z.string(),
scope: z.string().optional(),
});
export type GoogleConfig = z.infer<typeof googleConfigGuard>;
export const accessTokenResponseGuard = z.object({ export const accessTokenResponseGuard = z.object({
access_token: z.string(), access_token: z.string(),
@ -33,3 +27,11 @@ export const authResponseGuard = z.object({
code: z.string(), code: z.string(),
redirectUri: z.string(), redirectUri: z.string(),
}); });
/**
* Response payload from Google One Tap. Note the CSRF token is not included since it should be
* verified by the web server.
*/
export const googleOneTapDataGuard = z.object({
[GoogleConnector.oneTapParams.credential]: z.string(),
});

View file

@ -1,4 +1,4 @@
import type { ZodType } from 'zod'; import type { ZodType, z } from 'zod';
import { type ConnectorMetadata } from './metadata.js'; import { type ConnectorMetadata } from './metadata.js';
@ -17,3 +17,7 @@ export type BaseConnector<Type extends ConnectorType> = {
metadata: ConnectorMetadata; metadata: ConnectorMetadata;
configGuard: ZodType; configGuard: ZodType;
}; };
export type ToZodObject<T> = z.ZodObject<{
[K in keyof T]-?: z.ZodType<T[K]>;
}>;

View file

@ -1,9 +1,11 @@
import type { LanguageTag } from '@logto/language-kit'; import type { LanguageTag } from '@logto/language-kit';
import { isLanguageTag } from '@logto/language-kit'; import { isLanguageTag } from '@logto/language-kit';
import { type Nullable } from '@silverhand/essentials';
import type { ZodType } from 'zod'; import type { ZodType } from 'zod';
import { z } from 'zod'; import { z } from 'zod';
import { connectorConfigFormItemGuard } from './config-form.js'; import { connectorConfigFormItemGuard } from './config-form.js';
import { type ToZodObject } from './foundation.js';
export enum ConnectorPlatform { export enum ConnectorPlatform {
Native = 'Native', Native = 'Native',
@ -34,12 +36,32 @@ export type I18nPhrases = { en: string } & {
[K in Exclude<LanguageTag, 'en'>]?: string; [K in Exclude<LanguageTag, 'en'>]?: string;
}; };
export type SocialConnectorMetadata = {
platform: Nullable<ConnectorPlatform>;
isStandard?: boolean;
};
export const socialConnectorMetadataGuard = z.object({ export const socialConnectorMetadataGuard = z.object({
// Social connector platform. TODO: @darcyYe considering remove the nullable and make all the social connector field optional // Social connector platform. TODO: @darcyYe considering remove the nullable and make all the social connector field optional
platform: z.nativeEnum(ConnectorPlatform).nullable(), platform: z.nativeEnum(ConnectorPlatform).nullable(),
// Indicates custom connector that follows standard protocol. Currently supported standard connectors are OIDC, OAuth2, and SAML2 // Indicates custom connector that follows standard protocol. Currently supported standard connectors are OIDC, OAuth2, and SAML2
isStandard: z.boolean().optional(), isStandard: z.boolean().optional(),
}); }) satisfies ToZodObject<SocialConnectorMetadata>;
export type ConnectorMetadata = {
id: string;
target: string;
name: I18nPhrases;
description: I18nPhrases;
logo: string;
logoDark: Nullable<string>;
readme: string;
configTemplate?: string;
formItems?: Array<z.infer<typeof connectorConfigFormItemGuard>>;
customData?: Record<string, unknown>;
/** @deprecated Use `customData` instead. */
fromEmail?: string;
} & SocialConnectorMetadata;
export const connectorMetadataGuard = z export const connectorMetadataGuard = z
.object({ .object({
@ -57,11 +79,10 @@ export const connectorMetadataGuard = z
readme: z.string(), readme: z.string(),
configTemplate: z.string().optional(), // Connector config template configTemplate: z.string().optional(), // Connector config template
formItems: connectorConfigFormItemGuard.array().optional(), formItems: connectorConfigFormItemGuard.array().optional(),
customData: z.record(z.unknown()).optional(),
fromEmail: z.string().optional(),
}) })
.merge(socialConnectorMetadataGuard) .merge(socialConnectorMetadataGuard) satisfies ToZodObject<ConnectorMetadata>;
.catchall(z.unknown());
export type ConnectorMetadata = z.infer<typeof connectorMetadataGuard>;
// Configurable connector metadata guard. Stored in DB metadata field // Configurable connector metadata guard. Stored in DB metadata field
export const configurableConnectorMetadataGuard = connectorMetadataGuard export const configurableConnectorMetadataGuard = connectorMetadataGuard

View file

@ -2,7 +2,7 @@
import { type Json } from '@withtyped/server'; import { type Json } from '@withtyped/server';
import { z } from 'zod'; import { z } from 'zod';
import { type BaseConnector, type ConnectorType } from './foundation.js'; import { type ToZodObject, type BaseConnector, type ConnectorType } from './foundation.js';
// This type definition is for SAML connector // This type definition is for SAML connector
export type ValidateSamlAssertion = ( export type ValidateSamlAssertion = (
@ -80,3 +80,44 @@ export type SocialConnector = BaseConnector<ConnectorType.Social> & {
getUserInfo: GetUserInfo; getUserInfo: GetUserInfo;
validateSamlAssertion?: ValidateSamlAssertion; validateSamlAssertion?: ValidateSamlAssertion;
}; };
export type GoogleOneTapConfig = {
isEnabled?: boolean;
autoSelect?: boolean;
closeOnTapOutside?: boolean;
itpSupport?: boolean;
};
export const googleOneTapConfigGuard = z.object({
isEnabled: z.boolean().optional(),
autoSelect: z.boolean().optional(),
closeOnTapOutside: z.boolean().optional(),
itpSupport: z.boolean().optional(),
}) satisfies ToZodObject<GoogleOneTapConfig>;
/** An object that contains the configuration for the official Google connector. */
export const GoogleConnector = Object.freeze({
/** The target of Google connectors. */
target: 'google',
/** The factory ID of the official Google connector. */
factoryId: 'google-universal',
oneTapParams: Object.freeze({
/** The parameter Google One Tap uses to prevent CSRF attacks. */
csrfToken: 'g_csrf_token',
/** The parameter Google One Tap uses to carry the ID token. */
credential: 'credential',
}),
configGuard: z.object({
clientId: z.string(),
clientSecret: z.string(),
scope: z.string().optional(),
oneTap: googleOneTapConfigGuard.optional(),
}) satisfies ToZodObject<GoogleConnectorConfig>,
});
export type GoogleConnectorConfig = {
clientId: string;
clientSecret: string;
scope?: string;
oneTap?: GoogleOneTapConfig;
};

View file

@ -1142,6 +1142,9 @@ importers:
got: got:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.0 version: 14.0.0
jose:
specifier: ^5.0.0
version: 5.2.4
snakecase-keys: snakecase-keys:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0 version: 8.0.0
@ -3415,7 +3418,7 @@ importers:
version: 8.57.0 version: 8.57.0
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@20.10.4) version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
jest-matcher-specific-error: jest-matcher-specific-error:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -3667,7 +3670,7 @@ importers:
version: 3.0.0 version: 3.0.0
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@20.12.7) version: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
jest-environment-jsdom: jest-environment-jsdom:
specifier: ^29.0.0 specifier: ^29.0.0
version: 29.2.2 version: 29.2.2
@ -3676,7 +3679,7 @@ importers:
version: 2.0.0 version: 2.0.0
jest-transformer-svg: jest-transformer-svg:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0) version: 2.0.0(jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)))(react@18.2.0)
js-base64: js-base64:
specifier: ^3.7.5 specifier: ^3.7.5
version: 3.7.5 version: 3.7.5
@ -3815,7 +3818,7 @@ importers:
version: 10.0.0 version: 10.0.0
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@20.10.4) version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
jest-matcher-specific-error: jest-matcher-specific-error:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -14482,6 +14485,41 @@ snapshots:
- supports-color - supports-color
- ts-node - ts-node
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.12.7
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
jest-resolve: 29.7.0
jest-resolve-dependencies: 29.7.0
jest-runner: 29.7.0
jest-runtime: 29.7.0
jest-snapshot: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
jest-watcher: 29.7.0
micromatch: 4.0.5
pretty-format: 29.7.0
slash: 3.0.0
strip-ansi: 6.0.1
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
- ts-node
'@jest/create-cache-key-function@27.5.1': '@jest/create-cache-key-function@27.5.1':
dependencies: dependencies:
'@jest/types': 27.5.1 '@jest/types': 27.5.1
@ -14755,7 +14793,7 @@ snapshots:
'@logto/js': 4.1.1 '@logto/js': 4.1.1
'@silverhand/essentials': 2.9.1 '@silverhand/essentials': 2.9.1
camelcase-keys: 7.0.2 camelcase-keys: 7.0.2
jose: 5.2.2 jose: 5.2.4
'@logto/cloud@0.2.5-a7eedce(zod@3.22.4)': '@logto/cloud@0.2.5-a7eedce(zod@3.22.4)':
dependencies: dependencies:
@ -17852,13 +17890,13 @@ snapshots:
dependencies: dependencies:
lodash.get: 4.4.2 lodash.get: 4.4.2
create-jest@29.7.0(@types/node@20.10.4): create-jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)):
dependencies: dependencies:
'@jest/types': 29.6.3 '@jest/types': 29.6.3
chalk: 4.1.2 chalk: 4.1.2
exit: 0.1.2 exit: 0.1.2
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@20.10.4) jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
jest-util: 29.7.0 jest-util: 29.7.0
prompts: 2.4.2 prompts: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
@ -19994,35 +20032,16 @@ snapshots:
- babel-plugin-macros - babel-plugin-macros
- supports-color - supports-color
jest-cli@29.7.0(@types/node@20.10.4): jest-cli@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)):
dependencies: dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
'@jest/test-result': 29.7.0 '@jest/test-result': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
chalk: 4.1.2 chalk: 4.1.2
create-jest: 29.7.0(@types/node@20.10.4) create-jest: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
exit: 0.1.2 exit: 0.1.2
import-local: 3.1.0 import-local: 3.1.0
jest-config: 29.7.0(@types/node@20.10.4) jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest-cli@29.7.0(@types/node@20.12.7):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
jest-util: 29.7.0 jest-util: 29.7.0
jest-validate: 29.7.0 jest-validate: 29.7.0
yargs: 17.7.2 yargs: 17.7.2
@ -20051,7 +20070,7 @@ snapshots:
- supports-color - supports-color
- ts-node - ts-node
jest-config@29.7.0(@types/node@20.10.4): jest-config@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)):
dependencies: dependencies:
'@babel/core': 7.24.4 '@babel/core': 7.24.4
'@jest/test-sequencer': 29.7.0 '@jest/test-sequencer': 29.7.0
@ -20077,6 +20096,7 @@ snapshots:
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
optionalDependencies: optionalDependencies:
'@types/node': 20.10.4 '@types/node': 20.10.4
ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3)
transitivePeerDependencies: transitivePeerDependencies:
- babel-plugin-macros - babel-plugin-macros
- supports-color - supports-color
@ -20112,6 +20132,37 @@ snapshots:
- babel-plugin-macros - babel-plugin-macros
- supports-color - supports-color
jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)):
dependencies:
'@babel/core': 7.24.4
'@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.24.4)
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.11
jest-circus: 29.7.0
jest-environment-node: 29.7.0
jest-get-type: 29.6.3
jest-regex-util: 29.6.3
jest-resolve: 29.7.0
jest-runner: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 20.12.7
ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
jest-dev-server@10.0.0: jest-dev-server@10.0.0:
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
@ -20386,11 +20437,6 @@ snapshots:
jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) jest: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
react: 18.2.0 react: 18.2.0
jest-transformer-svg@2.0.0(jest@29.7.0(@types/node@20.12.7))(react@18.2.0):
dependencies:
jest: 29.7.0(@types/node@20.12.7)
react: 18.2.0
jest-util@29.5.0: jest-util@29.5.0:
dependencies: dependencies:
'@jest/types': 29.6.3 '@jest/types': 29.6.3
@ -20443,24 +20489,12 @@ snapshots:
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1
jest@29.7.0(@types/node@20.10.4): jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3)):
dependencies: dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3)) '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
'@jest/types': 29.6.3 '@jest/types': 29.6.3
import-local: 3.1.0 import-local: 3.1.0
jest-cli: 29.7.0(@types/node@20.10.4) jest-cli: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest@29.7.0(@types/node@20.12.7):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.3.3))
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@20.12.7)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- babel-plugin-macros - babel-plugin-macros
@ -23680,6 +23714,25 @@ snapshots:
optionalDependencies: optionalDependencies:
'@swc/core': 1.3.52(@swc/helpers@0.5.1) '@swc/core': 1.3.52(@swc/helpers@0.5.1)
ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 20.10.4
acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.3.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optional: true
tsconfig-paths@3.15.0: tsconfig-paths@3.15.0:
dependencies: dependencies:
'@types/json5': 0.0.29 '@types/json5': 0.0.29