diff --git a/.changeset/breezy-bags-help.md b/.changeset/breezy-bags-help.md new file mode 100644 index 000000000..596b1deba --- /dev/null +++ b/.changeset/breezy-bags-help.md @@ -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. diff --git a/.changeset/cold-masks-film.md b/.changeset/cold-masks-film.md new file mode 100644 index 000000000..557b5d27d --- /dev/null +++ b/.changeset/cold-masks-film.md @@ -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 diff --git a/packages/connectors/connector-google/package.json b/packages/connectors/connector-google/package.json index 06479432d..204d0e9e1 100644 --- a/packages/connectors/connector-google/package.json +++ b/packages/connectors/connector-google/package.json @@ -7,6 +7,7 @@ "@logto/connector-kit": "workspace:^3.0.0", "@silverhand/essentials": "^2.9.1", "got": "^14.0.0", + "jose": "^5.0.0", "snakecase-keys": "^8.0.0", "zod": "^3.22.4" }, diff --git a/packages/connectors/connector-google/src/constant.ts b/packages/connectors/connector-google/src/constant.ts index 9ff63ada2..28fd8a05e 100644 --- a/packages/connectors/connector-google/src/constant.ts +++ b/packages/connectors/connector-google/src/constant.ts @@ -1,5 +1,9 @@ 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 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 defaultMetadata: ConnectorMetadata = { - id: 'google-universal', - target: 'google', + id: GoogleConnector.factoryId, + target: GoogleConnector.target, platform: ConnectorPlatform.Universal, name: { en: 'Google', @@ -53,3 +57,6 @@ export const defaultMetadata: ConnectorMetadata = { }; 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'; diff --git a/packages/connectors/connector-google/src/index.ts b/packages/connectors/connector-google/src/index.ts index 19c8fd9a2..9a08f2f5d 100644 --- a/packages/connectors/connector-google/src/index.ts +++ b/packages/connectors/connector-google/src/index.ts @@ -11,6 +11,7 @@ import type { GetConnectorConfig, CreateConnector, SocialConnector, + GoogleConnectorConfig, } from '@logto/connector-kit'; import { ConnectorError, @@ -18,7 +19,9 @@ import { validateConfig, ConnectorType, parseJson, + GoogleConnector, } from '@logto/connector-kit'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; import { accessTokenEndpoint, @@ -27,20 +30,20 @@ import { userInfoEndpoint, defaultMetadata, defaultTimeout, + jwksUri, } from './constant.js'; -import type { GoogleConfig } from './types.js'; import { - googleConfigGuard, accessTokenResponseGuard, userInfoResponseGuard, authResponseGuard, + googleOneTapDataGuard, } from './types.js'; const getAuthorizationUri = (getConfig: GetConnectorConfig): GetAuthorizationUri => async ({ state, redirectUri }) => { const config = await getConfig(defaultMetadata.id); - validateConfig(config, googleConfigGuard); + validateConfig(config, GoogleConnector.configGuard); const queryParameters = new URLSearchParams({ client_id: config.clientId, @@ -54,7 +57,7 @@ const getAuthorizationUri = }; export const getAccessToken = async ( - config: GoogleConfig, + config: GoogleConnectorConfig, codeObject: { code: string; redirectUri: string } ) => { const { code, redirectUri } = codeObject; @@ -86,22 +89,58 @@ export const getAccessToken = async ( return { accessToken }; }; +type Json = ReturnType; + +/** + * 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 => { + // Google One Tap + const oneTapResult = googleOneTapDataGuard.safeParse(data); + + if (oneTapResult.success) { + const { payload } = await jwtVerify( + 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 { accessToken } = await getAccessToken(config, { code, redirectUri }); + + const httpResponse = await got.post(userInfoEndpoint, { + headers: { + authorization: `Bearer ${accessToken}`, + }, + timeout: { request: defaultTimeout }, + }); + return parseJson(httpResponse.body); +}; + const getUserInfo = (getConfig: GetConnectorConfig): GetUserInfo => async (data) => { - const { code, redirectUri } = await authorizationCallbackHandler(data); const config = await getConfig(defaultMetadata.id); - validateConfig(config, googleConfigGuard); - const { accessToken } = await getAccessToken(config, { code, redirectUri }); + validateConfig(config, GoogleConnector.configGuard); try { - const httpResponse = await got.post(userInfoEndpoint, { - headers: { - authorization: `Bearer ${accessToken}`, - }, - timeout: { request: defaultTimeout }, - }); - const rawData = parseJson(httpResponse.body); + const rawData = await getUserInfoJson(data, config); const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { @@ -150,7 +189,7 @@ const createGoogleConnector: CreateConnector = async ({ getConf return { metadata: defaultMetadata, type: ConnectorType.Social, - configGuard: googleConfigGuard, + configGuard: GoogleConnector.configGuard, getAuthorizationUri: getAuthorizationUri(getConfig), getUserInfo: getUserInfo(getConfig), }; diff --git a/packages/connectors/connector-google/src/types.ts b/packages/connectors/connector-google/src/types.ts index 5837e568e..8141627bb 100644 --- a/packages/connectors/connector-google/src/types.ts +++ b/packages/connectors/connector-google/src/types.ts @@ -1,12 +1,6 @@ import { z } from 'zod'; -export const googleConfigGuard = z.object({ - clientId: z.string(), - clientSecret: z.string(), - scope: z.string().optional(), -}); - -export type GoogleConfig = z.infer; +import { GoogleConnector } from '@logto/connector-kit'; export const accessTokenResponseGuard = z.object({ access_token: z.string(), @@ -33,3 +27,11 @@ export const authResponseGuard = z.object({ code: 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(), +}); diff --git a/packages/toolkit/connector-kit/src/types/foundation.ts b/packages/toolkit/connector-kit/src/types/foundation.ts index 5692bb36a..2819fbaa8 100644 --- a/packages/toolkit/connector-kit/src/types/foundation.ts +++ b/packages/toolkit/connector-kit/src/types/foundation.ts @@ -1,4 +1,4 @@ -import type { ZodType } from 'zod'; +import type { ZodType, z } from 'zod'; import { type ConnectorMetadata } from './metadata.js'; @@ -17,3 +17,7 @@ export type BaseConnector = { metadata: ConnectorMetadata; configGuard: ZodType; }; + +export type ToZodObject = z.ZodObject<{ + [K in keyof T]-?: z.ZodType; +}>; diff --git a/packages/toolkit/connector-kit/src/types/metadata.ts b/packages/toolkit/connector-kit/src/types/metadata.ts index 7eddebf16..50890cbc4 100644 --- a/packages/toolkit/connector-kit/src/types/metadata.ts +++ b/packages/toolkit/connector-kit/src/types/metadata.ts @@ -1,9 +1,11 @@ import type { LanguageTag } from '@logto/language-kit'; import { isLanguageTag } from '@logto/language-kit'; +import { type Nullable } from '@silverhand/essentials'; import type { ZodType } from 'zod'; import { z } from 'zod'; import { connectorConfigFormItemGuard } from './config-form.js'; +import { type ToZodObject } from './foundation.js'; export enum ConnectorPlatform { Native = 'Native', @@ -34,12 +36,32 @@ export type I18nPhrases = { en: string } & { [K in Exclude]?: string; }; +export type SocialConnectorMetadata = { + platform: Nullable; + isStandard?: boolean; +}; + export const socialConnectorMetadataGuard = z.object({ // Social connector platform. TODO: @darcyYe considering remove the nullable and make all the social connector field optional platform: z.nativeEnum(ConnectorPlatform).nullable(), // Indicates custom connector that follows standard protocol. Currently supported standard connectors are OIDC, OAuth2, and SAML2 isStandard: z.boolean().optional(), -}); +}) satisfies ToZodObject; + +export type ConnectorMetadata = { + id: string; + target: string; + name: I18nPhrases; + description: I18nPhrases; + logo: string; + logoDark: Nullable; + readme: string; + configTemplate?: string; + formItems?: Array>; + customData?: Record; + /** @deprecated Use `customData` instead. */ + fromEmail?: string; +} & SocialConnectorMetadata; export const connectorMetadataGuard = z .object({ @@ -57,11 +79,10 @@ export const connectorMetadataGuard = z readme: z.string(), configTemplate: z.string().optional(), // Connector config template formItems: connectorConfigFormItemGuard.array().optional(), + customData: z.record(z.unknown()).optional(), + fromEmail: z.string().optional(), }) - .merge(socialConnectorMetadataGuard) - .catchall(z.unknown()); - -export type ConnectorMetadata = z.infer; + .merge(socialConnectorMetadataGuard) satisfies ToZodObject; // Configurable connector metadata guard. Stored in DB metadata field export const configurableConnectorMetadataGuard = connectorMetadataGuard diff --git a/packages/toolkit/connector-kit/src/types/social.ts b/packages/toolkit/connector-kit/src/types/social.ts index 71f2008e6..b13b0300a 100644 --- a/packages/toolkit/connector-kit/src/types/social.ts +++ b/packages/toolkit/connector-kit/src/types/social.ts @@ -2,7 +2,7 @@ import { type Json } from '@withtyped/server'; 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 export type ValidateSamlAssertion = ( @@ -80,3 +80,44 @@ export type SocialConnector = BaseConnector & { getUserInfo: GetUserInfo; 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; + +/** 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, +}); + +export type GoogleConnectorConfig = { + clientId: string; + clientSecret: string; + scope?: string; + oneTap?: GoogleOneTapConfig; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 515209c67..6d1d1c752 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1142,6 +1142,9 @@ importers: got: specifier: ^14.0.0 version: 14.0.0 + jose: + specifier: ^5.0.0 + version: 5.2.4 snakecase-keys: specifier: ^8.0.0 version: 8.0.0 @@ -3415,7 +3418,7 @@ importers: version: 8.57.0 jest: 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: specifier: ^1.0.0 version: 1.0.0 @@ -3667,7 +3670,7 @@ importers: version: 3.0.0 jest: 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: specifier: ^29.0.0 version: 29.2.2 @@ -3676,7 +3679,7 @@ importers: version: 2.0.0 jest-transformer-svg: 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: specifier: ^3.7.5 version: 3.7.5 @@ -3815,7 +3818,7 @@ importers: version: 10.0.0 jest: 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: specifier: ^1.0.0 version: 1.0.0 @@ -14482,6 +14485,41 @@ snapshots: - supports-color - 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': dependencies: '@jest/types': 27.5.1 @@ -14755,7 +14793,7 @@ snapshots: '@logto/js': 4.1.1 '@silverhand/essentials': 2.9.1 camelcase-keys: 7.0.2 - jose: 5.2.2 + jose: 5.2.4 '@logto/cloud@0.2.5-a7eedce(zod@3.22.4)': dependencies: @@ -17852,13 +17890,13 @@ snapshots: dependencies: 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: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 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 prompts: 2.4.2 transitivePeerDependencies: @@ -19994,35 +20032,16 @@ snapshots: - babel-plugin-macros - 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: - '@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/types': 29.6.3 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 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.4) - 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-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 @@ -20051,7 +20070,7 @@ snapshots: - supports-color - 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: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -20077,6 +20096,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.10.4 + ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20112,6 +20132,37 @@ snapshots: - babel-plugin-macros - 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: dependencies: 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)) 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: dependencies: '@jest/types': 29.6.3 @@ -20443,24 +20489,12 @@ snapshots: merge-stream: 2.0.0 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: - '@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 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.10.4) - 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) + 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 @@ -23680,6 +23714,25 @@ snapshots: optionalDependencies: '@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: dependencies: '@types/json5': 0.0.29