From 5fbee285658f9482db4e73ab6dfa876f1938c7e5 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 19 Mar 2024 11:56:15 +0800 Subject: [PATCH 01/11] feat(core): add POST /configs/jwt-customizer/test API --- packages/core/src/routes/logto-config.ts | 46 +++++++++++++++++++- packages/schemas/src/types/jwt-customizer.ts | 12 +++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 5543462c841..fd10011b6f1 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -16,9 +16,12 @@ import { clientCredentialsJwtCustomizerGuard, LogtoJwtTokenKey, LogtoJwtTokenPath, + jsonObjectGuard, + customJwtFetcherGuard, } from '@logto/schemas'; import { z } from 'zod'; +import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard, { parse } from '#src/middleware/koa-guard.js'; import { exportJWK } from '#src/utils/jwks.js'; @@ -75,7 +78,7 @@ const getRedactedOidcKeyResponse = async ( ); export default function logtoConfigRoutes( - ...[router, { queries, logtoConfigs, invalidateCache }]: RouterInitArgs + ...[router, { queries, logtoConfigs, invalidateCache, cloudConnection }]: RouterInitArgs ) { const { getAdminConsoleConfig, @@ -287,4 +290,45 @@ export default function logtoConfigRoutes( return next(); } ); + + if (!EnvSet.values.isCloud) { + return; + } + + router.post( + '/configs/jwt-customizer/:tokenTypePath/test', + koaGuard({ + params: z.object({ + tokenTypePath: z.nativeEnum(LogtoJwtTokenPath), + }), + body: z.unknown(), + response: jsonObjectGuard, + /** + * 400 for cloud service zod error (data type does not match expectation, can be either request body or response body) + * 422 for cloud service syntax error + */ + status: [200, 400, 422], + }), + async (ctx, next) => { + const { + params: { tokenTypePath }, + body: rawBody, + } = ctx.guard; + const { + body: { tokenSample, contextSample, ...rest }, + } = getJwtTokenKeyAndBody(tokenTypePath, rawBody); + + const client = await cloudConnection.getClient(); + const testResult = await client.post(`/api/services/custom-jwt`, { + body: customJwtFetcherGuard.parse({ + ...rest, + tokenSample, + contextSample, + }), + }); + + ctx.body = testResult; + return next(); + } + ); } diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts index 22a21a71238..9406cfe0aef 100644 --- a/packages/schemas/src/types/jwt-customizer.ts +++ b/packages/schemas/src/types/jwt-customizer.ts @@ -47,6 +47,18 @@ export const customJwtFetcherGuard = jwtCustomizerGuard export type CustomJwtFetcher = z.infer; +/** + * This guard is for testing use (request body guard), renamed previous `token` and `context` + * fields (in `customJwtFetcherGuard`) to `tokenSample` and `contextSample`, which can bring + * convenience to the testing use case. + */ +export const customJwtTesterGuard = customJwtFetcherGuard + .pick({ script: true, envVars: true }) + .extend({ + tokenSample: jsonObjectGuard, + contextSample: jsonObjectGuard.optional(), + }); + export enum LogtoJwtTokenPath { AccessToken = 'access-token', ClientCredentials = 'client-credentials', From ef7e8e1fda83d6eec8afacbcec19f13a127bde39 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 19 Mar 2024 17:26:38 +0800 Subject: [PATCH 02/11] refactor(core): use discriminate union for custom jwt test API --- packages/core/src/routes/logto-config.ts | 51 ++++++++++++++---------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index fd10011b6f1..9c4f712fc3e 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -17,7 +17,7 @@ import { LogtoJwtTokenKey, LogtoJwtTokenPath, jsonObjectGuard, - customJwtFetcherGuard, + type CustomJwtFetcher, } from '@logto/schemas'; import { z } from 'zod'; @@ -296,38 +296,47 @@ export default function logtoConfigRoutes( } router.post( - '/configs/jwt-customizer/:tokenTypePath/test', + '/configs/jwt-customizer/test', koaGuard({ - params: z.object({ - tokenTypePath: z.nativeEnum(LogtoJwtTokenPath), - }), - body: z.unknown(), + body: z.discriminatedUnion('tokenType', [ + z.object({ + tokenType: z.literal(LogtoJwtTokenKey.AccessToken), + payload: accessTokenJwtCustomizerGuard, + }), + z.object({ + tokenType: z.literal(LogtoJwtTokenKey.ClientCredentials), + payload: clientCredentialsJwtCustomizerGuard, + }), + ]), response: jsonObjectGuard, /** - * 400 for cloud service zod error (data type does not match expectation, can be either request body or response body) - * 422 for cloud service syntax error + * Code 400 indicates Zod errors in cloud service (data type does not match expectation, can be either request body or response body). + * Code 422 indicates syntax errors in cloud service. */ status: [200, 400, 422], }), async (ctx, next) => { const { - params: { tokenTypePath }, - body: rawBody, + body: { + payload: { tokenSample, contextSample, ...rest }, + }, } = ctx.guard; - const { - body: { tokenSample, contextSample, ...rest }, - } = getJwtTokenKeyAndBody(tokenTypePath, rawBody); + /** + * We have ensured the API request body via koa guard, manually cast the cloud service API call's + * `requestBody` type and let the cloud service API to throw if needed. + */ + // eslint-disable-next-line no-restricted-syntax, @typescript-eslint/consistent-type-assertions + const requestBody = { + ...rest, + token: tokenSample, + context: contextSample, + } as CustomJwtFetcher; const client = await cloudConnection.getClient(); - const testResult = await client.post(`/api/services/custom-jwt`, { - body: customJwtFetcherGuard.parse({ - ...rest, - tokenSample, - contextSample, - }), - }); - ctx.body = testResult; + ctx.body = await client.post(`/api/services/custom-jwt`, { + body: requestBody, + }); return next(); } ); From aac7a7bcf82b086807904d2b90c45cb5ed7f4ee3 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 19 Mar 2024 18:00:28 +0800 Subject: [PATCH 03/11] chore: update core dependency --- packages/core/package.json | 2 +- packages/core/src/routes/logto-config.ts | 3 ++- packages/schemas/src/types/jwt-customizer.ts | 12 ------------ pnpm-lock.yaml | 17 +++++++++++++++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index bd1f2c7812d..99b46a4b106 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,7 +91,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@logto/cloud": "0.2.5-4ef0b45", + "@logto/cloud": "0.2.5-ceb63ed", "@silverhand/eslint-config": "5.0.0", "@silverhand/ts-config": "5.0.0", "@types/debug": "^4.1.7", diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 9c4f712fc3e..9dc6592cc64 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -312,8 +312,9 @@ export default function logtoConfigRoutes( /** * Code 400 indicates Zod errors in cloud service (data type does not match expectation, can be either request body or response body). * Code 422 indicates syntax errors in cloud service. + * Code 500 indicates internal server errors in cloud service. */ - status: [200, 400, 422], + status: [200, 400, 422, 500], }), async (ctx, next) => { const { diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts index 9406cfe0aef..22a21a71238 100644 --- a/packages/schemas/src/types/jwt-customizer.ts +++ b/packages/schemas/src/types/jwt-customizer.ts @@ -47,18 +47,6 @@ export const customJwtFetcherGuard = jwtCustomizerGuard export type CustomJwtFetcher = z.infer; -/** - * This guard is for testing use (request body guard), renamed previous `token` and `context` - * fields (in `customJwtFetcherGuard`) to `tokenSample` and `contextSample`, which can bring - * convenience to the testing use case. - */ -export const customJwtTesterGuard = customJwtFetcherGuard - .pick({ script: true, envVars: true }) - .extend({ - tokenSample: jsonObjectGuard, - contextSample: jsonObjectGuard.optional(), - }); - export enum LogtoJwtTokenPath { AccessToken = 'access-token', ClientCredentials = 'client-credentials', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c08d806d771..39c8c91921c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3400,8 +3400,8 @@ importers: version: 3.22.4 devDependencies: '@logto/cloud': - specifier: 0.2.5-4ef0b45 - version: 0.2.5-4ef0b45(zod@3.22.4) + specifier: 0.2.5-ceb63ed + version: 0.2.5-ceb63ed(zod@3.22.4) '@silverhand/eslint-config': specifier: 5.0.0 version: 5.0.0(eslint@8.44.0)(prettier@3.0.0)(typescript@5.3.3) @@ -7670,6 +7670,16 @@ packages: - zod dev: true + /@logto/cloud@0.2.5-ceb63ed(zod@3.22.4): + resolution: {integrity: sha512-mF4ce41qc4nnmLjlyvTxiy3i1K88xC91yjWcmyrJ66Zujr+w7AaR/Ye0g8TZ1B93qlDt5EJBFuBW/gqYDuwEEQ==} + engines: {node: ^20.9.0} + dependencies: + '@silverhand/essentials': 2.9.0 + '@withtyped/server': 0.13.3(zod@3.22.4) + transitivePeerDependencies: + - zod + dev: true + /@logto/js@4.0.0: resolution: {integrity: sha512-eKLS0HqFjQyf7imKTf2a7FUmMuNeebxFZ2b5A/1qUWSyO+BIxSu0XYI4JZ9qjtWNPvlGmL+jPOkIUuWQ9DZYZw==} dependencies: @@ -17849,6 +17859,9 @@ packages: resolution: {integrity: sha512-2GTVocFkwblV/TIg9AmT7TI2fO4xdWkyN8aFUEVtiVNWt96GTR3FgQyHFValfCbcj1k9Xf962Ws2hYXYUr9k1Q==} engines: {node: '>= 12.0.0'} hasBin: true + peerDependenciesMeta: + '@parcel/core': + optional: true dependencies: '@parcel/config-default': 2.9.3(@parcel/core@2.9.3)(postcss@8.4.31) '@parcel/core': 2.9.3 From e72e927bb1f05fdb5f65efd9c5555a045a899964 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 20 Mar 2024 13:31:36 +0800 Subject: [PATCH 04/11] chore: add todo --- packages/core/src/routes/logto-config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 9dc6592cc64..eaac25a774e 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -310,6 +310,8 @@ export default function logtoConfigRoutes( ]), response: jsonObjectGuard, /** + * TODO (LOG-8450): Add this path to openapi.json file, since now the swagger.json can not automatically adapt the path validation for OSS version. + * * Code 400 indicates Zod errors in cloud service (data type does not match expectation, can be either request body or response body). * Code 422 indicates syntax errors in cloud service. * Code 500 indicates internal server errors in cloud service. From ed710b06ec7cb8362ae832255a367980c277c249 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 20 Mar 2024 15:00:47 +0800 Subject: [PATCH 05/11] chore: remove 500 status guard --- packages/core/src/routes/logto-config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index eaac25a774e..1b6b6ce8fa3 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -314,9 +314,8 @@ export default function logtoConfigRoutes( * * Code 400 indicates Zod errors in cloud service (data type does not match expectation, can be either request body or response body). * Code 422 indicates syntax errors in cloud service. - * Code 500 indicates internal server errors in cloud service. */ - status: [200, 400, 422, 500], + status: [200, 400, 422], }), async (ctx, next) => { const { From 9c7956da5cc0c974401bf9c3df74a6bc614c879a Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 20 Mar 2024 15:23:18 +0800 Subject: [PATCH 06/11] refactor: refactor --- packages/core/src/routes/logto-config.ts | 32 +++++++++++--------- packages/schemas/src/types/jwt-customizer.ts | 8 ++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 1b6b6ce8fa3..91067ce8e50 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -17,7 +17,6 @@ import { LogtoJwtTokenKey, LogtoJwtTokenPath, jsonObjectGuard, - type CustomJwtFetcher, } from '@logto/schemas'; import { z } from 'zod'; @@ -298,14 +297,25 @@ export default function logtoConfigRoutes( router.post( '/configs/jwt-customizer/test', koaGuard({ + /** + * Early throws when: + * 1. no `script` provided. + * 2. no `tokenSample` provided. + */ body: z.discriminatedUnion('tokenType', [ z.object({ tokenType: z.literal(LogtoJwtTokenKey.AccessToken), - payload: accessTokenJwtCustomizerGuard, + payload: accessTokenJwtCustomizerGuard.required({ + script: true, + tokenSample: true, + }), }), z.object({ tokenType: z.literal(LogtoJwtTokenKey.ClientCredentials), - payload: clientCredentialsJwtCustomizerGuard, + payload: clientCredentialsJwtCustomizerGuard.required({ + script: true, + tokenSample: true, + }), }), ]), response: jsonObjectGuard, @@ -324,20 +334,14 @@ export default function logtoConfigRoutes( }, } = ctx.guard; - /** - * We have ensured the API request body via koa guard, manually cast the cloud service API call's - * `requestBody` type and let the cloud service API to throw if needed. - */ - // eslint-disable-next-line no-restricted-syntax, @typescript-eslint/consistent-type-assertions - const requestBody = { - ...rest, - token: tokenSample, - context: contextSample, - } as CustomJwtFetcher; const client = await cloudConnection.getClient(); ctx.body = await client.post(`/api/services/custom-jwt`, { - body: requestBody, + body: { + ...rest, + token: tokenSample, + context: contextSample, + }, }); return next(); } diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts index 22a21a71238..640d2d8d3a0 100644 --- a/packages/schemas/src/types/jwt-customizer.ts +++ b/packages/schemas/src/types/jwt-customizer.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { Organizations, Roles, UserSsoIdentities } from '../db-entries/index.js'; -import { mfaFactorsGuard, jsonObjectGuard } from '../foundations/index.js'; +import { Roles, UserSsoIdentities, Organizations } from '../db-entries/index.js'; +import { mfaFactorsGuard } from '../foundations/index.js'; import { jwtCustomizerGuard } from './logto-config/index.js'; import { scopeResponseGuard } from './scope.js'; @@ -41,8 +41,8 @@ export const customJwtFetcherGuard = jwtCustomizerGuard .pick({ script: true, envVars: true }) .required({ script: true }) .extend({ - token: jsonObjectGuard, - context: jsonObjectGuard.optional(), + token: z.record(z.unknown()), + context: z.record(z.unknown()).optional(), }); export type CustomJwtFetcher = z.infer; From 77b67fbd0485e0067ef1017327152ad18c7b71cf Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 20 Mar 2024 18:48:16 +0800 Subject: [PATCH 07/11] refactor(core): add cloud-only API prune for API docs --- .../core/src/routes/logto-config.openapi.json | 44 ++++++++++++++++ packages/core/src/routes/swagger/index.ts | 6 ++- .../core/src/routes/swagger/utils/general.ts | 51 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index 64fd081b752..320e32379f7 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -188,6 +188,50 @@ } } } + }, + "/api/configs/jwt-customizer/test": { + "post": { + "tags": ["cloud-only"], + "summary": "Test JWT customizer", + "description": "Test the JWT customizer script with the given sample context and sample token payload.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "tokenType": { + "description": "The token type to test the JWT customizer for." + }, + "payload": { + "properties": { + "script": { + "description": "The code snippet of the JWT customizer." + }, + "envVars": { + "description": "The environment variables for the JWT customizer." + }, + "contextSample": { + "description": "The sample context for the JWT customizer script testing purpose." + }, + "tokenSample": { + "description": "The sample token payload for the JWT customizer script testing purpose." + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The result of the JWT customizer script testing." + }, + "400": { + "description": "The request body is invalid." + } + } + } } } } diff --git a/packages/core/src/routes/swagger/index.ts b/packages/core/src/routes/swagger/index.ts index 97b5a1099ac..118547103c3 100644 --- a/packages/core/src/routes/swagger/index.ts +++ b/packages/core/src/routes/swagger/index.ts @@ -24,6 +24,7 @@ import { buildTag, findSupplementFiles, normalizePath, + pruneSupplementPaths, validateSupplement, validateSwaggerDocument, } from './utils/general.js'; @@ -192,12 +193,15 @@ export default function swaggerRoutes JSON.parse(await fs.readFile(path, 'utf8')) as Record ) ); + const supplementDocuments = rawSupplementDocuments.map((document) => + pruneSupplementPaths(document) + ); const baseDocument: OpenAPIV3.Document = { openapi: '3.0.1', diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index 850ebc0827e..6e04c905c75 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -6,6 +6,7 @@ import { isKeyInObject, type Optional } from '@silverhand/essentials'; import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; +import { EnvSet } from '#src/env-set/index.js'; import { consoleLog } from '#src/utils/console.js'; const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); @@ -201,3 +202,53 @@ export const validateSwaggerDocument = (document: OpenAPIV3.Document) => { } } }; + +/** + * Some path are only available in the cloud version, so we need to prune them out in the OSS. + */ +export const pruneSupplementPaths = (supplement: Record) => { + if (EnvSet.values.isCloud) { + return supplement; + } + + // eslint-disable-next-line no-restricted-syntax + const supplementName = ((supplement.tags ?? []) as OpenAPIV3.TagObject[])[0]?.name; + + if (!supplementName) { + return supplement; + } + + if (!supplement.paths) { + return supplement; + } + + // eslint-disable-next-line no-restricted-syntax + const supplementPaths = supplement.paths as OpenAPIV3.PathsObject; + + if (Object.entries(supplement.paths).length === 0) { + return supplement; + } + + // eslint-disable-next-line no-restricted-syntax + const newPaths = Object.fromEntries( + Object.entries(supplementPaths) + .map(([path, pathBody]) => [ + path, + Object.fromEntries( + // eslint-disable-next-line no-restricted-syntax + Object.entries(pathBody as OpenAPIV3.PathItemObject).filter( + ([_, operationBody]) => + // eslint-disable-next-line no-restricted-syntax + !((operationBody as OpenAPIV3.OperationObject).tags ?? []).includes('cloud-only') + ) + ), + ]) + // eslint-disable-next-line no-restricted-syntax + .filter(([_, pathBody]) => Object.entries(pathBody as OpenAPIV3.PathItemObject).length > 0) + ) as OpenAPIV3.PathsObject; + + return { + ...supplement, + paths: newPaths, + }; +}; From 45a7ee17aae4758fbc4d8b8c2eca1c5581907573 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 20 Mar 2024 23:29:45 +0800 Subject: [PATCH 08/11] refactor: remove cloud only operations when needed --- .../core/src/routes/logto-config.openapi.json | 2 +- packages/core/src/routes/swagger/index.ts | 25 +++--- .../core/src/routes/swagger/utils/general.ts | 78 ++++++++----------- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index 320e32379f7..4f79d364ff4 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -191,7 +191,7 @@ }, "/api/configs/jwt-customizer/test": { "post": { - "tags": ["cloud-only"], + "tags": ["Cloud only"], "summary": "Test JWT customizer", "description": "Test the JWT customizer script with the given sample context and sample token payload.", "requestBody": { diff --git a/packages/core/src/routes/swagger/index.ts b/packages/core/src/routes/swagger/index.ts index 118547103c3..195c92c5249 100644 --- a/packages/core/src/routes/swagger/index.ts +++ b/packages/core/src/routes/swagger/index.ts @@ -7,13 +7,14 @@ import deepmerge from 'deepmerge'; import { findUp } from 'find-up'; import type { IMiddleware } from 'koa-router'; import type Router from 'koa-router'; -import type { OpenAPIV3 } from 'openapi-types'; +import { type OpenAPIV3 } from 'openapi-types'; import { EnvSet } from '#src/env-set/index.js'; import { isKoaAuthMiddleware } from '#src/middleware/koa-auth/index.js'; import type { WithGuardConfig } from '#src/middleware/koa-guard.js'; import { isGuardMiddleware } from '#src/middleware/koa-guard.js'; import { isPaginationMiddleware } from '#src/middleware/koa-pagination.js'; +import { type DeepPartial } from '#src/test-utils/tenant.js'; import assertThat from '#src/utils/assert-that.js'; import { consoleLog } from '#src/utils/console.js'; import { translationSchemas, zodTypeToSwagger } from '#src/utils/zod.js'; @@ -24,7 +25,7 @@ import { buildTag, findSupplementFiles, normalizePath, - pruneSupplementPaths, + removeCloudOnlyOperations, validateSupplement, validateSwaggerDocument, } from './utils/general.js'; @@ -193,15 +194,14 @@ export default function swaggerRoutes JSON.parse(await fs.readFile(path, 'utf8')) as Record + const supplementDocuments = await Promise.all( + supplementPaths.map(async (path) => + removeCloudOnlyOperations( + // eslint-disable-next-line no-restricted-syntax -- trust the type here as we'll validate it later + JSON.parse(await fs.readFile(path, 'utf8')) as DeepPartial + ) ) ); - const supplementDocuments = rawSupplementDocuments.map((document) => - pruneSupplementPaths(document) - ); const baseDocument: OpenAPIV3.Document = { openapi: '3.0.1', @@ -232,8 +232,11 @@ export default function swaggerRoutes ({ name: tag })), }; - const data = supplementDocuments.reduce( - (document, supplement) => deepmerge(document, supplement, { arrayMerge: mergeParameters }), + const data = supplementDocuments.reduce( + (document, supplement) => + deepmerge>(document, supplement, { + arrayMerge: mergeParameters, + }), baseDocument ); diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index 6e04c905c75..c230cdcc4ad 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -7,10 +7,14 @@ import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; +import { type DeepPartial } from '#src/test-utils/tenant.js'; import { consoleLog } from '#src/utils/console.js'; const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); +/** The tag name used in the supplement document to indicate that the operation is cloud only. */ +const cloudOnlyTag = 'Cloud only'; + /** * Get the root component name from the given absolute path. * @example '/organizations/:id' -> 'organizations' @@ -106,9 +110,14 @@ const validateSupplementPaths = ( ); } - if (isKeyInObject(operations[method], 'tags')) { + const operation = operations[method]; + if ( + isKeyInObject(operation, 'tags') && + Array.isArray(operation.tags) && + (operation.tags.length > 1 || operation.tags[0] !== cloudOnlyTag) + ) { throw new TypeError( - `Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\`. Define tags in the document root instead.` + `Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\` except for \`${cloudOnlyTag}\`. Define tags in the document root instead.` ); } } @@ -125,7 +134,7 @@ const validateSupplementPaths = ( */ export const validateSupplement = ( original: OpenAPIV3.Document, - supplement: Record + supplement: DeepPartial ) => { if (supplement.tags) { const supplementTags = z.array(z.object({ name: z.string() })).parse(supplement.tags); @@ -204,51 +213,32 @@ export const validateSwaggerDocument = (document: OpenAPIV3.Document) => { }; /** - * Some path are only available in the cloud version, so we need to prune them out in the OSS. + * **CAUTION**: This function mutates the input document. + * + * Remove operations (path + method) that are tagged with `Cloud only` if the application is not + * running in the cloud. This will prevent the swagger validation from failing in the OSS + * environment. */ -export const pruneSupplementPaths = (supplement: Record) => { - if (EnvSet.values.isCloud) { - return supplement; - } - - // eslint-disable-next-line no-restricted-syntax - const supplementName = ((supplement.tags ?? []) as OpenAPIV3.TagObject[])[0]?.name; - - if (!supplementName) { - return supplement; +export const removeCloudOnlyOperations = ( + document: DeepPartial +): DeepPartial => { + if (EnvSet.values.isCloud || !document.paths) { + return document; } - if (!supplement.paths) { - return supplement; - } - - // eslint-disable-next-line no-restricted-syntax - const supplementPaths = supplement.paths as OpenAPIV3.PathsObject; + for (const [path, pathItem] of Object.entries(document.paths)) { + for (const method of Object.values(OpenAPIV3.HttpMethods)) { + if (pathItem?.[method]?.tags?.includes(cloudOnlyTag)) { + // eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete -- intended + delete pathItem[method]; + } + } - if (Object.entries(supplement.paths).length === 0) { - return supplement; + if (Object.keys(pathItem ?? {}).length === 0) { + // eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete -- intended + delete document.paths[path]; + } } - // eslint-disable-next-line no-restricted-syntax - const newPaths = Object.fromEntries( - Object.entries(supplementPaths) - .map(([path, pathBody]) => [ - path, - Object.fromEntries( - // eslint-disable-next-line no-restricted-syntax - Object.entries(pathBody as OpenAPIV3.PathItemObject).filter( - ([_, operationBody]) => - // eslint-disable-next-line no-restricted-syntax - !((operationBody as OpenAPIV3.OperationObject).tags ?? []).includes('cloud-only') - ) - ), - ]) - // eslint-disable-next-line no-restricted-syntax - .filter(([_, pathBody]) => Object.entries(pathBody as OpenAPIV3.PathItemObject).length > 0) - ) as OpenAPIV3.PathsObject; - - return { - ...supplement, - paths: newPaths, - }; + return document; }; From b2e00df1b9f00d8fddc48cc603f0009b2741e010 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 20 Mar 2024 23:49:36 +0800 Subject: [PATCH 09/11] refactor: keep origin customJwtFetcherGuard --- packages/schemas/src/types/jwt-customizer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts index 640d2d8d3a0..6e42f716cb5 100644 --- a/packages/schemas/src/types/jwt-customizer.ts +++ b/packages/schemas/src/types/jwt-customizer.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { Roles, UserSsoIdentities, Organizations } from '../db-entries/index.js'; -import { mfaFactorsGuard } from '../foundations/index.js'; +import { jsonObjectGuard, mfaFactorsGuard } from '../foundations/index.js'; import { jwtCustomizerGuard } from './logto-config/index.js'; import { scopeResponseGuard } from './scope.js'; @@ -41,8 +41,8 @@ export const customJwtFetcherGuard = jwtCustomizerGuard .pick({ script: true, envVars: true }) .required({ script: true }) .extend({ - token: z.record(z.unknown()), - context: z.record(z.unknown()).optional(), + token: jsonObjectGuard, + context: jsonObjectGuard.optional(), }); export type CustomJwtFetcher = z.infer; From 239c17f367719e46218d26e35788a2b57de29a69 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 21 Mar 2024 11:13:06 +0800 Subject: [PATCH 10/11] refactor: adopt suggestion --- packages/core/src/routes/logto-config.openapi.json | 5 ++++- packages/core/src/routes/logto-config.ts | 6 ------ packages/core/src/routes/swagger/utils/general.ts | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index 4f79d364ff4..a74fd09bd59 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -228,7 +228,10 @@ "description": "The result of the JWT customizer script testing." }, "400": { - "description": "The request body is invalid." + "description": "Zod errors in cloud service (data type does not match expectation, can be either request body or response body)." + }, + "422": { + "description": "Syntax errors in cloud service." } } } diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 91067ce8e50..8221e203a08 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -319,12 +319,6 @@ export default function logtoConfigRoutes( }), ]), response: jsonObjectGuard, - /** - * TODO (LOG-8450): Add this path to openapi.json file, since now the swagger.json can not automatically adapt the path validation for OSS version. - * - * Code 400 indicates Zod errors in cloud service (data type does not match expectation, can be either request body or response body). - * Code 422 indicates syntax errors in cloud service. - */ status: [200, 400, 422], }), async (ctx, next) => { diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index c230cdcc4ad..a658e24a33e 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -117,7 +117,7 @@ const validateSupplementPaths = ( (operation.tags.length > 1 || operation.tags[0] !== cloudOnlyTag) ) { throw new TypeError( - `Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\` except for \`${cloudOnlyTag}\`. Define tags in the document root instead.` + `Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\` except for tag \`${cloudOnlyTag}\`. Define tags in the document root instead.` ); } } From 88f759c6863822c268bb7cd3bce85cf488bfabfd Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 21 Mar 2024 12:51:57 +0800 Subject: [PATCH 11/11] chore: add status 403 for custom jwt test API --- packages/core/src/routes/logto-config.openapi.json | 3 +++ packages/core/src/routes/logto-config.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index a74fd09bd59..481db41cd71 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -230,6 +230,9 @@ "400": { "description": "Zod errors in cloud service (data type does not match expectation, can be either request body or response body)." }, + "403": { + "description": "Cloud connection does not have enough permission to perform the action." + }, "422": { "description": "Syntax errors in cloud service." } diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 8221e203a08..97e11799f0f 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -319,7 +319,7 @@ export default function logtoConfigRoutes( }), ]), response: jsonObjectGuard, - status: [200, 400, 422], + status: [200, 400, 403, 422], }), async (ctx, next) => { const {