From 18181f892e701abfce30e6bf4575cc7ba348bdf3 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 22 Sep 2023 13:43:56 +0800 Subject: [PATCH] refactor(shared)!: standardize id and secret generators (#4550) * refactor(shared)!: standardize id and secret generators * refactor: fix tests --- .changeset/proud-donuts-check.md | 15 ++++++++ packages/cli/src/commands/database/utils.ts | 4 +-- .../guides/web-asp-net-core-mvc/README.mdx | 1 - .../docs/guides/web-asp-net-core/README.mdx | 1 - .../assets/docs/guides/web-express/README.mdx | 4 +-- .../guides/web-next-app-router/README.mdx | 4 +-- .../assets/docs/guides/web-next/README.mdx | 4 +-- .../src/assets/docs/guides/web-php/README.mdx | 1 - .../assets/docs/guides/web-python/README.mdx | 1 - .../assets/docs/guides/web-remix/README.mdx | 4 +-- .../components/LinkAccountSection/index.tsx | 4 +-- packages/core/src/__mocks__/index.ts | 4 ++- .../core/src/libraries/hook/index.test.ts | 11 +++--- packages/core/src/libraries/user.ts | 6 ++-- .../core/src/middleware/koa-audit-log.test.ts | 21 +++++------ .../core/src/routes/admin-user-role.test.ts | 4 +-- .../core/src/routes/application-role.test.ts | 4 +-- packages/core/src/routes/application.test.ts | 16 ++++----- packages/core/src/routes/application.ts | 7 ++-- packages/core/src/routes/connector/index.ts | 6 ++-- .../core/src/routes/custom-phrase.test.ts | 4 +-- packages/core/src/routes/hook.ts | 6 ++-- packages/core/src/routes/resource.test.ts | 14 +++----- packages/core/src/routes/resource.ts | 9 ++--- .../core/src/routes/role.application.test.ts | 4 +-- packages/core/src/routes/role.scope.test.ts | 4 +-- packages/core/src/routes/role.test.ts | 4 +-- packages/core/src/routes/role.user.test.ts | 4 +-- packages/core/src/test-utils/nanoid.ts | 13 ++++++- packages/schemas/src/seeds/application.ts | 6 ++-- packages/shared/src/utils/id.test.ts | 36 +++++++++++++------ packages/shared/src/utils/id.ts | 27 ++++++++++++-- .../toolkit/core-kit/src/models/tenant.ts | 6 ++-- 33 files changed, 148 insertions(+), 111 deletions(-) create mode 100644 .changeset/proud-donuts-check.md diff --git a/.changeset/proud-donuts-check.md b/.changeset/proud-donuts-check.md new file mode 100644 index 000000000..9d02c0edc --- /dev/null +++ b/.changeset/proud-donuts-check.md @@ -0,0 +1,15 @@ +--- +"@logto/shared": major +"@logto/console": patch +"@logto/schemas": patch +"@logto/core": patch +"@logto/cli": patch +--- + +standardize id and secret generators + +- Remove `buildIdGenerator` export from `@logto/shared` +- Add `generateStandardSecret` and `generateStandardShortId` exports to `@logto/shared` +- Align comment and implementation of `buildIdGenerator` in `@logto/shared` + - The comment stated the function will include uppercase letters by default, but it did not; Now it does. +- Use `generateStandardSecret` for all secret generation diff --git a/packages/cli/src/commands/database/utils.ts b/packages/cli/src/commands/database/utils.ts index f24911c7b..e962b0a79 100644 --- a/packages/cli/src/commands/database/utils.ts +++ b/packages/cli/src/commands/database/utils.ts @@ -1,7 +1,7 @@ import { generateKeyPair } from 'node:crypto'; import { promisify } from 'node:util'; -import { generateStandardId } from '@logto/shared'; +import { generateStandardSecret } from '@logto/shared'; export enum PrivateKeyType { RSA = 'rsa', @@ -46,4 +46,4 @@ export const generateOidcPrivateKey = async (type: PrivateKeyType = PrivateKeyTy throw new Error(`Unsupported private key ${String(type)}`); }; -export const generateOidcCookieKey = () => generateStandardId(); +export const generateOidcCookieKey = () => generateStandardSecret(); diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx b/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx index 05720f708..d52a4bd35 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx +++ b/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx @@ -2,7 +2,6 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx b/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx index f2900b937..28c25efe7 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx +++ b/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx @@ -2,7 +2,6 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-express/README.mdx b/packages/console/src/assets/docs/guides/web-express/README.mdx index 14bd7b670..e145a67e0 100644 --- a/packages/console/src/assets/docs/guides/web-express/README.mdx +++ b/packages/console/src/assets/docs/guides/web-express/README.mdx @@ -2,7 +2,7 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardSecret } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; @@ -77,7 +77,7 @@ The SDK requires [express-session](https://www.npmjs.com/package/express-session import session from 'express-session'; app.use(cookieParser()); -app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`} +app.use(session({ secret: '${generateStandardSecret()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`} diff --git a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx index b90097209..b92989de9 100644 --- a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx @@ -2,7 +2,7 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardSecret } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; @@ -58,7 +58,7 @@ export const logtoClient = new LogtoClient({ appId: '${props.app.id}', appSecret: '${props.app.secret}', baseUrl: 'http://localhost:3000', // Change to your own base URL - cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret + cookieSecret: '${generateStandardSecret()}', // Auto-generated 32 digit secret cookieSecure: process.env.NODE_ENV === 'production', });`} diff --git a/packages/console/src/assets/docs/guides/web-next/README.mdx b/packages/console/src/assets/docs/guides/web-next/README.mdx index af6757dc1..8f372d4f7 100644 --- a/packages/console/src/assets/docs/guides/web-next/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next/README.mdx @@ -2,7 +2,7 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardSecret } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; @@ -58,7 +58,7 @@ export const logtoClient = new LogtoClient({ appId: '${props.app.id}', appSecret: '${props.app.secret}', baseUrl: 'http://localhost:3000', // Change to your own base URL - cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret + cookieSecret: '${generateStandardSecret()}', // Auto-generated 32 digit secret cookieSecure: process.env.NODE_ENV === 'production', });`} diff --git a/packages/console/src/assets/docs/guides/web-php/README.mdx b/packages/console/src/assets/docs/guides/web-php/README.mdx index 5bd785547..21c13c289 100644 --- a/packages/console/src/assets/docs/guides/web-php/README.mdx +++ b/packages/console/src/assets/docs/guides/web-php/README.mdx @@ -2,7 +2,6 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-python/README.mdx b/packages/console/src/assets/docs/guides/web-python/README.mdx index 4cbd5e4a7..1ff886610 100644 --- a/packages/console/src/assets/docs/guides/web-python/README.mdx +++ b/packages/console/src/assets/docs/guides/web-python/README.mdx @@ -2,7 +2,6 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-remix/README.mdx b/packages/console/src/assets/docs/guides/web-remix/README.mdx index 18e924756..2c196c559 100644 --- a/packages/console/src/assets/docs/guides/web-remix/README.mdx +++ b/packages/console/src/assets/docs/guides/web-remix/README.mdx @@ -2,7 +2,7 @@ import UriInputField from '@/mdx-components/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardSecret } from '@logto/shared/universal'; import Steps from '@/mdx-components/Steps'; import Step from '@/mdx-components/Step'; @@ -55,7 +55,7 @@ const sessionStorage = createCookieSessionStorage({ cookie: { name: "logto-session", maxAge: 14 * 24 * 60 * 60, - secrets: '${buildIdGenerator(12)()}', // Auto-generated secret + secrets: '${generateStandardSecret()}', // Auto-generated secret }, });`} diff --git a/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx b/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx index c3ee621e4..919b45bf8 100644 --- a/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx +++ b/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx @@ -2,7 +2,7 @@ import type { SocialUserInfo } from '@logto/connector-kit'; import { socialUserInfoGuard } from '@logto/connector-kit'; import { Theme } from '@logto/schemas'; import type { ConnectorResponse, UserProfileResponse } from '@logto/schemas'; -import { buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardId } from '@logto/shared/universal'; import type { Optional } from '@silverhand/essentials'; import { appendPath, conditional } from '@silverhand/essentials'; import { useCallback, useMemo } from 'react'; @@ -42,7 +42,7 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) { const getSocialAuthorizationUri = useCallback( async (connectorId: string) => { const adminTenantEndpointUrl = new URL(adminTenantEndpoint); - const state = buildIdGenerator(8)(); + const state = generateStandardId(8); const redirectUri = new URL(`/callback/${connectorId}`, adminTenantEndpointUrl).href; const { redirectTo } = await api .post('me/social/authorization-uri', { json: { connectorId, state, redirectUri } }) diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 8816df3e3..7fdcf15f8 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -11,6 +11,8 @@ import type { } from '@logto/schemas'; import { RoleType, ApplicationType } from '@logto/schemas'; +import { mockId } from '#src/test-utils/nanoid.js'; + export * from './connector.js'; export * from './sign-in-experience.js'; export * from './cloud-connection.js'; @@ -20,7 +22,7 @@ export * from './domain.js'; export const mockApplication: Application = { tenantId: 'fake_tenant', id: 'foo', - secret: 'randomId', + secret: mockId, name: 'foo', type: ApplicationType.SPA, description: null, diff --git a/packages/core/src/libraries/hook/index.test.ts b/packages/core/src/libraries/hook/index.test.ts index 720ac560a..f65e5a657 100644 --- a/packages/core/src/libraries/hook/index.test.ts +++ b/packages/core/src/libraries/hook/index.test.ts @@ -3,17 +3,14 @@ import { HookEvent, InteractionEvent, LogResult } from '@logto/schemas'; import { createMockUtils } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { generateHookTestPayload, parseResponse } from './utils.js'; const { jest } = import.meta; -const { mockEsmWithActual, mockEsm } = createMockUtils(jest); +const { mockEsm } = createMockUtils(jest); -const nanoIdMock = 'mockId'; -await mockEsmWithActual('@logto/shared', () => ({ - buildIdGenerator: jest.fn().mockReturnValue(nanoIdMock), - generateStandardId: jest.fn().mockReturnValue(nanoIdMock), -})); +await mockIdGenerators(); const mockSignature = 'mockSignature'; mockEsm('#src/utils/sign.js', () => ({ @@ -95,7 +92,7 @@ describe('triggerInteractionHooks()', () => { }); const calledPayload: unknown = insertLog.mock.calls[0][0]; - expect(calledPayload).toHaveProperty('id', nanoIdMock); + expect(calledPayload).toHaveProperty('id', mockId); expect(calledPayload).toHaveProperty('key', 'TriggerHook.' + HookEvent.PostSignIn); expect(calledPayload).toHaveProperty('payload.result', LogResult.Success); expect(calledPayload).toHaveProperty('payload.hookId', 'foo'); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 0d2088fd2..66fd53b08 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -1,6 +1,6 @@ import type { User, CreateUser, Scope } from '@logto/schemas'; import { Users, UsersPasswordEncryptionMethod } from '@logto/schemas'; -import { buildIdGenerator, generateStandardId } from '@logto/shared'; +import { generateStandardShortId, generateStandardId } from '@logto/shared'; import type { OmitAutoSetFields } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; import { deduplicate } from '@silverhand/essentials'; @@ -15,8 +15,6 @@ import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { encryptPassword } from '#src/utils/password.js'; -const userId = buildIdGenerator(12); - export const encryptUserPassword = async ( password: string ): Promise<{ @@ -64,7 +62,7 @@ export const createUserLibrary = (queries: Queries) => { const generateUserId = async (retries = 500) => pRetry( async () => { - const id = userId(); + const id = generateStandardShortId(); if (!(await hasUserWithId(id))) { return id; diff --git a/packages/core/src/middleware/koa-audit-log.test.ts b/packages/core/src/middleware/koa-audit-log.test.ts index 919ee049a..05fb51c32 100644 --- a/packages/core/src/middleware/koa-audit-log.test.ts +++ b/packages/core/src/middleware/koa-audit-log.test.ts @@ -3,18 +3,15 @@ import { LogResult } from '@logto/schemas'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; import i18next from 'i18next'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; + import type { WithLogContext, LogPayload } from './koa-audit-log.js'; const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); -const nanoIdMock = 'mockId'; -await mockEsmWithActual('@logto/shared', () => ({ - // eslint-disable-next-line unicorn/consistent-function-scoping - buildIdGenerator: () => () => nanoIdMock, - generateStandardId: () => nanoIdMock, -})); +await mockIdGenerators(); const { default: RequestError } = await import('#src/errors/RequestError/index.js'); const { MockQueries } = await import('#src/test-utils/tenant.js'); @@ -56,7 +53,7 @@ describe('koaAuditLog middleware', () => { await koaLog(queries)(ctx, next); expect(insertLog).toBeCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: { ...mockPayload, @@ -95,12 +92,12 @@ describe('koaAuditLog middleware', () => { }; expect(insertLog).toHaveBeenCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: basePayload, }); expect(insertLog).toHaveBeenCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: { ...basePayload, @@ -147,7 +144,7 @@ describe('koaAuditLog middleware', () => { await koaLog(queries)(ctx, next); expect(insertLog).toBeCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: { ...mockPayload, @@ -179,7 +176,7 @@ describe('koaAuditLog middleware', () => { await expect(koaLog(queries)(ctx, next)).rejects.toMatchError(error); expect(insertLog).toBeCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: { ...mockPayload, @@ -216,7 +213,7 @@ describe('koaAuditLog middleware', () => { expect(insertLog).toHaveBeenCalledTimes(2); expect(insertLog).toBeCalledWith({ - id: nanoIdMock, + id: mockId, key: logKey, payload: { ...mockPayload, diff --git a/packages/core/src/routes/admin-user-role.test.ts b/packages/core/src/routes/admin-user-role.test.ts index 47920b5fa..83bfb6997 100644 --- a/packages/core/src/routes/admin-user-role.test.ts +++ b/packages/core/src/routes/admin-user-role.test.ts @@ -8,13 +8,13 @@ import { mockAdminUserRole3, mockUserRole, } from '#src/__mocks__/index.js'; -import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const users = { findUserById: jest.fn() }; diff --git a/packages/core/src/routes/application-role.test.ts b/packages/core/src/routes/application-role.test.ts index eb8c755fd..8cc16f2af 100644 --- a/packages/core/src/routes/application-role.test.ts +++ b/packages/core/src/routes/application-role.test.ts @@ -7,13 +7,13 @@ import { mockAdminUserRole2, mockApplicationRole, } from '#src/__mocks__/index.js'; -import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const mockM2mApplication = { ...mockApplication, type: ApplicationType.MachineToMachine }; diff --git a/packages/core/src/routes/application.test.ts b/packages/core/src/routes/application.test.ts index 40aa33030..ffbb94adb 100644 --- a/packages/core/src/routes/application.test.ts +++ b/packages/core/src/routes/application.test.ts @@ -1,22 +1,18 @@ import type { Application, CreateApplication } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; -import { pickDefault, createMockUtils } from '@logto/shared/esm'; +import { pickDefault } from '@logto/shared/esm'; import { mockApplication } from '#src/__mocks__/index.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; const { jest } = import.meta; -const { mockEsmWithActual } = createMockUtils(jest); const findApplicationById = jest.fn(async () => mockApplication); const deleteApplicationById = jest.fn(); -await mockEsmWithActual('@logto/shared', () => ({ - // eslint-disable-next-line unicorn/consistent-function-scoping - buildIdGenerator: () => () => 'randomId', - generateStandardId: () => 'randomId', -})); +await mockIdGenerators(); const tenantContext = new MockTenant( undefined, @@ -83,8 +79,8 @@ describe('application route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ ...mockApplication, - id: 'randomId', - secret: 'randomId', + id: mockId, + secret: mockId, name, description, type, @@ -101,7 +97,7 @@ describe('application route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ ...mockApplication, - id: 'randomId', + id: mockId, name, type, customClientMetadata, diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index fc97602e7..574a015b8 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -6,7 +6,7 @@ import { InternalRole, ApplicationType, } from '@logto/schemas'; -import { generateStandardId, buildIdGenerator } from '@logto/shared'; +import { generateStandardId, generateStandardSecret } from '@logto/shared'; import { boolean, object, string, z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -18,7 +18,6 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; -const applicationId = buildIdGenerator(21); const includesInternalAdminRole = (roles: Readonly>) => roles.some(({ role: { name } }) => name === InternalRole.Admin); @@ -108,8 +107,8 @@ export default function applicationRoutes( ); ctx.body = await insertApplication({ - id: applicationId(), - secret: generateStandardId(), + id: generateStandardId(), + secret: generateStandardSecret(), oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata), ...rest, }); diff --git a/packages/core/src/routes/connector/index.ts b/packages/core/src/routes/connector/index.ts index 3c7e62c5f..3474c493c 100644 --- a/packages/core/src/routes/connector/index.ts +++ b/packages/core/src/routes/connector/index.ts @@ -1,7 +1,7 @@ import { type ConnectorFactory, buildRawConnector } from '@logto/cli/lib/connector/index.js'; import { demoConnectorIds, validateConfig } from '@logto/connector-kit'; import { Connectors, ConnectorType, connectorResponseGuard, type JsonObject } from '@logto/schemas'; -import { buildIdGenerator } from '@logto/shared'; +import { generateStandardShortId } from '@logto/shared'; import { conditional } from '@silverhand/essentials'; import cleanDeep from 'clean-deep'; import { string, object } from 'zod'; @@ -20,8 +20,6 @@ import connectorAuthorizationUriRoutes from './authorization-uri.js'; import connectorConfigTestingRoutes from './config-testing.js'; import connectorFactoryRoutes from './factory.js'; -const generateConnectorId = buildIdGenerator(12); - const guardConnectorsQuota = async (factory: ConnectorFactory, quota: QuotaLibrary) => { if (factory.metadata.isStandard) { await quota.guardKey('standardConnectorsLimit'); @@ -130,7 +128,7 @@ export default function connectorRoutes( validateConfig(config, rawConnector.configGuard); } - const insertConnectorId = proposedId ?? generateConnectorId(); + const insertConnectorId = proposedId ?? generateStandardShortId(); await insertConnector({ id: insertConnectorId, connectorId, diff --git a/packages/core/src/routes/custom-phrase.test.ts b/packages/core/src/routes/custom-phrase.test.ts index 4d645b2af..a2dff935a 100644 --- a/packages/core/src/routes/custom-phrase.test.ts +++ b/packages/core/src/routes/custom-phrase.test.ts @@ -5,14 +5,14 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { mockZhCnCustomPhrase, trTrTag, zhCnTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockIdGenerators } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); -await mockStandardId(); +await mockIdGenerators(); const mockLanguageTag = zhCnTag; const mockPhrase = mockZhCnCustomPhrase; diff --git a/packages/core/src/routes/hook.ts b/packages/core/src/routes/hook.ts index 6879d0e4c..8ae802138 100644 --- a/packages/core/src/routes/hook.ts +++ b/packages/core/src/routes/hook.ts @@ -8,7 +8,7 @@ import { type HookResponse, type Hook, } from '@logto/schemas'; -import { generateStandardId } from '@logto/shared'; +import { generateStandardId, generateStandardSecret } from '@logto/shared'; import { conditional, deduplicate, yes } from '@silverhand/essentials'; import { subDays } from 'date-fns'; import { z } from 'zod'; @@ -171,7 +171,7 @@ export default function hookRoutes( ctx.body = await insertHook({ ...rest, id: generateStandardId(), - signingKey: generateStandardId(), + signingKey: generateStandardSecret(), events: events ?? [], enabled: enabled ?? true, ...conditional(event && { event }), @@ -242,7 +242,7 @@ export default function hookRoutes( } = ctx.guard; ctx.body = await updateHookById(id, { - signingKey: generateStandardId(), + signingKey: generateStandardSecret(), }); return next(); diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts index 10c9ec39b..90bf5bf81 100644 --- a/packages/core/src/routes/resource.test.ts +++ b/packages/core/src/routes/resource.test.ts @@ -1,15 +1,14 @@ import type { Resource, CreateResource } from '@logto/schemas'; -import { pickDefault, createMockUtils } from '@logto/shared/esm'; +import { pickDefault } from '@logto/shared/esm'; import { type Nullable } from '@silverhand/essentials'; import { mockResource, mockScope } from '#src/__mocks__/index.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -const { mockEsm } = createMockUtils(jest); - const resources = { findTotalNumberOfResources: async () => ({ count: 10 }), findAllResources: async (): Promise => [mockResource], @@ -45,10 +44,7 @@ const scopes = { }; const { insertScope, updateScopeById } = scopes; -mockEsm('@logto/shared', () => ({ - // eslint-disable-next-line unicorn/consistent-function-scoping - buildIdGenerator: () => () => 'randomId', -})); +await mockIdGenerators(); const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined); @@ -89,7 +85,7 @@ describe('resource routes', () => { expect(response.status).toEqual(201); expect(response.body).toEqual({ tenantId: 'fake_tenant', - id: 'randomId', + id: mockId, name, indicator, isDefault: false, @@ -183,7 +179,7 @@ describe('resource routes', () => { expect(response.status).toEqual(201); expect(findResourceById).toHaveBeenCalledWith('foo'); expect(insertScope).toHaveBeenCalledWith({ - id: 'randomId', + id: mockId, name, description, resourceId: 'foo', diff --git a/packages/core/src/routes/resource.ts b/packages/core/src/routes/resource.ts index 4033d83d2..94a0b3bc6 100644 --- a/packages/core/src/routes/resource.ts +++ b/packages/core/src/routes/resource.ts @@ -1,5 +1,5 @@ import { Resources, Scopes } from '@logto/schemas'; -import { buildIdGenerator } from '@logto/shared'; +import { generateStandardId } from '@logto/shared'; import { tryThat, yes } from '@silverhand/essentials'; import { boolean, object, string } from 'zod'; @@ -13,9 +13,6 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; -const resourceId = buildIdGenerator(21); -const scopeId = resourceId; - export default function resourceRoutes( ...[ router, @@ -110,7 +107,7 @@ export default function resourceRoutes( ); const resource = await insertResource({ - id: resourceId(), + id: generateStandardId(), ...body, }); @@ -280,7 +277,7 @@ export default function resourceRoutes( ctx.status = 201; ctx.body = await insertScope({ ...body, - id: scopeId(), + id: generateStandardId(), resourceId, }); diff --git a/packages/core/src/routes/role.application.test.ts b/packages/core/src/routes/role.application.test.ts index 47c3b00ce..21cf83944 100644 --- a/packages/core/src/routes/role.application.test.ts +++ b/packages/core/src/routes/role.application.test.ts @@ -1,14 +1,14 @@ import { pickDefault } from '@logto/shared/esm'; import { mockAdminApplicationRole, mockApplication } from '#src/__mocks__/index.js'; -import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const roles = { findRoleById: jest.fn(), diff --git a/packages/core/src/routes/role.scope.test.ts b/packages/core/src/routes/role.scope.test.ts index 7093784c8..f51a117d5 100644 --- a/packages/core/src/routes/role.scope.test.ts +++ b/packages/core/src/routes/role.scope.test.ts @@ -7,14 +7,14 @@ import { mockResource, mockScopeWithResource, } from '#src/__mocks__/index.js'; -import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const roles = { findRoles: jest.fn(async (): Promise => [mockAdminUserRole]), diff --git a/packages/core/src/routes/role.test.ts b/packages/core/src/routes/role.test.ts index 76f875527..7e8e0641a 100644 --- a/packages/core/src/routes/role.test.ts +++ b/packages/core/src/routes/role.test.ts @@ -2,14 +2,14 @@ import type { Role } from '@logto/schemas'; import { pickDefault } from '@logto/shared/esm'; import { mockAdminUserRole, mockScope, mockUser } from '#src/__mocks__/index.js'; -import { mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockIdGenerators } from '#src/test-utils/nanoid.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const roles = { findRoles: jest.fn(async (): Promise => [mockAdminUserRole]), diff --git a/packages/core/src/routes/role.user.test.ts b/packages/core/src/routes/role.user.test.ts index e40aa8163..35f8ab2e1 100644 --- a/packages/core/src/routes/role.user.test.ts +++ b/packages/core/src/routes/role.user.test.ts @@ -1,14 +1,14 @@ import { pickDefault } from '@logto/shared/esm'; import { mockAdminUserRole, mockUser } from '#src/__mocks__/index.js'; -import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; +import { mockId, mockIdGenerators } from '#src/test-utils/nanoid.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -await mockStandardId(); +await mockIdGenerators(); const roles = { findRoleById: jest.fn(), diff --git a/packages/core/src/test-utils/nanoid.ts b/packages/core/src/test-utils/nanoid.ts index 411c6b37c..b1d359ff2 100644 --- a/packages/core/src/test-utils/nanoid.ts +++ b/packages/core/src/test-utils/nanoid.ts @@ -2,8 +2,19 @@ import { createMockUtils } from '@logto/shared/esm'; const { mockEsmWithActual } = createMockUtils(import.meta.jest); +/** The mock id generated by all id generators. */ export const mockId = 'mockId'; -export const mockStandardId = async () => + +/** + * Mock all id generators to return the same {@link mockId}. List of id generators: + * + * - generateStandardId + * - generateStandardShortId + * - generateStandardSecret + */ +export const mockIdGenerators = async () => mockEsmWithActual('@logto/shared', () => ({ generateStandardId: () => mockId, + generateStandardShortId: () => mockId, + generateStandardSecret: () => mockId, })); diff --git a/packages/schemas/src/seeds/application.ts b/packages/schemas/src/seeds/application.ts index 946c303c6..00dd56a6d 100644 --- a/packages/schemas/src/seeds/application.ts +++ b/packages/schemas/src/seeds/application.ts @@ -1,4 +1,4 @@ -import { generateStandardId } from '@logto/shared/universal'; +import { generateStandardId, generateStandardSecret } from '@logto/shared/universal'; import type { Application, @@ -35,7 +35,7 @@ export const createDefaultAdminConsoleApplication = (): Readonly { +describe('standard id generator', () => { it('should match the input length', () => { - const id = buildIdGenerator(10)(); - expect(id.length).toEqual(10); - }); - - it('to random id should not equal', () => { - const id_1 = buildIdGenerator(10)(); - const id_2 = buildIdGenerator(10)(); - - expect(id_1).not.toEqual(id_2); + const id = generateStandardId(); + expect(id.length).toEqual(21); + }); +}); + +describe('standard short id generator', () => { + it('should match the input length', () => { + const id = generateStandardShortId(); + expect(id.length).toEqual(12); + }); +}); + +describe('standard secret generator', () => { + it('should match the input length', () => { + const id = generateStandardSecret(); + expect(id.length).toEqual(32); + }); + + it('should generate id with uppercase', () => { + // If it can't generate uppercase, it will timeout + while (!/[A-Z]/.test(generateStandardSecret())) { + // Do nothing + } }); }); diff --git a/packages/shared/src/utils/id.ts b/packages/shared/src/utils/id.ts index f48d9ee22..a04fea66b 100644 --- a/packages/shared/src/utils/id.ts +++ b/packages/shared/src/utils/id.ts @@ -1,6 +1,6 @@ import { customAlphabet } from 'nanoid'; -const lowercaseAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; +const lowercaseAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz' as const; const alphabet = `${lowercaseAlphabet}ABCDEFGHIJKLMNOPQRSTUVWXYZ` as const; type BuildIdGenerator = { @@ -17,6 +17,27 @@ type BuildIdGenerator = { (size: number, includingUppercase: false): ReturnType; }; -export const buildIdGenerator: BuildIdGenerator = (size: number, includingUppercase = false) => +const buildIdGenerator: BuildIdGenerator = (size: number, includingUppercase = true) => customAlphabet(includingUppercase ? alphabet : lowercaseAlphabet, size); -export const generateStandardId = buildIdGenerator(21); + +/** + * Generate a standard id with 21 characters, including lowercase letters and numbers. + * + * @see {@link lowercaseAlphabet} + */ +export const generateStandardId = buildIdGenerator(21, false); + +/** + * Generate a standard short id with 12 characters, including lowercase letters and numbers. + * + * @see {@link lowercaseAlphabet} + */ +export const generateStandardShortId = buildIdGenerator(12, false); + +/** + * Generate a standard secret with 32 characters, including uppercase letters, lowercase + * letters, and numbers. + * + * @see {@link alphabet} + */ +export const generateStandardSecret = buildIdGenerator(32); diff --git a/packages/toolkit/core-kit/src/models/tenant.ts b/packages/toolkit/core-kit/src/models/tenant.ts index ad52d5167..9b1f73605 100644 --- a/packages/toolkit/core-kit/src/models/tenant.ts +++ b/packages/toolkit/core-kit/src/models/tenant.ts @@ -1,7 +1,7 @@ -import { generateStandardId, buildIdGenerator } from '@logto/shared/universal'; +import { generateStandardId } from '@logto/shared/universal'; // Use lowercase letters for tenant IDs to improve compatibility -const generateTenantId = buildIdGenerator(6, false); +const generateTenantId = () => generateStandardId(6); export type TenantMetadata = { id: string; @@ -12,7 +12,7 @@ export type TenantMetadata = { export const createTenantMetadata = ( databaseName: string, - tenantId = generateTenantId(6) + tenantId = generateTenantId() ): TenantMetadata => { const parentRole = `logto_tenant_${databaseName}`; const role = `logto_tenant_${databaseName}_${tenantId}`;