From 2e103f513e23dd42631f6a90d4ea2fd94db1bbfa Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 18 Apr 2024 14:24:11 +0800 Subject: [PATCH 1/4] fix(core): update cloud dependency, cloud client calls --- .../connector-logto-email/package.json | 2 +- packages/console/package.json | 2 +- packages/core/package.json | 2 +- .../logto-config.script-merging.test.ts | 357 ++++++++++++++++++ packages/core/src/libraries/logto-config.ts | 42 ++- packages/core/src/oidc/extra-token-claims.ts | 1 + .../logto-config/jwt-customizer.test.ts | 3 + .../src/routes/logto-config/jwt-customizer.ts | 1 + .../src/utils/{ => custom-jwt}/custom-jwt.ts | 6 +- packages/core/src/utils/custom-jwt/index.ts | 2 + packages/core/src/utils/custom-jwt/types.ts | 8 + pnpm-lock.yaml | 19 +- 12 files changed, 416 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/libraries/logto-config.script-merging.test.ts rename packages/core/src/utils/{ => custom-jwt}/custom-jwt.ts (60%) create mode 100644 packages/core/src/utils/custom-jwt/index.ts create mode 100644 packages/core/src/utils/custom-jwt/types.ts diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json index 18321191c..00b994154 100644 --- a/packages/connectors/connector-logto-email/package.json +++ b/packages/connectors/connector-logto-email/package.json @@ -52,7 +52,7 @@ "access": "public" }, "devDependencies": { - "@logto/cloud": "0.2.5-821690c", + "@logto/cloud": "0.2.5-e5d8200", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", diff --git a/packages/console/package.json b/packages/console/package.json index 3f76816e8..9f775e904 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@fontsource/roboto-mono": "^5.0.0", "@jest/types": "^29.5.0", - "@logto/cloud": "0.2.5-821690c", + "@logto/cloud": "0.2.5-e5d8200", "@logto/connector-kit": "workspace:^3.0.0", "@logto/core-kit": "workspace:^2.4.0", "@logto/language-kit": "workspace:^1.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index c9a2f9ae4..377e40a62 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -92,7 +92,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@logto/cloud": "0.2.5-821690c", + "@logto/cloud": "0.2.5-e5d8200", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/debug": "^4.1.7", diff --git a/packages/core/src/libraries/logto-config.script-merging.test.ts b/packages/core/src/libraries/logto-config.script-merging.test.ts new file mode 100644 index 000000000..2f1bd546f --- /dev/null +++ b/packages/core/src/libraries/logto-config.script-merging.test.ts @@ -0,0 +1,357 @@ +import { LogtoJwtTokenKey } from '@logto/schemas'; +import { cond } from '@silverhand/essentials'; +import deepmerge from 'deepmerge'; + +describe('Test the deploy custom JWT script', () => { + describe('Test script when both AccessToken & ClientCredentials scripts are working', () => { + it.each(Object.values(LogtoJwtTokenKey))('test %s script', (key) => { + expect( + deepmerge( + { + [LogtoJwtTokenKey.AccessToken]: { + production: `${LogtoJwtTokenKey.AccessToken}-production`, + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: `${LogtoJwtTokenKey.ClientCredentials}-production`, + }, + }, + { + [key]: { + test: `${key}-test`, + }, + } + ) + ).toEqual({ + [LogtoJwtTokenKey.AccessToken]: { + production: `${LogtoJwtTokenKey.AccessToken}-production`, + ...cond(key === LogtoJwtTokenKey.AccessToken && { test: `${key}-test` }), + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: `${LogtoJwtTokenKey.ClientCredentials}-production`, + ...cond(key === LogtoJwtTokenKey.ClientCredentials && { test: `${key}-test` }), + }, + }); + }); + }); + + describe('Test script:', () => { + // Test it.each() can not be nested, so we have to test each key separately. + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.AccessToken} script is working, test $s script`, + (testingKey) => { + const workingKey = LogtoJwtTokenKey.AccessToken; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [testingKey]: { + test: `${testingKey}-test`, + }, + }) + ).toEqual( + workingKey === testingKey + ? { + [workingKey]: { + production: `${workingKey}-production`, + test: `${workingKey}-test`, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [testingKey]: { + test: `${testingKey}-test`, + }, + } + ); + } + ); + + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.ClientCredentials} script is working, test $s script`, + (testingKey) => { + const workingKey = LogtoJwtTokenKey.ClientCredentials; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [testingKey]: { + test: `${testingKey}-test`, + }, + }) + ).toEqual( + workingKey === testingKey + ? { + [workingKey]: { + production: `${workingKey}-production`, + test: `${workingKey}-test`, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [testingKey]: { + test: `${testingKey}-test`, + }, + } + ); + } + ); + }); + + describe('Test script when both AccessToken & ClientCredentials scripts are not working', () => { + it.each(Object.values(LogtoJwtTokenKey))('test %s script', (key) => { + expect( + deepmerge( + {}, + { + [key]: `${key}-test`, + } + ) + ).toEqual({ + [key]: `${key}-test`, + }); + }); + }); +}); + +describe('Test deploy custom JWT script', () => { + describe('Deploy script when both AccessToken & ClientCredentials scripts are working', () => { + it.each(Object.values(LogtoJwtTokenKey))('deploy %s script', (key) => { + expect( + deepmerge( + { + [LogtoJwtTokenKey.AccessToken]: { + production: `${LogtoJwtTokenKey.AccessToken}-production`, + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: `${LogtoJwtTokenKey.ClientCredentials}-production`, + }, + }, + { + [key]: { + production: `${key}-production-new`, + }, + } + ) + ).toEqual({ + [LogtoJwtTokenKey.AccessToken]: { + production: `${LogtoJwtTokenKey.AccessToken}-production${ + key === LogtoJwtTokenKey.AccessToken ? '-new' : '' + }`, + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: `${LogtoJwtTokenKey.ClientCredentials}-production${ + key === LogtoJwtTokenKey.ClientCredentials ? '-new' : '' + }`, + }, + }); + }); + }); + + describe('Deploy script:', () => { + // Test it.each() can not be nested, so we have to test each key separately. + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.AccessToken} script is working, deploy $s script`, + (deployingKey) => { + const workingKey = LogtoJwtTokenKey.AccessToken; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [deployingKey]: { + production: `${deployingKey}-production-new`, + }, + }) + ).toEqual( + workingKey === deployingKey + ? { + [workingKey]: { + production: `${workingKey}-production-new`, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [deployingKey]: { + production: `${deployingKey}-production-new`, + }, + } + ); + } + ); + + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.ClientCredentials} script is working, deploy $s script`, + (deployingKey) => { + const workingKey = LogtoJwtTokenKey.ClientCredentials; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [deployingKey]: { + production: `${deployingKey}-production-new`, + }, + }) + ).toEqual( + workingKey === deployingKey + ? { + [workingKey]: { + production: `${workingKey}-production-new`, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [deployingKey]: { + production: `${deployingKey}-production-new`, + }, + } + ); + } + ); + }); + + describe('Deploy script when both AccessToken & ClientCredentials scripts are not working', () => { + it.each(Object.values(LogtoJwtTokenKey))('deploy %s script', (key) => { + expect( + deepmerge( + {}, + { + [key]: { + production: `${key}-production-new`, + }, + } + ) + ).toEqual({ + [key]: { + production: `${key}-production-new`, + }, + }); + }); + }); +}); + +describe('Test undeploy custom JWT script', () => { + describe('Undeploy script when both AccessToken & ClientCredentials scripts are working', () => { + it.each(Object.values(LogtoJwtTokenKey))('undeploy %s script', (key) => { + expect( + deepmerge( + { + [LogtoJwtTokenKey.AccessToken]: { + production: `${LogtoJwtTokenKey.AccessToken}-production`, + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: `${LogtoJwtTokenKey.ClientCredentials}-production`, + }, + }, + { + [key]: { + production: undefined, + }, + } + ) + ).toEqual({ + [LogtoJwtTokenKey.AccessToken]: { + production: + key === LogtoJwtTokenKey.AccessToken + ? undefined + : `${LogtoJwtTokenKey.AccessToken}-production`, + }, + [LogtoJwtTokenKey.ClientCredentials]: { + production: + key === LogtoJwtTokenKey.ClientCredentials + ? undefined + : `${LogtoJwtTokenKey.ClientCredentials}-production`, + }, + }); + }); + }); + + describe('Undeploy script:', () => { + // Test it.each() can not be nested, so we have to test each key separately. + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.AccessToken} script is working, undeploy $s script`, + (undeployingKey) => { + const workingKey = LogtoJwtTokenKey.AccessToken; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [undeployingKey]: { + production: undefined, + }, + }) + ).toEqual( + workingKey === undeployingKey + ? { + [workingKey]: { + production: undefined, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [undeployingKey]: { + production: undefined, + }, + } + ); + } + ); + + it.each(Object.values(LogtoJwtTokenKey))( + `when ${LogtoJwtTokenKey.ClientCredentials} script is working, undeploy $s script`, + (undeployingKey) => { + const workingKey = LogtoJwtTokenKey.ClientCredentials; + const workingScript = { + [workingKey]: { + production: `${workingKey}-production`, + }, + }; + expect( + deepmerge(workingScript, { + [undeployingKey]: { + production: undefined, + }, + }) + ).toEqual( + workingKey === undeployingKey + ? { + [workingKey]: { + production: undefined, + }, + } + : { + [workingKey]: { + production: `${workingKey}-production`, + }, + [undeployingKey]: { + production: undefined, + }, + } + ); + } + ); + }); +}); diff --git a/packages/core/src/libraries/logto-config.ts b/packages/core/src/libraries/logto-config.ts index 0a5f203d8..2aaf2453a 100644 --- a/packages/core/src/libraries/logto-config.ts +++ b/packages/core/src/libraries/logto-config.ts @@ -10,12 +10,16 @@ import { } from '@logto/schemas'; import { assert } from '@silverhand/essentials'; import chalk from 'chalk'; +import deepmerge from 'deepmerge'; import { ZodError, z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; import { consoleLog } from '#src/utils/console.js'; -import { getJwtCustomizerScripts } from '#src/utils/custom-jwt.js'; +import { + getJwtCustomizerScripts, + type CustomJwtDeployRequestBody, +} from '#src/utils/custom-jwt/index.js'; import { type CloudConnectionLibrary } from './cloud-connection.js'; @@ -160,17 +164,22 @@ export const createLogtoConfigLibrary = ({ const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers); - const newCustomizerScripts: { [key in LogtoJwtTokenKey]?: string } = { - [payload.key]: payload.value.script, + const newCustomizerScripts: CustomJwtDeployRequestBody = { + /** + * Only add `/test` endpoint for Cloudflare workers when testing. + * O/w overwrite the existing JWT customizer script. + */ + [payload.key]: payload.isTest + ? { + test: payload.value.script, + } + : { + production: payload.value.script, + }, }; await client.put(`/api/services/custom-jwt/worker`, { - body: { - production: payload.isTest - ? customizerScriptsFromDatabase - : { ...customizerScriptsFromDatabase, ...newCustomizerScripts }, - test: payload.isTest ? newCustomizerScripts : undefined, - }, + body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts), }); }; @@ -191,16 +200,17 @@ export const createLogtoConfigLibrary = ({ return; } - // Remove the JWT customizer script from the existing JWT customizer scripts and redeploy. + // Remove the JWT customizer script (of given `key`) from the existing JWT customizer scripts and redeploy. const customizerScriptsFromDatabase = getJwtCustomizerScripts(jwtCustomizers); + const newCustomizerScripts: CustomJwtDeployRequestBody = { + [key]: { + production: undefined, + test: undefined, + }, + }; await client.put(`/api/services/custom-jwt/worker`, { - body: { - production: { - ...customizerScriptsFromDatabase, - [key]: undefined, - }, - }, + body: deepmerge(customizerScriptsFromDatabase, newCustomizerScripts), }); }; diff --git a/packages/core/src/oidc/extra-token-claims.ts b/packages/core/src/oidc/extra-token-claims.ts index 34e38ff31..139f518cd 100644 --- a/packages/core/src/oidc/extra-token-claims.ts +++ b/packages/core/src/oidc/extra-token-claims.ts @@ -139,6 +139,7 @@ export const getExtraTokenClaimsForJwtCustomization = async ( // eslint-disable-next-line no-restricted-syntax context: { user: logtoUserInfo as Record }, }, + search: {}, }); } catch (error: unknown) { const entry = new LogEntry( diff --git a/packages/core/src/routes/logto-config/jwt-customizer.test.ts b/packages/core/src/routes/logto-config/jwt-customizer.test.ts index 90662f467..ae763299a 100644 --- a/packages/core/src/routes/logto-config/jwt-customizer.test.ts +++ b/packages/core/src/routes/logto-config/jwt-customizer.test.ts @@ -174,6 +174,9 @@ describe('configs JWT customizer routes', () => { expect(mockCloudClient.post).toHaveBeenCalledWith('/api/services/custom-jwt', { body: payload, + search: { + isTest: 'true', + }, }); expect(response.status).toEqual(200); diff --git a/packages/core/src/routes/logto-config/jwt-customizer.ts b/packages/core/src/routes/logto-config/jwt-customizer.ts index 2ca1afe8e..ec86e92ba 100644 --- a/packages/core/src/routes/logto-config/jwt-customizer.ts +++ b/packages/core/src/routes/logto-config/jwt-customizer.ts @@ -236,6 +236,7 @@ export default function logtoConfigJwtCustomizerRoutes( try { ctx.body = await client.post(`/api/services/custom-jwt`, { body, + search: { isTest: 'true' }, }); } catch (error: unknown) { /** diff --git a/packages/core/src/utils/custom-jwt.ts b/packages/core/src/utils/custom-jwt/custom-jwt.ts similarity index 60% rename from packages/core/src/utils/custom-jwt.ts rename to packages/core/src/utils/custom-jwt/custom-jwt.ts index 7d8e1c0cf..d7f194db8 100644 --- a/packages/core/src/utils/custom-jwt.ts +++ b/packages/core/src/utils/custom-jwt/custom-jwt.ts @@ -1,8 +1,10 @@ import { LogtoJwtTokenKey, type JwtCustomizerType } from '@logto/schemas'; +import { type CustomJwtDeployRequestBody } from './types.js'; + export const getJwtCustomizerScripts = (jwtCustomizers: Partial) => { // eslint-disable-next-line no-restricted-syntax -- enable to infer the type using `Object.fromEntries` return Object.fromEntries( - Object.values(LogtoJwtTokenKey).map((key) => [key, jwtCustomizers[key]?.script]) - ) as { [key in LogtoJwtTokenKey]?: string }; + Object.values(LogtoJwtTokenKey).map((key) => [key, { production: jwtCustomizers[key]?.script }]) + ) as CustomJwtDeployRequestBody; }; diff --git a/packages/core/src/utils/custom-jwt/index.ts b/packages/core/src/utils/custom-jwt/index.ts new file mode 100644 index 000000000..71c629ee9 --- /dev/null +++ b/packages/core/src/utils/custom-jwt/index.ts @@ -0,0 +1,2 @@ +export * from './custom-jwt.js'; +export * from './types.js'; diff --git a/packages/core/src/utils/custom-jwt/types.ts b/packages/core/src/utils/custom-jwt/types.ts new file mode 100644 index 000000000..a55f39f9f --- /dev/null +++ b/packages/core/src/utils/custom-jwt/types.ts @@ -0,0 +1,8 @@ +import type router from '@logto/cloud/routes'; +import { type GuardedPayload, type RouterRoutes } from '@withtyped/client'; + +type PutRoutes = RouterRoutes['put']; + +export type CustomJwtDeployRequestBody = GuardedPayload< + PutRoutes['/api/services/custom-jwt/worker'] +>['body']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0d3b5f94..fe4da768e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1211,8 +1211,8 @@ importers: version: 3.22.4 devDependencies: '@logto/cloud': - specifier: 0.2.5-821690c - version: 0.2.5-821690c(zod@3.22.4) + specifier: 0.2.5-e5d8200 + version: 0.2.5-e5d8200(zod@3.22.4) '@rollup/plugin-commonjs': specifier: ^25.0.7 version: 25.0.7(rollup@4.12.0) @@ -2688,8 +2688,8 @@ importers: specifier: ^29.5.0 version: 29.5.0 '@logto/cloud': - specifier: 0.2.5-821690c - version: 0.2.5-821690c(zod@3.22.4) + specifier: 0.2.5-e5d8200 + version: 0.2.5-e5d8200(zod@3.22.4) '@logto/connector-kit': specifier: workspace:^3.0.0 version: link:../toolkit/connector-kit @@ -3184,8 +3184,8 @@ importers: version: 3.22.4 devDependencies: '@logto/cloud': - specifier: 0.2.5-821690c - version: 0.2.5-821690c(zod@3.22.4) + specifier: 0.2.5-e5d8200 + version: 0.2.5-e5d8200(zod@3.22.4) '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3) @@ -6690,8 +6690,8 @@ packages: jose: 5.2.2 dev: true - /@logto/cloud@0.2.5-821690c(zod@3.22.4): - resolution: {integrity: sha512-eVTlJxknWbvmaeaitKzPPMTx6C4GK4TLTb97hFr91E2u6SwKP+csE3oMBgL7ZdoDLOGG+nY+j08JpVMQ8QdOWw==} + /@logto/cloud@0.2.5-e5d8200(zod@3.22.4): + resolution: {integrity: sha512-/ZPaiIU7ORfKtNqsopVg4jxDt/sM6CGAGny06ppv/2FNsL7h8rX6JXOUyyKmT6ffCh/K/5s2HZe7v86zx5gENQ==} engines: {node: ^20.9.0} dependencies: '@silverhand/essentials': 2.9.0 @@ -17833,6 +17833,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 801eae87d2e2a7f049110c922f5d350ba1f33eea Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 18 Apr 2024 19:45:50 +0800 Subject: [PATCH 2/4] chore: apply suggestions from code review Co-authored-by: Gao Sun --- .../logto-config.script-merging.test.ts | 142 +++++++++--------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/core/src/libraries/logto-config.script-merging.test.ts b/packages/core/src/libraries/logto-config.script-merging.test.ts index 2f1bd546f..9dacab08c 100644 --- a/packages/core/src/libraries/logto-config.script-merging.test.ts +++ b/packages/core/src/libraries/logto-config.script-merging.test.ts @@ -3,7 +3,7 @@ import { cond } from '@silverhand/essentials'; import deepmerge from 'deepmerge'; describe('Test the deploy custom JWT script', () => { - describe('Test script when both AccessToken & ClientCredentials scripts are working', () => { + describe('Test script when both AccessToken & ClientCredentials scripts are existing', () => { it.each(Object.values(LogtoJwtTokenKey))('test %s script', (key) => { expect( deepmerge( @@ -37,31 +37,31 @@ describe('Test the deploy custom JWT script', () => { describe('Test script:', () => { // Test it.each() can not be nested, so we have to test each key separately. it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.AccessToken} script is working, test $s script`, + `when ${LogtoJwtTokenKey.AccessToken} script is existing, test $s script`, (testingKey) => { - const workingKey = LogtoJwtTokenKey.AccessToken; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.AccessToken; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [testingKey]: { test: `${testingKey}-test`, }, }) ).toEqual( - workingKey === testingKey + existingKey === testingKey ? { - [workingKey]: { - production: `${workingKey}-production`, - test: `${workingKey}-test`, + [existingKey]: { + production: `${existingKey}-production`, + test: `${existingKey}-test`, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [testingKey]: { test: `${testingKey}-test`, @@ -72,31 +72,31 @@ describe('Test the deploy custom JWT script', () => { ); it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.ClientCredentials} script is working, test $s script`, + `when ${LogtoJwtTokenKey.ClientCredentials} script is existing, test $s script`, (testingKey) => { - const workingKey = LogtoJwtTokenKey.ClientCredentials; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.ClientCredentials; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [testingKey]: { test: `${testingKey}-test`, }, }) ).toEqual( - workingKey === testingKey + existingKey === testingKey ? { - [workingKey]: { - production: `${workingKey}-production`, - test: `${workingKey}-test`, + [existingKey]: { + production: `${existingKey}-production`, + test: `${existingKey}-test`, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [testingKey]: { test: `${testingKey}-test`, @@ -107,7 +107,7 @@ describe('Test the deploy custom JWT script', () => { ); }); - describe('Test script when both AccessToken & ClientCredentials scripts are not working', () => { + describe('Test script when both AccessToken & ClientCredentials scripts are not existing', () => { it.each(Object.values(LogtoJwtTokenKey))('test %s script', (key) => { expect( deepmerge( @@ -124,7 +124,7 @@ describe('Test the deploy custom JWT script', () => { }); describe('Test deploy custom JWT script', () => { - describe('Deploy script when both AccessToken & ClientCredentials scripts are working', () => { + describe('Deploy script when both AccessToken & ClientCredentials scripts are existing', () => { it.each(Object.values(LogtoJwtTokenKey))('deploy %s script', (key) => { expect( deepmerge( @@ -160,30 +160,30 @@ describe('Test deploy custom JWT script', () => { describe('Deploy script:', () => { // Test it.each() can not be nested, so we have to test each key separately. it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.AccessToken} script is working, deploy $s script`, + `when ${LogtoJwtTokenKey.AccessToken} script is existing, deploy $s script`, (deployingKey) => { - const workingKey = LogtoJwtTokenKey.AccessToken; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.AccessToken; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [deployingKey]: { production: `${deployingKey}-production-new`, }, }) ).toEqual( - workingKey === deployingKey + existingKey === deployingKey ? { - [workingKey]: { - production: `${workingKey}-production-new`, + [existingKey]: { + production: `${existingKey}-production-new`, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [deployingKey]: { production: `${deployingKey}-production-new`, @@ -194,30 +194,30 @@ describe('Test deploy custom JWT script', () => { ); it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.ClientCredentials} script is working, deploy $s script`, + `when ${LogtoJwtTokenKey.ClientCredentials} script is existing, deploy $s script`, (deployingKey) => { - const workingKey = LogtoJwtTokenKey.ClientCredentials; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.ClientCredentials; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [deployingKey]: { production: `${deployingKey}-production-new`, }, }) ).toEqual( - workingKey === deployingKey + existingKey === deployingKey ? { - [workingKey]: { - production: `${workingKey}-production-new`, + [existingKey]: { + production: `${existingKey}-production-new`, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [deployingKey]: { production: `${deployingKey}-production-new`, @@ -228,7 +228,7 @@ describe('Test deploy custom JWT script', () => { ); }); - describe('Deploy script when both AccessToken & ClientCredentials scripts are not working', () => { + describe('Deploy script when both AccessToken & ClientCredentials scripts are not existing', () => { it.each(Object.values(LogtoJwtTokenKey))('deploy %s script', (key) => { expect( deepmerge( @@ -249,7 +249,7 @@ describe('Test deploy custom JWT script', () => { }); describe('Test undeploy custom JWT script', () => { - describe('Undeploy script when both AccessToken & ClientCredentials scripts are working', () => { + describe('Undeploy script when both AccessToken & ClientCredentials scripts are existing', () => { it.each(Object.values(LogtoJwtTokenKey))('undeploy %s script', (key) => { expect( deepmerge( @@ -287,30 +287,30 @@ describe('Test undeploy custom JWT script', () => { describe('Undeploy script:', () => { // Test it.each() can not be nested, so we have to test each key separately. it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.AccessToken} script is working, undeploy $s script`, + `when ${LogtoJwtTokenKey.AccessToken} script is existing, undeploy $s script`, (undeployingKey) => { - const workingKey = LogtoJwtTokenKey.AccessToken; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.AccessToken; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [undeployingKey]: { production: undefined, }, }) ).toEqual( - workingKey === undeployingKey + existingKey === undeployingKey ? { - [workingKey]: { + [existingKey]: { production: undefined, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [undeployingKey]: { production: undefined, @@ -321,30 +321,30 @@ describe('Test undeploy custom JWT script', () => { ); it.each(Object.values(LogtoJwtTokenKey))( - `when ${LogtoJwtTokenKey.ClientCredentials} script is working, undeploy $s script`, + `when ${LogtoJwtTokenKey.ClientCredentials} script is existing, undeploy $s script`, (undeployingKey) => { - const workingKey = LogtoJwtTokenKey.ClientCredentials; - const workingScript = { - [workingKey]: { - production: `${workingKey}-production`, + const existingKey = LogtoJwtTokenKey.ClientCredentials; + const existingScript = { + [existingKey]: { + production: `${existingKey}-production`, }, }; expect( - deepmerge(workingScript, { + deepmerge(existingScript, { [undeployingKey]: { production: undefined, }, }) ).toEqual( - workingKey === undeployingKey + existingKey === undeployingKey ? { - [workingKey]: { + [existingKey]: { production: undefined, }, } : { - [workingKey]: { - production: `${workingKey}-production`, + [existingKey]: { + production: `${existingKey}-production`, }, [undeployingKey]: { production: undefined, From e035377b8377ff82c7b829e0064f7f02aac526b4 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 18 Apr 2024 20:21:54 +0800 Subject: [PATCH 3/4] chore: update util function interface and update comments --- packages/core/src/libraries/logto-config.ts | 24 ++++++++++--------- .../logto-config/jwt-customizer.test.ts | 4 +++- .../src/routes/logto-config/jwt-customizer.ts | 4 +++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/core/src/libraries/logto-config.ts b/packages/core/src/libraries/logto-config.ts index 2aaf2453a..333586b27 100644 --- a/packages/core/src/libraries/logto-config.ts +++ b/packages/core/src/libraries/logto-config.ts @@ -147,14 +147,14 @@ export const createLogtoConfigLibrary = ({ * @params payload - The latest JWT customizer payload needs to be deployed. * @params payload.key - The tokenType of the JWT customizer. * @params payload.value - JWT customizer value - * @params payload.isTest - Whether the JWT customizer is for test environment. + * @params payload.useCase - The use case of JWT customizer script, can be either `test` or `production`. */ const deployJwtCustomizerScript = async ( cloudConnection: CloudConnectionLibrary, payload: { key: T; value: JwtCustomizerType[T]; - isTest?: boolean; + useCase: 'test' | 'production'; } ) => { const [client, jwtCustomizers] = await Promise.all([ @@ -166,16 +166,18 @@ export const createLogtoConfigLibrary = ({ const newCustomizerScripts: CustomJwtDeployRequestBody = { /** - * Only add `/test` endpoint for Cloudflare workers when testing. - * O/w overwrite the existing JWT customizer script. + * There are at most 4 custom JWT scripts in the `CustomJwtDeployRequestBody`-typed object, + * and can be indexed by `data[CustomJwtType][UseCase]`. + * + * Per our design, each script will be deployed as a API endpoint in the Cloudflare + * worker service. A production script will be deployed to `/api/custom-jwt` + * endpoint and a test script will be deployed to `/api/custom-jwt/test` endpoint. + * + * If the current use case is `test`, then the script should be deployed to a `/test` endpoint; + * otherwise, the script should be deployed to the `/api/custom-jwt` endpoint and overwrite + * previous handler of the API endpoint. */ - [payload.key]: payload.isTest - ? { - test: payload.value.script, - } - : { - production: payload.value.script, - }, + [payload.key]: { [payload.useCase]: payload.value.script }, }; await client.put(`/api/services/custom-jwt/worker`, { diff --git a/packages/core/src/routes/logto-config/jwt-customizer.test.ts b/packages/core/src/routes/logto-config/jwt-customizer.test.ts index ae763299a..88c0424c7 100644 --- a/packages/core/src/routes/logto-config/jwt-customizer.test.ts +++ b/packages/core/src/routes/logto-config/jwt-customizer.test.ts @@ -60,6 +60,7 @@ describe('configs JWT customizer routes', () => { { key: LogtoJwtTokenKey.AccessToken, value: mockJwtCustomizerConfigForAccessToken.value, + useCase: 'production', } ); @@ -104,6 +105,7 @@ describe('configs JWT customizer routes', () => { { key: LogtoJwtTokenKey.AccessToken, value: mockJwtCustomizerConfigForAccessToken.value, + useCase: 'production', } ); @@ -168,7 +170,7 @@ describe('configs JWT customizer routes', () => { { key: LogtoJwtTokenKey.ClientCredentials, value: payload, - isTest: true, + useCase: 'test', } ); diff --git a/packages/core/src/routes/logto-config/jwt-customizer.ts b/packages/core/src/routes/logto-config/jwt-customizer.ts index ec86e92ba..da629a241 100644 --- a/packages/core/src/routes/logto-config/jwt-customizer.ts +++ b/packages/core/src/routes/logto-config/jwt-customizer.ts @@ -86,6 +86,7 @@ export default function logtoConfigJwtCustomizerRoutes( await deployJwtCustomizerScript(cloudConnection, { key, value: body, + useCase: 'production', }); } @@ -129,6 +130,7 @@ export default function logtoConfigJwtCustomizerRoutes( await deployJwtCustomizerScript(cloudConnection, { key, value: body, + useCase: 'production', }); } @@ -228,7 +230,7 @@ export default function logtoConfigJwtCustomizerRoutes( ? LogtoJwtTokenKey.AccessToken : LogtoJwtTokenKey.ClientCredentials, value: body, - isTest: true, + useCase: 'test', }); const client = await cloudConnection.getClient(); From 959f2d2795cf0653c8838f29da80a93fdfa64561 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Fri, 19 Apr 2024 00:34:50 +0800 Subject: [PATCH 4/4] chore(console): update custom JWT scripts sample (#5747) --- .../SettingsSection/InstructionTab/index.tsx | 1 + .../CustomizeJwtDetails/utils/config.tsx | 86 +++++++------------ 2 files changed, 31 insertions(+), 56 deletions(-) diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx index 7edcb4896..f34685c80 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx +++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/SettingsSection/InstructionTab/index.tsx @@ -111,6 +111,7 @@ function InstructionTab({ isActive }: Props) { language="typescript" className={styles.sampleCode} value={environmentVariablesCodeExample} + path="file:///env-variables-sample.js" height="400px" theme="logto-dark" options={sampleCodeEditorOptions} diff --git a/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx b/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx index 4264623ce..7fc158059 100644 --- a/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx +++ b/packages/console/src/pages/CustomizeJwtDetails/utils/config.tsx @@ -20,70 +20,43 @@ import { * JWT token code editor configuration */ const accessTokenJwtCustomizerDefinition = ` -declare global { - export interface CustomJwtClaims extends Record {} +declare interface CustomJwtClaims extends Record {} - /** Logto internal data that can be used to pass additional information - * @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} user - The user info associated with the token. - */ - export type Data = { - user: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}; - } - - export interface Exports { - /** - * This function is called during the access token generation process to get custom claims for the JWT token. - * - * @param {${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}} token -The JWT token. - * @param {Data} data - Logto internal data that can be used to pass additional information - * @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} data.user - The user info associated with the token. - * @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} envVariables - The environment variables. - * - * @returns The custom claims. - */ - getCustomJwtClaims: (token: ${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}, data: Data, envVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}) => Promise; - } - - const exports: Exports; +/** Logto internal data that can be used to pass additional information + * @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} user - The user info associated with the token. + */ +declare type Context = { + user: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}; } -export { exports as default }; -`; +declare type Payload = { + token: ${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}; + context: Context; + environmentVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}; +};`; const clientCredentialsJwtCustomizerDefinition = ` -declare global { - export interface CustomJwtClaims extends Record {} +declare interface CustomJwtClaims extends Record {} - export interface Exports { - /** - * This function is called during the access token generation process to get custom claims for the JWT token. - * - * @param {${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}} token -The JWT token. - * - * @returns The custom claims. - */ - getCustomJwtClaims: (token: ${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}, envVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}) => Promise; - } - - const exports: Exports; -} - -export { exports as default }; -`; +declare type Payload = { + token: ${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}; + environmentVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}; +};`; export const defaultAccessTokenJwtCustomizerCode = `/** * This function is called during the access token generation process to get custom claims for the JWT token. * Limit custom claims to under 50KB. -* -* @param {${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}} token -The JWT token. -* @param {Data} data - Logto internal data that can be used to pass additional information -* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} data.user - The user info associated with the token. -* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [envVariables] - The environment variables. +* +* @param {Payload} payload - The input payload of the function. +* @param {${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}} payload.token -The JWT token. +* @param {Context} payload.context - Logto internal data that can be used to pass additional information +* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} payload.context.user - The user info associated with the token. +* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [payload.environmentVariables] - The environment variables. * * @returns The custom claims. */ -exports.getCustomJwtClaims = async (token, data, envVariables) => { +const getCustomJwtClaims = async ({ token, context, environmentVariables }) => { return {}; }`; @@ -91,13 +64,14 @@ export const defaultClientCredentialsJwtCustomizerCode = `/** * This function is called during the access token generation process to get custom claims for the JWT token. * Limit custom claims to under 50KB. * -* @param {${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}} token -The JWT token. -* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [envVariables] - The environment variables. +* @param {Payload} payload - The input payload of the function. +* @param {${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}} payload.token -The JWT token. +* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [payload.environmentVariables] - The environment variables. * * @returns The custom claims. */ -exports.getCustomJwtClaims = async (token, envVariables) => { +const getCustomJwtClaims = async ({ token, environmentVariables }) => { return {}; }`; @@ -170,15 +144,15 @@ return { externalData: data, };`; -export const environmentVariablesCodeExample = `exports.getCustomJwtClaims = async (token, data, envVariables) => { - const { apiKey } = envVariables; +export const environmentVariablesCodeExample = `const getCustomJwtClaimsSample = async ({ environmentVariables }) => { + const { apiKey } = environmentVariables; const response = await fetch('https://api.example.com/data', { headers: { Authorization: apiKey, } }); - + const data = await response.json(); return {