diff --git a/packages/cli/package.json b/packages/cli/package.json index e93d01f70..de2deeb2c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -47,7 +47,7 @@ "@logto/core-kit": "workspace:*", "@logto/schemas": "workspace:*", "@logto/shared": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "chalk": "^5.0.0", "decamelize": "^6.0.0", "dotenv": "^16.0.0", diff --git a/packages/cloud/package.json b/packages/cloud/package.json index 92cc56ae6..8b0a749aa 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -28,7 +28,7 @@ "@logto/core-kit": "workspace:*", "@logto/schemas": "workspace:*", "@logto/shared": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@withtyped/postgres": "^0.8.1", "@withtyped/server": "^0.8.1", "accepts": "^1.3.8", diff --git a/packages/console/package.json b/packages/console/package.json index 31da40e7d..b6cec53ae 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -35,7 +35,7 @@ "@parcel/transformer-svg-react": "2.8.3", "@silverhand/eslint-config": "2.0.1", "@silverhand/eslint-config-react": "2.0.1", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@silverhand/ts-config": "2.0.3", "@silverhand/ts-config-react": "2.0.3", "@tsconfig/docusaurus": "^1.0.5", diff --git a/packages/core/package.json b/packages/core/package.json index 53268eeed..0b5ebed44 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,7 +35,7 @@ "@logto/phrases-ui": "workspace:*", "@logto/schemas": "workspace:*", "@logto/shared": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "aws-sdk": "^2.1329.0", "chalk": "^5.0.0", "clean-deep": "^3.4.0", @@ -51,6 +51,7 @@ "iconv-lite": "0.6.3", "jose": "^4.11.0", "js-yaml": "^4.1.0", + "keyv": "^4.5.2", "koa": "^2.13.1", "koa-body": "^5.0.0", "koa-compose": "^4.1.0", @@ -63,6 +64,7 @@ "lru-cache": "^7.14.1", "nanoid": "^4.0.0", "oidc-provider": "^8.0.0", + "p-memoize": "^7.1.1", "p-retry": "^5.1.2", "pg-protocol": "^1.6.0", "roarr": "^7.11.0", diff --git a/packages/core/src/caches/well-known.ts b/packages/core/src/caches/well-known.ts new file mode 100644 index 000000000..3e9901b37 --- /dev/null +++ b/packages/core/src/caches/well-known.ts @@ -0,0 +1,30 @@ +import Keyv from 'keyv'; +import type { AnyAsyncFunction } from 'p-memoize'; +import pMemoize from 'p-memoize'; + +const cacheKeys = Object.freeze(['sie', 'sie-full', 'phrases', 'lng-tags'] as const); + +/** Well-known data type key for cache. */ +export type WellKnownCacheKey = (typeof cacheKeys)[number]; + +// Not sure if we need guard value for `.has()` and `.get()`, +// trust cache value for now. +const wellKnownCache = new Keyv({ ttl: 300_000 /* 5 minutes */ }); + +/** + * Use for centralized well-known data caching. + * + * WARN: You should store only well-known (public) data since it's a central cache. + */ +export const useWellKnownCache = <FunctionToMemoize extends AnyAsyncFunction>( + tenantId: string, + key: WellKnownCacheKey, + run: FunctionToMemoize +) => + pMemoize(run, { + cacheKey: () => `${tenantId}:${key}`, + cache: wellKnownCache, + }); + +export const invalidateWellKnownCache = async (tenantId: string) => + wellKnownCache.delete(cacheKeys.map((key) => `${tenantId}:${key}` as const)); diff --git a/packages/core/src/libraries/phrase.test.ts b/packages/core/src/libraries/phrase.test.ts index f9faea10f..8ab9410dd 100644 --- a/packages/core/src/libraries/phrase.test.ts +++ b/packages/core/src/libraries/phrase.test.ts @@ -11,6 +11,7 @@ import { zhCnTag, zhHkTag, } from '#src/__mocks__/custom-phrase.js'; +import { invalidateWellKnownCache } from '#src/caches/well-known.js'; import RequestError from '#src/errors/RequestError/index.js'; import { MockQueries } from '#src/test-utils/tenant.js'; @@ -41,12 +42,15 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => { return mockCustomPhrase; }); +const tenantId = 'mock_id'; const { createPhraseLibrary } = await import('#src/libraries/phrase.js'); const { getPhrases } = createPhraseLibrary( - new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } }) + new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } }), + tenantId ); -afterEach(() => { +afterEach(async () => { + await invalidateWellKnownCache(tenantId); jest.clearAllMocks(); }); diff --git a/packages/core/src/libraries/phrase.ts b/packages/core/src/libraries/phrase.ts index bb6f163c5..05743c0ae 100644 --- a/packages/core/src/libraries/phrase.ts +++ b/packages/core/src/libraries/phrase.ts @@ -4,12 +4,16 @@ import type { CustomPhrase } from '@logto/schemas'; import cleanDeep from 'clean-deep'; import deepmerge from 'deepmerge'; +import { useWellKnownCache } from '#src/caches/well-known.js'; import type Queries from '#src/tenants/Queries.js'; -export const createPhraseLibrary = (queries: Queries) => { - const { findCustomPhraseByLanguageTag } = queries.customPhrases; +export const createPhraseLibrary = (queries: Queries, tenantId: string) => { + const { findCustomPhraseByLanguageTag, findAllCustomLanguageTags } = queries.customPhrases; - const getPhrases = async (supportedLanguage: string, customLanguages: string[]) => { + const _getPhrases = async ( + supportedLanguage: string, + customLanguages: string[] + ): Promise<LocalePhrase> => { if (!isBuiltInLanguageTag(supportedLanguage)) { return deepmerge<LocalePhrase, CustomPhrase>( resource.en, @@ -27,5 +31,18 @@ export const createPhraseLibrary = (queries: Queries) => { ); }; - return { getPhrases }; + const getPhrases = useWellKnownCache(tenantId, 'phrases', _getPhrases); + + const getAllCustomLanguageTags = useWellKnownCache( + tenantId, + 'lng-tags', + findAllCustomLanguageTags + ); + + return { + /** NOTE: This function is cached by the first parameter. */ + getPhrases, + /** NOTE: This function is cached. */ + getAllCustomLanguageTags, + }; }; 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 b48305b2d..660e82e26 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -42,7 +42,7 @@ const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); const { createSignInExperienceLibrary } = await import('./index.js'); const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } = - createSignInExperienceLibrary(queries, connectorLibrary); + createSignInExperienceLibrary(queries, connectorLibrary, 'mock_id'); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 5d055b63f..d9f14ab54 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -1,8 +1,11 @@ +import { connectorMetadataGuard } from '@logto/connector-kit'; import { builtInLanguages } from '@logto/phrases-ui'; -import type { LanguageInfo, SignInExperience } from '@logto/schemas'; -import { ConnectorType } from '@logto/schemas'; +import type { ConnectorMetadata, LanguageInfo, SignInExperience } from '@logto/schemas'; +import { SignInExperiences, ConnectorType } from '@logto/schemas'; import { deduplicate } from '@silverhand/essentials'; +import { z } from 'zod'; +import { useWellKnownCache } from '#src/caches/well-known.js'; import RequestError from '#src/errors/RequestError/index.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; import type Queries from '#src/tenants/Queries.js'; @@ -15,12 +18,12 @@ export type SignInExperienceLibrary = ReturnType<typeof createSignInExperienceLi export const createSignInExperienceLibrary = ( queries: Queries, - connectorLibrary: ConnectorLibrary + { getLogtoConnectors }: ConnectorLibrary, + tenantId: string ) => { const { customPhrases: { findAllCustomLanguageTags }, signInExperiences: { findDefaultSignInExperience, updateDefaultSignInExperience }, - users: { hasActiveUsers }, } = queries; const validateLanguageInfo = async (languageInfo: LanguageInfo) => { @@ -36,7 +39,7 @@ export const createSignInExperienceLibrary = ( }; const removeUnavailableSocialConnectorTargets = async () => { - const connectors = await connectorLibrary.getLogtoConnectors(); + const connectors = await getLogtoConnectors(); const availableSocialConnectorTargets = deduplicate( connectors .filter(({ type }) => type === ConnectorType.Social) @@ -52,11 +55,65 @@ export const createSignInExperienceLibrary = ( }); }; - const getSignInExperience = async (): Promise<SignInExperience> => findDefaultSignInExperience(); + const getSignInExperience = useWellKnownCache(tenantId, 'sie', findDefaultSignInExperience); + + const _getFullSignInExperience = async (): Promise<FullSignInExperience> => { + const [signInExperience, logtoConnectors] = await Promise.all([ + getSignInExperience(), + getLogtoConnectors(), + ]); + + const forgotPassword = { + phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms), + email: logtoConnectors.some(({ type }) => type === ConnectorType.Email), + }; + + const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce< + Array<ConnectorMetadata & { id: string }> + >((previous, connectorTarget) => { + const connectors = logtoConnectors.filter( + ({ metadata: { target } }) => target === connectorTarget + ); + + return [ + ...previous, + ...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })), + ]; + }, []); + + return { + ...signInExperience, + socialConnectors, + forgotPassword, + }; + }; + + const getFullSignInExperience = useWellKnownCache(tenantId, 'sie-full', _getFullSignInExperience); return { validateLanguageInfo, removeUnavailableSocialConnectorTargets, + /** NOTE: This function is cached. */ getSignInExperience, + /** NOTE: This function is cached. */ + getFullSignInExperience, }; }; + +export type ForgotPassword = { + phone: boolean; + email: boolean; +}; + +export type ConnectorMetadataWithId = ConnectorMetadata & { id: string }; + +export type FullSignInExperience = SignInExperience & { + socialConnectors: ConnectorMetadataWithId[]; + forgotPassword: ForgotPassword; +}; + +export const guardFullSignInExperience: z.ZodType<FullSignInExperience> = + SignInExperiences.guard.extend({ + socialConnectors: connectorMetadataGuard.extend({ id: z.string() }).array(), + forgotPassword: z.object({ phone: z.boolean(), email: z.boolean() }), + }); diff --git a/packages/core/src/queries/custom-phrase.ts b/packages/core/src/queries/custom-phrase.ts index bb65c2fd6..369fbfddc 100644 --- a/packages/core/src/queries/custom-phrase.ts +++ b/packages/core/src/queries/custom-phrase.ts @@ -58,6 +58,10 @@ export const createCustomPhraseQueries = (pool: CommonQueryMethods) => { }; return { + /** + * NOTE: Use `getAllCustomLanguageTags()` from phrase library + * if possible since that function leverages cache. + */ findAllCustomLanguageTags, findAllCustomPhrases, findCustomPhraseByLanguageTag, diff --git a/packages/core/src/queries/sign-in-experience.ts b/packages/core/src/queries/sign-in-experience.ts index fe1a3f09d..1740342d9 100644 --- a/packages/core/src/queries/sign-in-experience.ts +++ b/packages/core/src/queries/sign-in-experience.ts @@ -16,5 +16,12 @@ export const createSignInExperienceQueries = (pool: CommonQueryMethods) => { const findDefaultSignInExperience = async () => buildFindEntityByIdWithPool(pool)(SignInExperiences)(id); - return { updateDefaultSignInExperience, findDefaultSignInExperience }; + return { + updateDefaultSignInExperience, + /** + * NOTE: Use `getSignInExperience()` from sign-in experience library + * if possible since that function leverages cache. + */ + findDefaultSignInExperience, + }; }; diff --git a/packages/core/src/routes-me/social.ts b/packages/core/src/routes-me/social.ts index 58146a546..c543c4262 100644 --- a/packages/core/src/routes-me/social.ts +++ b/packages/core/src/routes-me/social.ts @@ -20,13 +20,11 @@ export default function socialRoutes<T extends AuthedMeRouter>( ...[router, tenant]: RouterInitArgs<T> ) { const { - libraries: { - connectors: { getLogtoConnectors, getLogtoConnectorById }, - }, queries: { users: { findUserById, updateUserById, deleteUserIdentity, hasUserWithIdentity }, signInExperiences: { findDefaultSignInExperience }, }, + connectors: { getLogtoConnectors, getLogtoConnectorById }, } = tenant; router.get('/social/connectors', async (ctx, next) => { diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index 9dca79d52..327ad7640 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -105,7 +105,9 @@ const usersLibraries = { const adminUserRoutes = await pickDefault(import('./admin-user.js')); describe('adminUserRoutes', () => { - const tenantContext = new MockTenant(undefined, mockedQueries, { users: usersLibraries }); + const tenantContext = new MockTenant(undefined, mockedQueries, undefined, { + users: usersLibraries, + }); const userRequest = createRequester({ authedRoutes: adminUserRoutes, tenantContext }); afterEach(() => { diff --git a/packages/core/src/routes/authn.test.ts b/packages/core/src/routes/authn.test.ts index 6b1c0759c..8cf1a8931 100644 --- a/packages/core/src/routes/authn.test.ts +++ b/packages/core/src/routes/authn.test.ts @@ -62,6 +62,7 @@ const usersLibraries = { const tenantContext = new MockTenant( createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), undefined, + undefined, { users: usersLibraries, socials: socialsLibraries } ); const { createRequester } = await import('#src/utils/test-utils.js'); diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index 94a207b97..e3c93a8ac 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -76,24 +76,24 @@ const tenantContext = new MockTenant( undefined, { connectors: connectorQueries }, { - signInExperiences: { removeUnavailableSocialConnectorTargets }, - connectors: { - getLogtoConnectors, - getLogtoConnectorById: async (connectorId: string) => { - const connectors = await getLogtoConnectors(); - const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId); - assertThat( - connector, - new RequestError({ - code: 'entity.not_found', - connectorId, - status: 404, - }) - ); + getLogtoConnectors, + getLogtoConnectorById: async (connectorId: string) => { + const connectors = await getLogtoConnectors(); + const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId); + assertThat( + connector, + new RequestError({ + code: 'entity.not_found', + connectorId, + status: 404, + }) + ); - return connector; - }, + return connector; }, + }, + { + signInExperiences: { removeUnavailableSocialConnectorTargets }, } ); diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 4002c8c00..440551c9d 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -21,7 +21,7 @@ import type { AuthedRouter, RouterInitArgs } from './types.js'; const generateConnectorId = buildIdGenerator(12); export default function connectorRoutes<T extends AuthedRouter>( - ...[router, { queries, libraries }]: RouterInitArgs<T> + ...[router, { queries, connectors, libraries }]: RouterInitArgs<T> ) { const { findConnectorById, @@ -31,8 +31,8 @@ export default function connectorRoutes<T extends AuthedRouter>( insertConnector, updateConnector, } = queries.connectors; + const { getLogtoConnectorById, getLogtoConnectors } = connectors; const { - connectors: { getLogtoConnectorById, getLogtoConnectors }, signInExperiences: { removeUnavailableSocialConnectorTargets }, } = libraries; diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index c87c1b375..0da984db7 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -44,10 +44,10 @@ const tenantContext = new MockTenant( undefined, { connectors: { updateConnector } }, { - connectors: { - getLogtoConnectors, - getLogtoConnectorById, - }, + getLogtoConnectors, + getLogtoConnectorById, + }, + { signInExperiences: { // eslint-disable-next-line @typescript-eslint/no-empty-function removeUnavailableSocialConnectorTargets: async () => {}, diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index 6ca2cb2da..f026a572d 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -56,7 +56,8 @@ describe('submit action', () => { const tenant = new MockTenant( undefined, { users: userQueries, signInExperiences: { updateDefaultSignInExperience: jest.fn() } }, - { users: userLibraries, connectors: { getLogtoConnectorById } } + { getLogtoConnectorById }, + { users: userLibraries } ); const ctx = { ...createContextWithRouteParameters(), diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 1a81e4d08..4b3bed01f 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -149,7 +149,7 @@ const parseUserProfile = async ( export default async function submitInteraction( interaction: VerifiedInteractionResult, ctx: WithInteractionDetailsContext, - { provider, libraries, queries }: TenantContext, + { provider, libraries, connectors, queries }: TenantContext, log?: LogEntry ) { const { hasActiveUsers, findUserById, updateUserById } = queries.users; @@ -157,7 +157,6 @@ export default async function submitInteraction( const { users: { generateUserId, insertUser }, - connectors, } = libraries; const { event, profile } = interaction; diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index 09dd93c5b..3aec98a74 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -101,21 +101,21 @@ const tenantContext = new MockTenant( createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), undefined, { - connectors: { - getLogtoConnectorById: async (connectorId: string) => { - const connector = await getLogtoConnectorByIdHelper(connectorId); + getLogtoConnectorById: async (connectorId: string) => { + const connector = await getLogtoConnectorByIdHelper(connectorId); - if (connector.type !== ConnectorType.Social) { - throw new RequestError({ - code: 'entity.not_found', - status: 404, - }); - } + if (connector.type !== ConnectorType.Social) { + throw new RequestError({ + code: 'entity.not_found', + status: 404, + }); + } - // @ts-expect-error - return connector as LogtoConnector; - }, + // @ts-expect-error + return connector as LogtoConnector; }, + }, + { signInExperiences: { getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), }, diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts index a2c8b4009..dbfcb1fe2 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts @@ -18,7 +18,7 @@ const tenantContext = new MockTenant( { users: queries, }, - { connectors: { getLogtoConnectorById } } + { getLogtoConnectorById } ); const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js')); diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts index d4ca483ac..efc523b3d 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts @@ -3,12 +3,12 @@ import type TenantContext from '#src/tenants/TenantContext.js'; import type { UserIdentity } from '../types/index.js'; export default async function findUserByIdentifier( - { queries, libraries }: TenantContext, + { queries, connectors }: TenantContext, identity: UserIdentity ) { const { findUserByEmail, findUserByUsername, findUserByPhone, findUserByIdentity } = queries.users; - const { getLogtoConnectorById } = libraries.connectors; + const { getLogtoConnectorById } = connectors; if ('username' in identity) { return findUserByUsername(identity.username); diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts index 01508ceb8..19eba8280 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.test.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts @@ -11,7 +11,9 @@ const { mockEsm } = createMockUtils(jest); const getUserInfoByAuthCode = jest.fn().mockResolvedValue({ id: 'foo' }); -const tenant = new MockTenant(undefined, undefined, { socials: { getUserInfoByAuthCode } }); +const tenant = new MockTenant(undefined, undefined, undefined, { + socials: { getUserInfoByAuthCode }, +}); mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts index 10ee4d633..b88b7be23 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.ts @@ -14,12 +14,10 @@ import type { SocialAuthorizationUrlPayload } from '../types/index.js'; export const createSocialAuthorizationUrl = async ( ctx: WithLogContext, - { provider, libraries }: TenantContext, + { provider, connectors }: TenantContext, payload: SocialAuthorizationUrlPayload ) => { - const { - connectors: { getLogtoConnectorById }, - } = libraries; + const { getLogtoConnectorById } = connectors; const { connectorId, state, redirectUri } = payload; assertThat(state && redirectUri, 'session.insufficient_info'); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts index a13d88611..8917c333c 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts @@ -21,11 +21,7 @@ const getLogtoConnectorById = jest.fn().mockResolvedValue({ metadata: { target: 'logto' }, }); -const tenantContext = new MockTenant( - undefined, - { users: userQueries }, - { connectors: { getLogtoConnectorById } } -); +const tenantContext = new MockTenant(undefined, { users: userQueries }, { getLogtoConnectorById }); const verifyProfile = await pickDefault(import('./profile-verification.js')); const identifiers: Identifier[] = [ diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts index 420dc87b6..36b85cdb1 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts @@ -19,11 +19,9 @@ const tenantContext = new MockTenant( }, }, { - connectors: { - getLogtoConnectorById: jest.fn().mockResolvedValue({ - metadata: { target: 'logto' }, - }), - }, + getLogtoConnectorById: jest.fn().mockResolvedValue({ + metadata: { target: 'logto' }, + }), } ); const verifyProfile = await pickDefault(import('./profile-verification.js')); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts index fef88d309..066b40ce0 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts @@ -58,7 +58,7 @@ const verifyProfileIdentifiers = ( }; const verifyProfileNotRegisteredByOtherUserAccount = async ( - { queries, libraries }: TenantContext, + { queries, connectors }: TenantContext, { username, email, phone, connectorId }: Profile, identifiers: Identifier[] = [] ) => { @@ -97,7 +97,7 @@ const verifyProfileNotRegisteredByOtherUserAccount = async ( if (connectorId) { const { metadata: { target }, - } = await libraries.connectors.getLogtoConnectorById(connectorId); + } = await connectors.getLogtoConnectorById(connectorId); const socialIdentifier = identifiers.find( (identifier): identifier is SocialIdentifier => identifier.key === 'social' diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts index caa019c17..5f0fd92fc 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts @@ -11,11 +11,9 @@ const { mockEsmDefault } = createMockUtils(jest); const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn()); -const tenant = new MockTenant( - undefined, - {}, - { socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) } } -); +const tenant = new MockTenant(undefined, undefined, undefined, { + socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) }, +}); const verifyUserAccount = await pickDefault(import('./user-identity-verification.js')); diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts index 9614c3292..a6ef70388 100644 --- a/packages/core/src/routes/resource.test.ts +++ b/packages/core/src/routes/resource.test.ts @@ -52,7 +52,7 @@ mockEsm('@logto/core-kit', () => ({ buildIdGenerator: () => () => 'randomId', })); -const tenantContext = new MockTenant(undefined, { scopes, resources }, libraries); +const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined, libraries); const resourceRoutes = await pickDefault(import('./resource.js')); diff --git a/packages/core/src/routes/sign-in-experience/guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts index b5c29cf67..0ed434363 100644 --- a/packages/core/src/routes/sign-in-experience/guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.test.ts @@ -20,6 +20,7 @@ const tenantContext = new MockTenant( }), }, }, + undefined, { signInExperiences: { validateLanguageInfo, diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts index cd6e076ae..4304594e4 100644 --- a/packages/core/src/routes/sign-in-experience/index.test.ts +++ b/packages/core/src/routes/sign-in-experience/index.test.ts @@ -1,6 +1,9 @@ import type { SignInExperience, CreateSignInExperience } from '@logto/schemas'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createRequester } from '#src/utils/test-utils.js'; + import { mockFacebookConnector, mockGithubConnector, @@ -17,8 +20,6 @@ import { mockPrivacyPolicyUrl, mockDemoSocialConnector, } from '#src/__mocks__/index.js'; -import { MockTenant } from '#src/test-utils/tenant.js'; -import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); @@ -56,15 +57,9 @@ const mockDeleteConnectorById = jest.fn(); const tenantContext = new MockTenant( undefined, - { - signInExperiences, - customPhrases: { findAllCustomLanguageTags: async () => [] }, - connectors: { deleteConnectorById: mockDeleteConnectorById }, - }, - { - signInExperiences: { validateLanguageInfo }, - connectors: { getLogtoConnectors: mockGetLogtoConnectors }, - } + { signInExperiences, customPhrases: { findAllCustomLanguageTags: async () => [] } }, + { getLogtoConnectors: async () => logtoConnectors }, + { signInExperiences: { validateLanguageInfo } } ); const signInExperiencesRoutes = await pickDefault(import('./index.js')); diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index f260aeffa..b8c5b027d 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -8,14 +8,14 @@ import koaGuard from '#src/middleware/koa-guard.js'; import type { AuthedRouter, RouterInitArgs } from '../types.js'; export default function signInExperiencesRoutes<T extends AuthedRouter>( - ...[router, { queries, libraries }]: RouterInitArgs<T> + ...[router, { queries, libraries, connectors }]: RouterInitArgs<T> ) { const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences; const { deleteConnectorById } = queries.connectors; const { signInExperiences: { validateLanguageInfo }, - connectors: { getLogtoConnectors }, } = libraries; + const { getLogtoConnectors } = connectors; /** * As we only support single signInExperience settings for V1 diff --git a/packages/core/src/routes/verification-code.test.ts b/packages/core/src/routes/verification-code.test.ts index c890903f7..0e9f4e517 100644 --- a/packages/core/src/routes/verification-code.test.ts +++ b/packages/core/src/routes/verification-code.test.ts @@ -23,13 +23,9 @@ const passcodeQueries = await mockEsmWithActual('#src/queries/passcode.js', () = const verificationCodeRoutes = await pickDefault(import('./verification-code.js')); describe('Generic verification code flow triggered by management API', () => { - const tenantContext = new MockTenant( - undefined, - { passcodes: passcodeQueries }, - { - passcodes: passcodeLibraries, - } - ); + const tenantContext = new MockTenant(undefined, { passcodes: passcodeQueries }, undefined, { + passcodes: passcodeLibraries, + }); const verificationCodeRequest = createRequester({ authedRoutes: verificationCodeRoutes, tenantContext, diff --git a/packages/core/src/routes/well-known.phrases.content-language.test.ts b/packages/core/src/routes/well-known.phrases.content-language.test.ts index f09b21f79..2e5282c18 100644 --- a/packages/core/src/routes/well-known.phrases.content-language.test.ts +++ b/packages/core/src/routes/well-known.phrases.content-language.test.ts @@ -4,6 +4,7 @@ import { pickDefault } from '@logto/shared/esm'; import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; +import { invalidateWellKnownCache } from '#src/caches/well-known.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -29,6 +30,7 @@ const tenantContext = new MockTenant( customPhrases: { findAllCustomLanguageTags: async () => [trTrTag, zhCnTag] }, signInExperiences: { findDefaultSignInExperience }, }, + undefined, { phrases: { getPhrases: jest.fn().mockResolvedValue(en) } } ); @@ -39,7 +41,8 @@ const phraseRequest = createRequester({ tenantContext, }); -afterEach(() => { +afterEach(async () => { + await invalidateWellKnownCache(tenantContext.id); jest.clearAllMocks(); }); diff --git a/packages/core/src/routes/well-known.phrases.test.ts b/packages/core/src/routes/well-known.phrases.test.ts index eb43a0fc2..98cfa491d 100644 --- a/packages/core/src/routes/well-known.phrases.test.ts +++ b/packages/core/src/routes/well-known.phrases.test.ts @@ -4,6 +4,7 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { zhCnTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; +import { invalidateWellKnownCache } from '#src/caches/well-known.js'; import Queries from '#src/tenants/Queries.js'; import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { MockTenant } from '#src/test-utils/tenant.js'; @@ -46,6 +47,7 @@ const getPhrases = jest.fn(async () => zhCN); const tenantContext = new MockTenant( createMockProvider(), { customPhrases, signInExperiences: { findDefaultSignInExperience } }, + undefined, { phrases: { getPhrases } } ); @@ -57,11 +59,12 @@ const phraseRequest = createRequester({ tenantContext, }); -describe('when the application is not admin-console', () => { - afterEach(() => { - jest.clearAllMocks(); - }); +afterEach(async () => { + await invalidateWellKnownCache(tenantContext.id); + jest.clearAllMocks(); +}); +describe('when the application is not admin-console', () => { it('should call findDefaultSignInExperience', async () => { await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200); expect(findDefaultSignInExperience).toBeCalledTimes(1); @@ -123,4 +126,16 @@ describe('when the application is not admin-console', () => { ); expect(getPhrases).toBeCalledWith('fr', [customizedLanguage]); }); + + it('should use cache for continuous requests', async () => { + const [response1, response2, response3] = await Promise.all([ + phraseRequest.get('/.well-known/phrases'), + phraseRequest.get('/.well-known/phrases'), + phraseRequest.get('/.well-known/phrases'), + ]); + expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1); + expect(findAllCustomLanguageTags).toHaveBeenCalledTimes(1); + expect(response1.body).toStrictEqual(response2.body); + expect(response1.body).toStrictEqual(response3.body); + }); }); diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 6cefa6861..1258dd477 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -10,6 +10,7 @@ import { mockWechatConnector, mockWechatNativeConnector, } from '#src/__mocks__/index.js'; +import { invalidateWellKnownCache } from '#src/caches/well-known.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); @@ -32,34 +33,36 @@ const { createMockProvider } = await import('#src/test-utils/oidc-provider.js'); const { MockTenant } = await import('#src/test-utils/tenant.js'); const { createRequester } = await import('#src/utils/test-utils.js'); +const provider = createMockProvider(); +const getLogtoConnectors = jest.fn(async () => { + return [ + mockAliyunDmConnector, + mockAliyunSmsConnector, + mockFacebookConnector, + mockGithubConnector, + mockGoogleConnector, + mockWechatConnector, + mockWechatNativeConnector, + ]; +}); +const tenantContext = new MockTenant( + provider, + { + signInExperiences: sieQueries, + users: { hasActiveUsers: jest.fn().mockResolvedValue(true) }, + }, + { getLogtoConnectors } +); + describe('GET /.well-known/sign-in-exp', () => { - afterEach(() => { + afterEach(async () => { + await invalidateWellKnownCache(tenantContext.id); jest.clearAllMocks(); }); - const provider = createMockProvider(); const sessionRequest = createRequester({ anonymousRoutes: wellKnownRoutes, - tenantContext: new MockTenant( - provider, - { - signInExperiences: sieQueries, - users: { hasActiveUsers: jest.fn().mockResolvedValue(true) }, - }, - { - connectors: { - getLogtoConnectors: jest.fn(async () => [ - mockAliyunDmConnector, - mockAliyunSmsConnector, - mockFacebookConnector, - mockGithubConnector, - mockGoogleConnector, - mockWechatConnector, - mockWechatNativeConnector, - ]), - }, - } - ), + tenantContext, middlewares: [ async (ctx, next) => { ctx.addLogContext = jest.fn(); @@ -96,4 +99,16 @@ describe('GET /.well-known/sign-in-exp', () => { ], }); }); + + it('should use cache for continuous requests', async () => { + const [response1, response2, response3] = await Promise.all([ + sessionRequest.get('/.well-known/sign-in-exp'), + sessionRequest.get('/.well-known/sign-in-exp'), + sessionRequest.get('/.well-known/sign-in-exp'), + ]); + expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1); + expect(getLogtoConnectors).toHaveBeenCalledTimes(1); + expect(response1.body).toStrictEqual(response2.body); + expect(response2.body).toStrictEqual(response3.body); + }); }); diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 463355608..baaf12e25 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -1,27 +1,22 @@ -import type { ConnectorMetadata } from '@logto/connector-kit'; -import { ConnectorType } from '@logto/connector-kit'; import { isBuiltInLanguageTag } from '@logto/phrases-ui'; import { adminTenantId } from '@logto/schemas'; -import { object, string } from 'zod'; +import { conditionalArray } from '@silverhand/essentials'; +import { z } from 'zod'; import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import detectLanguage from '#src/i18n/detect-language.js'; +import { guardFullSignInExperience } from '#src/libraries/sign-in-experience/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import type { AnonymousRouter, RouterInitArgs } from './types.js'; export default function wellKnownRoutes<T extends AnonymousRouter>( - ...[router, { queries, libraries, id }]: RouterInitArgs<T> + ...[router, { libraries, id }]: RouterInitArgs<T> ) { const { - customPhrases: { findAllCustomLanguageTags }, - signInExperiences: { findDefaultSignInExperience }, - } = queries; - const { - signInExperiences: { getSignInExperience }, - connectors: { getLogtoConnectors }, - phrases: { getPhrases }, + signInExperiences: { getSignInExperience, getFullSignInExperience }, + phrases: { getPhrases, getAllCustomLanguageTags }, } = libraries; if (id === adminTenantId) { @@ -38,45 +33,24 @@ export default function wellKnownRoutes<T extends AnonymousRouter>( }); } - router.get('/.well-known/sign-in-exp', async (ctx, next) => { - const [signInExperience, logtoConnectors] = await Promise.all([ - getSignInExperience(), - getLogtoConnectors(), - ]); + router.get( + '/.well-known/sign-in-exp', + koaGuard({ response: guardFullSignInExperience, status: 200 }), + async (ctx, next) => { + ctx.body = await getFullSignInExperience(); - const forgotPassword = { - phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms), - email: logtoConnectors.some(({ type }) => type === ConnectorType.Email), - }; - - const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce< - Array<ConnectorMetadata & { id: string }> - >((previous, connectorTarget) => { - const connectors = logtoConnectors.filter( - ({ metadata: { target } }) => target === connectorTarget - ); - - return [ - ...previous, - ...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })), - ]; - }, []); - - ctx.body = { - ...signInExperience, - socialConnectors, - forgotPassword, - }; - - return next(); - }); + return next(); + } + ); router.get( '/.well-known/phrases', koaGuard({ - query: object({ - lng: string().optional(), + query: z.object({ + lng: z.string().optional(), }), + response: z.record(z.string().or(z.record(z.unknown()))), + status: 200, }), async (ctx, next) => { const { @@ -85,12 +59,14 @@ export default function wellKnownRoutes<T extends AnonymousRouter>( const { languageInfo: { autoDetect, fallbackLanguage }, - } = await findDefaultSignInExperience(); + } = await getSignInExperience(); - const targetLanguage = lng ? [lng] : []; - const detectedLanguages = autoDetect ? detectLanguage(ctx) : []; - const acceptableLanguages = [...targetLanguage, ...detectedLanguages, fallbackLanguage]; - const customLanguages = await findAllCustomLanguageTags(); + const acceptableLanguages = conditionalArray<string | string[]>( + lng, + autoDetect && detectLanguage(ctx), + fallbackLanguage + ); + const customLanguages = await getAllCustomLanguageTags(); const language = acceptableLanguages.find( (tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag) diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index 9ddef88ab..88b1ee828 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -1,5 +1,5 @@ import { createApplicationLibrary } from '#src/libraries/application.js'; -import { createConnectorLibrary } from '#src/libraries/connector.js'; +import type { ConnectorLibrary } from '#src/libraries/connector.js'; import { createHookLibrary } from '#src/libraries/hook.js'; import { createPasscodeLibrary } from '#src/libraries/passcode.js'; import { createPhraseLibrary } from '#src/libraries/phrase.js'; @@ -12,10 +12,9 @@ import { createVerificationStatusLibrary } from '#src/libraries/verification-sta import type Queries from './Queries.js'; export default class Libraries { - connectors = createConnectorLibrary(this.queries); users = createUserLibrary(this.queries); - signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors); - phrases = createPhraseLibrary(this.queries); + signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors, this.tenantId); + phrases = createPhraseLibrary(this.queries, this.tenantId); resources = createResourceLibrary(this.queries); hooks = createHookLibrary(this.queries); socials = createSocialLibrary(this.queries, this.connectors); @@ -23,5 +22,10 @@ export default class Libraries { applications = createApplicationLibrary(this.queries); verificationStatuses = createVerificationStatusLibrary(this.queries); - constructor(private readonly queries: Queries) {} + constructor( + public readonly tenantId: string, + private readonly queries: Queries, + // Explicitly passing connector library to eliminate dependency issue + private readonly connectors: ConnectorLibrary + ) {} } diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index ed018c990..87cb7e8ee 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -8,6 +8,7 @@ import mount from 'koa-mount'; import type Provider from 'oidc-provider'; import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js'; +import { createConnectorLibrary } from '#src/libraries/connector.js'; import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js'; import koaErrorHandler from '#src/middleware/koa-error-handler.js'; @@ -38,15 +39,18 @@ export default class Tenant implements TenantContext { #onRequestEmpty?: () => Promise<void>; public readonly provider: Provider; - public readonly queries: Queries; - public readonly libraries: Libraries; public readonly run: MiddlewareType; private readonly app: Koa; - private constructor(public readonly envSet: EnvSet, public readonly id: string) { - const queries = new Queries(envSet.pool); - const libraries = new Libraries(queries); + // eslint-disable-next-line max-params + private constructor( + public readonly envSet: EnvSet, + public readonly id: string, + public readonly queries = new Queries(envSet.pool), + public readonly connectors = createConnectorLibrary(queries), + public readonly libraries = new Libraries(id, queries, connectors) + ) { const isAdminTenant = id === adminTenantId; const mountedApps = [ ...Object.values(UserApps), @@ -54,8 +58,6 @@ export default class Tenant implements TenantContext { ]; this.envSet = envSet; - this.queries = queries; - this.libraries = libraries; // Init app const app = new Koa(); @@ -76,6 +78,7 @@ export default class Tenant implements TenantContext { id, provider, queries, + connectors, libraries, envSet, }; diff --git a/packages/core/src/tenants/TenantContext.ts b/packages/core/src/tenants/TenantContext.ts index 35d6c2bc6..e9fc4fab1 100644 --- a/packages/core/src/tenants/TenantContext.ts +++ b/packages/core/src/tenants/TenantContext.ts @@ -1,6 +1,7 @@ import type Provider from 'oidc-provider'; import type { EnvSet } from '#src/env-set/index.js'; +import type { ConnectorLibrary } from '#src/libraries/connector.js'; import type Libraries from './Libraries.js'; import type Queries from './Queries.js'; @@ -10,5 +11,6 @@ export default abstract class TenantContext { public abstract readonly envSet: EnvSet; public abstract readonly provider: Provider; public abstract readonly queries: Queries; + public abstract readonly connectors: ConnectorLibrary; public abstract readonly libraries: Libraries; } diff --git a/packages/core/src/test-utils/tenant.ts b/packages/core/src/test-utils/tenant.ts index af5881fb5..8ad4c144b 100644 --- a/packages/core/src/test-utils/tenant.ts +++ b/packages/core/src/test-utils/tenant.ts @@ -1,5 +1,7 @@ import { createMockPool, createMockQueryResult } from 'slonik'; +import type { ConnectorLibrary } from '#src/libraries/connector.js'; +import { createConnectorLibrary } from '#src/libraries/connector.js'; import Libraries from '#src/tenants/Libraries.js'; import Queries from '#src/tenants/Queries.js'; import type TenantContext from '#src/tenants/TenantContext.js'; @@ -46,15 +48,18 @@ export class MockTenant implements TenantContext { public id = 'mock_id'; public envSet = mockEnvSet; public queries: Queries; + public connectors: ConnectorLibrary; public libraries: Libraries; constructor( public provider = createMockProvider(), queriesOverride?: Partial2<Queries>, + connectorsOverride?: Partial<ConnectorLibrary>, librariesOverride?: Partial2<Libraries> ) { this.queries = new MockQueries(queriesOverride); - this.libraries = new Libraries(this.queries); + this.connectors = { ...createConnectorLibrary(this.queries), ...connectorsOverride }; + this.libraries = new Libraries(this.id, this.queries, this.connectors); this.setPartial('libraries', librariesOverride); } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index ca6540f16..6c61d8e15 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -28,7 +28,7 @@ "@logto/schemas": "workspace:*", "@peculiar/webcrypto": "^1.3.3", "@silverhand/eslint-config": "2.0.1", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@silverhand/ts-config": "2.0.3", "@types/expect-puppeteer": "^5.0.3", "@types/jest": "^29.4.0", diff --git a/packages/phrases-ui/package.json b/packages/phrases-ui/package.json index a49aba92b..576e6f479 100644 --- a/packages/phrases-ui/package.json +++ b/packages/phrases-ui/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@logto/language-kit": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "zod": "^3.20.2" }, "devDependencies": { diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 745dd9dc0..a9e1c4db3 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@logto/language-kit": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "zod": "^3.20.2" }, "devDependencies": { diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 3b1180d79..0fd703b03 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@silverhand/eslint-config": "2.0.1", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@silverhand/ts-config": "2.0.3", "@types/inquirer": "^9.0.0", "@types/jest": "^29.4.0", diff --git a/packages/shared/package.json b/packages/shared/package.json index 10779853a..ccf99c8d9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -56,7 +56,7 @@ "dependencies": { "@logto/core-kit": "workspace:*", "@logto/schemas": "workspace:*", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "chalk": "^5.0.0", "find-up": "^6.3.0", "nanoid": "^4.0.0", diff --git a/packages/toolkit/connector-kit/package.json b/packages/toolkit/connector-kit/package.json index acadffa46..c6702cb6b 100644 --- a/packages/toolkit/connector-kit/package.json +++ b/packages/toolkit/connector-kit/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@logto/language-kit": "workspace:*", - "@silverhand/essentials": "2.4.1" + "@silverhand/essentials": "^2.4.1" }, "optionalDependencies": { "zod": "^3.20.2" diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index ab4f1773d..480c9f5a2 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -136,7 +136,7 @@ const connectorConfigFormItemGuard = z.discriminatedUnion('type', [ export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>; -const connectorMetadataGuard = z.object({ +export const connectorMetadataGuard = z.object({ id: z.string(), target: z.string(), platform: z.nativeEnum(ConnectorPlatform).nullable(), diff --git a/packages/toolkit/core-kit/package.json b/packages/toolkit/core-kit/package.json index bcdd03e92..881b62525 100644 --- a/packages/toolkit/core-kit/package.json +++ b/packages/toolkit/core-kit/package.json @@ -50,7 +50,7 @@ "@jest/types": "^29.0.3", "@silverhand/eslint-config": "2.0.1", "@silverhand/eslint-config-react": "2.0.1", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@silverhand/ts-config": "2.0.3", "@types/color": "^3.0.3", "@types/jest": "^29.4.0", diff --git a/packages/ui/package.json b/packages/ui/package.json index c9ebcaf3b..885d2e222 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,7 +33,7 @@ "@react-spring/web": "^9.6.1", "@silverhand/eslint-config": "2.0.1", "@silverhand/eslint-config-react": "2.0.1", - "@silverhand/essentials": "2.4.1", + "@silverhand/essentials": "^2.4.1", "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "2.0.3", "@silverhand/ts-config-react": "2.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11706ad28..9f57850b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,7 @@ importers: '@logto/schemas': workspace:* '@logto/shared': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/inquirer': ^9.0.0 '@types/jest': ^29.4.0 @@ -116,7 +116,7 @@ importers: '@logto/schemas': workspace:* '@logto/shared': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/jest-config': ^2.0.1 '@silverhand/ts-config': 2.0.3 '@types/accepts': ^1.3.5 @@ -196,7 +196,7 @@ importers: '@parcel/transformer-svg-react': 2.8.3 '@silverhand/eslint-config': 2.0.1 '@silverhand/eslint-config-react': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@silverhand/ts-config-react': 2.0.3 '@tsconfig/docusaurus': ^1.0.5 @@ -346,7 +346,7 @@ importers: '@logto/schemas': workspace:* '@logto/shared': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/debug': ^4.1.7 '@types/etag': ^1.8.1 @@ -385,6 +385,7 @@ importers: jest-matcher-specific-error: ^1.0.0 jose: ^4.11.0 js-yaml: ^4.1.0 + keyv: ^4.5.2 koa: ^2.13.1 koa-body: ^5.0.0 koa-compose: ^4.1.0 @@ -401,6 +402,7 @@ importers: nodemon: ^2.0.19 oidc-provider: ^8.0.0 openapi-types: ^12.0.0 + p-memoize: ^7.1.1 p-retry: ^5.1.2 pg-protocol: ^1.6.0 prettier: ^2.8.2 @@ -442,6 +444,7 @@ importers: iconv-lite: 0.6.3 jose: 4.11.0 js-yaml: 4.1.0 + keyv: 4.5.2 koa: 2.13.4 koa-body: 5.0.0 koa-compose: 4.1.0 @@ -454,6 +457,7 @@ importers: lru-cache: 7.14.1 nanoid: 4.0.0 oidc-provider: 8.0.0 + p-memoize: 7.1.1 p-retry: 5.1.2 pg-protocol: 1.6.0 roarr: 7.11.0 @@ -573,7 +577,7 @@ importers: '@logto/schemas': workspace:* '@peculiar/webcrypto': ^1.3.3 '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/expect-puppeteer': ^5.0.3 '@types/jest': ^29.4.0 @@ -625,7 +629,7 @@ importers: specifiers: '@logto/language-kit': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 eslint: ^8.34.0 lint-staged: ^13.0.0 @@ -648,7 +652,7 @@ importers: specifiers: '@logto/language-kit': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 buffer: ^5.7.1 eslint: ^8.34.0 @@ -677,7 +681,7 @@ importers: '@logto/phrases': workspace:* '@logto/phrases-ui': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/inquirer': ^9.0.0 '@types/jest': ^29.4.0 @@ -730,7 +734,7 @@ importers: '@logto/core-kit': workspace:* '@logto/schemas': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/jest': ^29.4.0 '@types/node': ^18.11.18 @@ -767,7 +771,7 @@ importers: specifiers: '@logto/language-kit': workspace:* '@silverhand/eslint-config': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/node': ^18.11.18 eslint: ^8.34.0 @@ -797,7 +801,7 @@ importers: '@logto/language-kit': workspace:* '@silverhand/eslint-config': 2.0.1 '@silverhand/eslint-config-react': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/ts-config': 2.0.3 '@types/color': ^3.0.3 '@types/jest': ^29.4.0 @@ -886,7 +890,7 @@ importers: '@react-spring/web': ^9.6.1 '@silverhand/eslint-config': 2.0.1 '@silverhand/eslint-config-react': 2.0.1 - '@silverhand/essentials': 2.4.1 + '@silverhand/essentials': ^2.4.1 '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 2.0.3 '@silverhand/ts-config-react': 2.0.3 @@ -10638,7 +10642,6 @@ packages: /mimic-fn/4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - dev: true /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -11233,6 +11236,14 @@ packages: aggregate-error: 3.1.0 dev: true + /p-memoize/7.1.1: + resolution: {integrity: sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA==} + engines: {node: '>=14.16'} + dependencies: + mimic-fn: 4.0.0 + type-fest: 3.5.2 + dev: false + /p-retry/5.1.2: resolution: {integrity: sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}