0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(shared)!: standardize id and secret generators (#4550)

* refactor(shared)!: standardize id and secret generators

* refactor: fix tests
This commit is contained in:
Gao Sun 2023-09-22 13:43:56 +08:00 committed by GitHub
parent d254dae5ff
commit 18181f892e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 148 additions and 111 deletions

View file

@ -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

View file

@ -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();

View file

@ -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';

View file

@ -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';

View file

@ -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 } }));`}
</code>
</pre>

View file

@ -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',
});`}
</code>

View file

@ -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',
});`}
</code>

View file

@ -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';

View file

@ -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';

View file

@ -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
},
});`}
</code>

View file

@ -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 } })

View file

@ -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,

View file

@ -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');

View file

@ -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;

View file

@ -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,

View file

@ -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() };

View file

@ -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 };

View file

@ -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,

View file

@ -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<Array<{ role: Role }>>) =>
roles.some(({ role: { name } }) => name === InternalRole.Admin);
@ -108,8 +107,8 @@ export default function applicationRoutes<T extends AuthedRouter>(
);
ctx.body = await insertApplication({
id: applicationId(),
secret: generateStandardId(),
id: generateStandardId(),
secret: generateStandardSecret(),
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
...rest,
});

View file

@ -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<T extends AuthedRouter>(
validateConfig(config, rawConnector.configGuard);
}
const insertConnectorId = proposedId ?? generateConnectorId();
const insertConnectorId = proposedId ?? generateStandardShortId();
await insertConnector({
id: insertConnectorId,
connectorId,

View file

@ -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;

View file

@ -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<T extends AuthedRouter>(
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<T extends AuthedRouter>(
} = ctx.guard;
ctx.body = await updateHookById(id, {
signingKey: generateStandardId(),
signingKey: generateStandardSecret(),
});
return next();

View file

@ -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<Resource[]> => [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',

View file

@ -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<T extends AuthedRouter>(
...[
router,
@ -110,7 +107,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
);
const resource = await insertResource({
id: resourceId(),
id: generateStandardId(),
...body,
});
@ -280,7 +277,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
ctx.status = 201;
ctx.body = await insertScope({
...body,
id: scopeId(),
id: generateStandardId(),
resourceId,
});

View file

@ -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(),

View file

@ -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<Role[]> => [mockAdminUserRole]),

View file

@ -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<Role[]> => [mockAdminUserRole]),

View file

@ -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(),

View file

@ -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,
}));

View file

@ -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<CreateApplicati
tenantId: adminTenantId,
id: adminConsoleApplicationId,
name: 'Admin Console',
secret: generateStandardId(),
secret: generateStandardSecret(),
description: 'Logto Admin Console.',
type: ApplicationType.SPA,
oidcClientMetadata: { redirectUris: [], postLogoutRedirectUris: [] },
@ -49,7 +49,7 @@ export const createTenantMachineToMachineApplication = (
id: generateStandardId(),
name: 'Cloud Service',
description: `Machine to machine application for tenant ${tenantId}`,
secret: generateStandardId(),
secret: generateStandardSecret(),
type: ApplicationType.MachineToMachine,
oidcClientMetadata: {
redirectUris: [],

View file

@ -1,15 +1,29 @@
import { buildIdGenerator } from './id.js';
import { generateStandardId, generateStandardSecret, generateStandardShortId } from './id.js';
describe('id generator', () => {
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
}
});
});

View file

@ -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<typeof customAlphabet>;
};
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);

View file

@ -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}`;