diff --git a/packages/core/src/libraries/cloud-connection.test.ts b/packages/core/src/libraries/cloud-connection.test.ts index 9895fe9aa41..6c3289c1984 100644 --- a/packages/core/src/libraries/cloud-connection.test.ts +++ b/packages/core/src/libraries/cloud-connection.test.ts @@ -34,6 +34,7 @@ const logtoConfigs: LogtoConfigLibrary = { resource: 'resource', }), getOidcConfigs: jest.fn(), + upsertJwtCustomizer: jest.fn(), }; describe('getAccessToken()', () => { diff --git a/packages/core/src/libraries/logto-config.ts b/packages/core/src/libraries/logto-config.ts index 96d2a026d89..dc7882b0d1c 100644 --- a/packages/core/src/libraries/logto-config.ts +++ b/packages/core/src/libraries/logto-config.ts @@ -1,10 +1,11 @@ -import type { LogtoOidcConfigType } from '@logto/schemas'; import { cloudApiIndicator, cloudConnectionDataGuard, logtoOidcConfigGuard, LogtoOidcConfigKey, + jwtCustomizerConfigGuard, } from '@logto/schemas'; +import type { LogtoOidcConfigType, LogtoJwtTokenKey } from '@logto/schemas'; import chalk from 'chalk'; import { z, ZodError } from 'zod'; @@ -14,7 +15,11 @@ import { consoleLog } from '#src/utils/console.js'; export type LogtoConfigLibrary = ReturnType; export const createLogtoConfigLibrary = ({ - logtoConfigs: { getRowsByKeys, getCloudConnectionData: queryCloudConnectionData }, + logtoConfigs: { + getRowsByKeys, + getCloudConnectionData: queryCloudConnectionData, + upsertJwtCustomizer: queryUpsertJwtCustomizer, + }, }: Pick) => { const getOidcConfigs = async (): Promise => { try { @@ -59,5 +64,18 @@ export const createLogtoConfigLibrary = ({ }; }; - return { getOidcConfigs, getCloudConnectionData }; + // Can not narrow down the type of value if we utilize `buildInsertIntoWithPool` method. + const upsertJwtCustomizer = async ( + key: T, + value: z.infer<(typeof jwtCustomizerConfigGuard)[T]> + ) => { + const { value: rawValue } = await queryUpsertJwtCustomizer(key, value); + + return { + key, + value: jwtCustomizerConfigGuard[key].parse(rawValue), + }; + }; + + return { getOidcConfigs, getCloudConnectionData, upsertJwtCustomizer }; }; diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index 2f7e7b38057..04c61c70b94 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -57,6 +57,7 @@ const cloudConnection = createCloudConnectionLibrary({ resource: 'resource', }), getOidcConfigs: jest.fn(), + upsertJwtCustomizer: jest.fn(), }); const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); diff --git a/packages/core/src/middleware/koa-guard.ts b/packages/core/src/middleware/koa-guard.ts index 5ea3656268d..8506aa67f43 100644 --- a/packages/core/src/middleware/koa-guard.ts +++ b/packages/core/src/middleware/koa-guard.ts @@ -92,27 +92,34 @@ export const isGuardMiddleware = ( ): function_ is WithGuardConfig => function_.name === 'guardMiddleware' && has(function_, 'config'); -export function tryParse( +/** + * Previous `tryParse` function's output type was `Output | undefined`. + * It can not properly infer the output type to be `Output` even if the guard is provided, + * which brings additional but unnecessary type checks. + */ +export const parse = ( type: 'query' | 'body' | 'params' | 'files', guard: ZodType, data: unknown -): Output; -export function tryParse( - type: 'query' | 'body' | 'params' | 'files', - guard: undefined, - data: unknown -): undefined; -export function tryParse( - type: 'query' | 'body' | 'params' | 'files', - guard: Optional>, - data: unknown -) { +) => { try { - return guard?.parse(data); + return guard.parse(data); } catch (error: unknown) { throw new RequestError({ code: 'guard.invalid_input', type }, error); } -} +}; + +const tryParse = ( + type: 'query' | 'body' | 'params' | 'files', + guard: Optional>, + data: unknown +) => { + if (!guard) { + return; + } + + return parse(type, guard, data); +}; export default function koaGuard< StateT, diff --git a/packages/core/src/queries/logto-config.ts b/packages/core/src/queries/logto-config.ts index 14f0b8c6737..e7a68528ef5 100644 --- a/packages/core/src/queries/logto-config.ts +++ b/packages/core/src/queries/logto-config.ts @@ -8,7 +8,6 @@ import { type LogtoOidcConfigKey, type OidcConfigKey, type LogtoJwtTokenKey, - type JwtCustomizerType, } from '@logto/schemas'; import { convertToIdentifiers } from '@logto/shared'; import type { CommonQueryMethods } from 'slonik'; @@ -57,7 +56,7 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => { key: T, value: z.infer<(typeof jwtCustomizerConfigGuard)[T]> ) => - pool.one<{ key: T; value: JwtCustomizerType[T] }>( + pool.one<{ key: T; value: Record }>( sql` insert into ${table} (${fields.key}, ${fields.value}) values (${key}, ${sql.jsonb(value)}) diff --git a/packages/core/src/routes/logto-config.test.ts b/packages/core/src/routes/logto-config.test.ts index b82422b1a13..ac4fc2e4ac6 100644 --- a/packages/core/src/routes/logto-config.test.ts +++ b/packages/core/src/routes/logto-config.test.ts @@ -54,7 +54,7 @@ const logtoConfigQueries = { }), updateOidcConfigsByKey: jest.fn(), getRowsByKeys: jest.fn(async () => mockLogtoConfigRows), - upsertJwtCustomizer: jest.fn(), + // UpsertJwtCustomizer: jest.fn(), }; const logtoConfigLibraries = { @@ -62,6 +62,7 @@ const logtoConfigLibraries = { [LogtoOidcConfigKey.PrivateKeys]: mockPrivateKeys, [LogtoOidcConfigKey.CookieKeys]: mockCookieKeys, })), + upsertJwtCustomizer: jest.fn(), }; const settingRoutes = await pickDefault(import('./logto-config.js')); @@ -232,13 +233,13 @@ describe('configs routes', () => { rows: [], rowCount: 0, }); - logtoConfigQueries.upsertJwtCustomizer.mockResolvedValueOnce( + logtoConfigLibraries.upsertJwtCustomizer.mockResolvedValueOnce( mockJwtCustomizerConfigForAccessToken ); const response = await routeRequester .put(`/configs/jwt-customizer/access-token`) .send(mockJwtCustomizerConfigForAccessToken.value); - expect(logtoConfigQueries.upsertJwtCustomizer).toHaveBeenCalledWith( + expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalledWith( LogtoJwtTokenKey.AccessToken, mockJwtCustomizerConfigForAccessToken.value ); @@ -252,13 +253,13 @@ describe('configs routes', () => { rows: [mockJwtCustomizerConfigForAccessToken], rowCount: 1, }); - logtoConfigQueries.upsertJwtCustomizer.mockResolvedValueOnce( + logtoConfigLibraries.upsertJwtCustomizer.mockResolvedValueOnce( mockJwtCustomizerConfigForAccessToken ); const response = await routeRequester .put('/configs/jwt-customizer/access-token') .send(mockJwtCustomizerConfigForAccessToken.value); - expect(logtoConfigQueries.upsertJwtCustomizer).toHaveBeenCalledWith( + expect(logtoConfigLibraries.upsertJwtCustomizer).toHaveBeenCalledWith( LogtoJwtTokenKey.AccessToken, mockJwtCustomizerConfigForAccessToken.value ); diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 8f82d61f36e..965a1a62a79 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -19,7 +19,7 @@ import { import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; -import koaGuard, { tryParse } from '#src/middleware/koa-guard.js'; +import koaGuard, { parse } from '#src/middleware/koa-guard.js'; import { exportJWK } from '#src/utils/jwks.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; @@ -41,12 +41,12 @@ const getJwtTokenKeyAndBody = (tokenPath: LogtoJwtTokenPath, body: unknown) => { if (tokenPath === LogtoJwtTokenPath.AccessToken) { return { key: LogtoJwtTokenKey.AccessToken, - body: tryParse('body', jwtCustomizerAccessTokenGuard, body), + body: parse('body', jwtCustomizerAccessTokenGuard, body), }; } return { key: LogtoJwtTokenKey.ClientCredentials, - body: tryParse('body', jwtCustomizerClientCredentialsGuard, body), + body: parse('body', jwtCustomizerClientCredentialsGuard, body), }; }; @@ -81,14 +81,9 @@ const getRedactedOidcKeyResponse = async ( export default function logtoConfigRoutes( ...[router, { queries, logtoConfigs, invalidateCache }]: RouterInitArgs ) { - const { - getAdminConsoleConfig, - getRowsByKeys, - upsertJwtCustomizer, - updateAdminConsoleConfig, - updateOidcConfigsByKey, - } = queries.logtoConfigs; - const { getOidcConfigs } = logtoConfigs; + const { getAdminConsoleConfig, getRowsByKeys, updateAdminConsoleConfig, updateOidcConfigsByKey } = + queries.logtoConfigs; + const { getOidcConfigs, upsertJwtCustomizer } = logtoConfigs; router.get( '/configs/admin-console',