0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

Merge pull request #3285 from logto-io/gao-allow-admin-to-create-tenant

refactor: allow admin to create tenants
This commit is contained in:
Gao Sun 2023-03-06 13:54:25 +08:00 committed by GitHub
commit ea66dcbf2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 255 additions and 134 deletions

View file

@ -0,0 +1,16 @@
---
"@logto/cli": minor
"@logto/cloud": minor
"@logto/console": minor
"@logto/core": minor
"@logto/integration-tests": minor
"@logto/phrases": minor
"@logto/phrases-ui": minor
"@logto/schemas": minor
"@logto/shared": minor
"@logto/connector-kit": minor
"@logto/core-kit": minor
"@logto/ui": minor
---
Allow admin tenant admin to create tenants without limitation

View file

@ -46,7 +46,7 @@
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"chalk": "^5.0.0",
"decamelize": "^6.0.0",
"dotenv": "^16.0.0",

View file

@ -24,7 +24,7 @@ import { updateDatabaseTimestamp } from '../../../queries/system.js';
import { getPathInModule, log } from '../../../utils.js';
import { appendAdminConsoleRedirectUris } from './cloud.js';
import { seedOidcConfigs } from './oidc-config.js';
import { createTenant, seedAdminData } from './tenant.js';
import { assignScopesToRole, createTenant, seedAdminData } from './tenant.js';
const getExplicitOrder = (query: string) => {
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
@ -123,9 +123,20 @@ export const seedTables = async (
await createTenant(connection, adminTenantId);
await seedOidcConfigs(connection, adminTenantId);
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
await seedAdminData(connection, createAdminDataInAdminTenant(adminTenantId));
const adminAdminData = createAdminDataInAdminTenant(adminTenantId);
await seedAdminData(connection, adminAdminData);
await seedAdminData(connection, createMeApiInAdminTenant());
await seedAdminData(connection, createCloudApi());
const [cloudData, ...cloudAdditionalScopes] = createCloudApi();
await seedAdminData(connection, cloudData, ...cloudAdditionalScopes);
// Assign all cloud API scopes to role `admin:admin`
await assignScopesToRole(
connection,
adminTenantId,
adminAdminData.role.id,
...cloudAdditionalScopes.map(({ id }) => id)
);
await Promise.all([
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),

View file

@ -1,5 +1,5 @@
import { generateStandardId } from '@logto/core-kit';
import type { TenantModel, AdminData, UpdateAdminData } from '@logto/schemas';
import type { TenantModel, AdminData, UpdateAdminData, CreateScope } from '@logto/schemas';
import { CreateRolesScope } from '@logto/schemas';
import { createTenantMetadata } from '@logto/shared';
import { assert } from '@silverhand/essentials';
@ -25,7 +25,8 @@ export const createTenant = async (pool: CommonQueryMethods, tenantId: string) =
export const seedAdminData = async (
pool: CommonQueryMethods,
data: AdminData | UpdateAdminData
data: AdminData | UpdateAdminData,
...additionalScopes: CreateScope[]
) => {
const { resource, scope, role } = data;
@ -53,6 +54,7 @@ export const seedAdminData = async (
await pool.query(insertInto(resource, 'resources'));
await pool.query(insertInto(scope, 'scopes'));
await Promise.all(additionalScopes.map(async (scope) => pool.query(insertInto(scope, 'scopes'))));
const roleId = await processRole();
await pool.query(
@ -67,3 +69,26 @@ export const seedAdminData = async (
)
);
};
export const assignScopesToRole = async (
pool: CommonQueryMethods,
tenantId: string,
roleId: string,
...scopeIds: string[]
) => {
await Promise.all(
scopeIds.map(async (scopeId) =>
pool.query(
insertInto(
{
id: generateStandardId(),
roleId,
scopeId,
tenantId,
} satisfies CreateRolesScope,
'roles_scopes'
)
)
)
);
};

View file

@ -23,7 +23,7 @@
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@withtyped/postgres": "^0.8.1",
"@withtyped/server": "^0.8.0",
"chalk": "^5.0.0",

View file

@ -12,13 +12,17 @@ export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants')
return next({ ...context, json: await library.getAvailableTenants(context.auth.id) });
})
.post('/', { response: tenantInfoGuard }, async (context, next) => {
if (!context.auth.scopes.includes(CloudScope.CreateTenant)) {
if (
![CloudScope.CreateTenant, CloudScope.ManageTenant].some((scope) =>
context.auth.scopes.includes(scope)
)
) {
throw new RequestError('Forbidden due to lack of permission.', 403);
}
const tenants = await library.getAvailableTenants(context.auth.id);
if (tenants.length > 0) {
if (!context.auth.scopes.includes(CloudScope.ManageTenant) && tenants.length > 0) {
throw new RequestError('The user already has a tenant.', 409);
}

View file

@ -33,7 +33,7 @@
"@parcel/transformer-svg-react": "2.8.3",
"@silverhand/eslint-config": "2.0.1",
"@silverhand/eslint-config-react": "2.0.1",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@silverhand/ts-config": "2.0.3",
"@silverhand/ts-config-react": "2.0.3",
"@tsconfig/docusaurus": "^1.0.5",

View file

@ -1,7 +1,7 @@
import { UserScope } from '@logto/core-kit';
import { LogtoProvider } from '@logto/react';
import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas';
import { deduplicate } from '@silverhand/essentials';
import { conditionalArray, deduplicate } from '@silverhand/essentials';
import { useContext } from 'react';
import 'overlayscrollbars/styles/overlayscrollbars.css';
@ -28,21 +28,26 @@ void initI18n();
const Content = () => {
const { tenants, isSettle, currentTenantId } = useContext(TenantsContext);
const resources = deduplicate([
// Explicitly add `currentTenantId` and deduplicate since the user may directly
// access a URL with Tenant ID, adding the ID from the URL here can possibly remove one
// additional redirect.
...(currentTenantId && [getManagementApi(currentTenantId).indicator]),
...(tenants ?? []).map(({ id }) => getManagementApi(id).indicator),
...(isCloud ? [cloudApi.indicator] : []),
meApi.indicator,
]);
const resources = deduplicate(
conditionalArray(
// Explicitly add `currentTenantId` and deduplicate since the user may directly
// access a URL with Tenant ID, adding the ID from the URL here can possibly remove one
// additional redirect.
currentTenantId && getManagementApi(currentTenantId).indicator,
...(tenants ?? []).map(({ id }) => getManagementApi(id).indicator),
isCloud && cloudApi.indicator,
meApi.indicator
)
);
const scopes = [
UserScope.Email,
UserScope.Identities,
UserScope.CustomData,
PredefinedScope.All,
cloudApi.scopes.CreateTenant, // It's fine to keep scope here since core will filter
...conditionalArray(
isCloud && cloudApi.scopes.CreateTenant,
isCloud && cloudApi.scopes.ManageTenant
),
];
return (
@ -54,26 +59,26 @@ const Content = () => {
scopes,
}}
>
{!isCloud || isSettle ? (
<AppEndpointsProvider>
<AppConfirmModalProvider>
<Main />
</AppConfirmModalProvider>
</AppEndpointsProvider>
) : (
<CloudApp />
)}
<ErrorBoundary>
{!isCloud || isSettle ? (
<AppEndpointsProvider>
<AppConfirmModalProvider>
<Main />
</AppConfirmModalProvider>
</AppEndpointsProvider>
) : (
<CloudApp />
)}
</ErrorBoundary>
</LogtoProvider>
);
};
const App = () => {
return (
<ErrorBoundary>
<TenantsProvider>
<Content />
</TenantsProvider>
</ErrorBoundary>
<TenantsProvider>
<Content />
</TenantsProvider>
);
};
export default App;

View file

@ -1,21 +1,24 @@
import { useLogto } from '@logto/react';
import { useTranslation } from 'react-i18next';
import { useHref } from 'react-router-dom';
import AppError from '../AppError';
import Button from '../Button';
import * as styles from './index.module.scss';
const SessionExpired = () => {
const { error, signIn } = useLogto();
const href = useHref('/callback');
type Props = {
error: Error;
callbackHref?: string;
};
const SessionExpired = ({ callbackHref = '/callback', error }: Props) => {
const { signIn, signOut } = useLogto();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<AppError
title={t('session_expired.title')}
errorMessage={t('session_expired.subtitle')}
callStack={error?.stack}
callStack={error.stack}
>
<Button
className={styles.retryButton}
@ -23,7 +26,7 @@ const SessionExpired = () => {
type="outline"
title="session_expired.button"
onClick={() => {
void signIn(new URL(href, window.location.origin).toString());
void signIn(new URL(callbackHref, window.location.origin).toString());
}}
/>
</AppError>

View file

@ -46,7 +46,7 @@ const AppLayout = () => {
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired />;
return <SessionExpired error={error} callbackHref={href} />;
}
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {

View file

@ -1,7 +1,10 @@
import { conditional } from '@silverhand/essentials';
import { HTTPError } from 'ky';
import type { ReactNode } from 'react';
import { Component } from 'react';
import SessionExpired from '@/components/SessionExpired';
import AppError from '../../components/AppError';
type Props = {
@ -9,30 +12,15 @@ type Props = {
};
type State = {
callStack?: string;
componentStack?: string;
errorMessage?: string;
hasError: boolean;
error?: Error;
};
class ErrorBoundary extends Component<Props, State> {
static getDerivedStateFromError(error: Error): State {
const errorMessage = String(error);
const callStack = conditional(
typeof error === 'object' &&
typeof error.stack === 'string' &&
error.stack.split('\n').slice(1).join('\n')
);
return { callStack, errorMessage, hasError: true };
return { error };
}
public state: State = {
callStack: undefined,
errorMessage: undefined,
hasError: false,
};
public state: State = {};
promiseRejectionHandler(error: unknown) {
this.setState(
@ -54,10 +42,20 @@ class ErrorBoundary extends Component<Props, State> {
render() {
const { children } = this.props;
const { callStack, errorMessage, hasError } = this.state;
const { error } = this.state;
if (hasError) {
return <AppError errorMessage={errorMessage} callStack={callStack} />;
if (error) {
if (error instanceof HTTPError && error.response.status === 401) {
return <SessionExpired error={error} />;
}
const callStack = conditional(
typeof error === 'object' &&
typeof error.stack === 'string' &&
error.stack.split('\n').slice(1).join('\n')
);
return <AppError errorMessage={error.message} callStack={callStack} />;
}
return children;

View file

@ -1,5 +1,6 @@
import { useLogto } from '@logto/react';
import type { RequestErrorBody } from '@logto/schemas';
import { conditionalArray } from '@silverhand/essentials';
import ky from 'ky';
import { useCallback, useContext, useMemo } from 'react';
import { toast } from 'react-hot-toast';
@ -76,15 +77,14 @@ export const useStaticApi = ({
prefixUrl,
timeout: requestTimeout,
hooks: {
beforeError: hideErrorToast
? []
: [
async (error) => {
await toastError(error.response);
beforeError: conditionalArray(
!hideErrorToast &&
(async (error) => {
await toastError(error.response);
return error;
},
],
return error;
})
),
beforeRequest: [
async (request) => {
if (isAuthenticated) {

View file

@ -28,7 +28,7 @@ const Welcome = () => {
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired />;
return <SessionExpired error={error} callbackHref={href} />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;

View file

@ -34,7 +34,7 @@
"@logto/phrases-ui": "workspace:*",
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@withtyped/postgres": "^0.8.1",
"@withtyped/server": "^0.8.0",
"chalk": "^5.0.0",

View file

@ -8,7 +8,7 @@ import {
InteractionEvent,
adminConsoleApplicationId,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { conditional, conditionalArray } from '@silverhand/essentials';
import { EnvSet } from '#src/env-set/index.js';
import type { ConnectorLibrary } from '#src/libraries/connector.js';
@ -16,7 +16,6 @@ import { assignInteractionResults } from '#src/libraries/session.js';
import { encryptUserPassword } from '#src/libraries/user.js';
import type { LogEntry } from '#src/middleware/koa-audit-log.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import { conditionalArray } from '#src/utils/array.js';
import { getTenantId } from '#src/utils/tenant.js';
import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';

View file

@ -1,5 +0,0 @@
import type { Falsy } from '@silverhand/essentials';
import { notFalsy } from '@silverhand/essentials';
export const conditionalArray = <T>(...exp: Array<T | Falsy>): T[] =>
exp.filter((value): value is Exclude<T, Falsy> => notFalsy(value));

View file

@ -27,7 +27,7 @@
"@logto/schemas": "workspace:*",
"@peculiar/webcrypto": "^1.3.3",
"@silverhand/eslint-config": "2.0.1",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@silverhand/ts-config": "2.0.3",
"@types/jest": "^29.1.2",
"@types/jest-environment-puppeteer": "^5.0.2",

View file

@ -35,7 +35,7 @@
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"zod": "^3.20.2"
},
"devDependencies": {

View file

@ -35,7 +35,7 @@
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"zod": "^3.20.2"
},
"devDependencies": {

View file

@ -0,0 +1,56 @@
import { generateStandardId } from '@logto/core-kit';
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const adminTenantId = 'admin';
const alteration: AlterationScript = {
up: async (pool) => {
const scopeId = generateStandardId();
const { id: resourceId } = await pool.one<{ id: string }>(sql`
select id from resources
where tenant_id = ${adminTenantId}
and indicator = 'https://cloud.logto.io/api'
`);
await pool.query(sql`
insert into scopes (tenant_id, id, name, description, resource_id)
values (
${adminTenantId},
${scopeId},
'manage:tenant',
'Allow managing existing tenants, including create without limitation, update, and delete.',
${resourceId}
);
`);
const { id: roleId } = await pool.one<{ id: string }>(sql`
select id from roles
where tenant_id = ${adminTenantId}
and name = 'admin:admin'
`);
await pool.query(sql`
insert into roles_scopes (tenant_id, id, role_id, scope_id)
values (
${adminTenantId},
${generateStandardId()},
${roleId},
${scopeId}
);
`);
},
down: async (pool) => {
await pool.query(sql`
delete from scopes
using resources
where resources.id = scopes.resource_id
and scopes.tenant_id = ${adminTenantId}
and resources.indicator = 'https://cloud.logto.io/api'
and scopes.name='manage:tenant';
`);
},
};
export default alteration;

View file

@ -41,7 +41,7 @@
},
"devDependencies": {
"@silverhand/eslint-config": "2.0.1",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@silverhand/ts-config": "2.0.3",
"@types/inquirer": "^9.0.0",
"@types/jest": "^29.1.2",

View file

@ -1,5 +1,6 @@
import { generateStandardId } from '@logto/core-kit';
import type { CreateScope } from '../index.js';
import { UserRole } from '../types/index.js';
import type { UpdateAdminData } from './management-api.js';
import { adminTenantId } from './tenant.js';
@ -9,28 +10,36 @@ export const cloudApiIndicator = 'https://cloud.logto.io/api';
export enum CloudScope {
CreateTenant = 'create:tenant',
ManageTenant = 'manage:tenant',
}
export const createCloudApi = (): Readonly<UpdateAdminData> => {
export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]> => {
const resourceId = generateStandardId();
return Object.freeze({
resource: {
tenantId: adminTenantId,
id: resourceId,
indicator: cloudApiIndicator,
name: `Logto Cloud API`,
},
scope: {
tenantId: adminTenantId,
id: generateStandardId(),
name: CloudScope.CreateTenant,
description: 'Allow creating new tenants.',
resourceId,
},
role: {
tenantId: adminTenantId,
name: UserRole.User,
},
const buildScope = (name: CloudScope, description: string) => ({
tenantId: adminTenantId,
id: generateStandardId(),
name,
description,
resourceId,
});
return Object.freeze([
{
resource: {
tenantId: adminTenantId,
id: resourceId,
indicator: cloudApiIndicator,
name: `Logto Cloud API`,
},
scope: buildScope(CloudScope.CreateTenant, 'Allow creating new tenants.'),
role: {
tenantId: adminTenantId,
name: UserRole.User,
},
},
buildScope(
CloudScope.ManageTenant,
'Allow managing existing tenants, including create without limitation, update, and delete.'
),
]);
};

View file

@ -56,7 +56,7 @@
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"chalk": "^5.0.0",
"find-up": "^6.3.0",
"nanoid": "^4.0.0",

View file

@ -34,7 +34,7 @@
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.2.0"
"@silverhand/essentials": "2.3.0"
},
"optionalDependencies": {
"zod": "^3.20.2"

View file

@ -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.2.0",
"@silverhand/essentials": "2.3.0",
"@silverhand/ts-config": "2.0.3",
"@types/color": "^3.0.3",
"@types/jest": "^29.0.3",

View file

@ -31,7 +31,7 @@
"@react-spring/web": "^9.6.1",
"@silverhand/eslint-config": "2.0.1",
"@silverhand/eslint-config-react": "2.0.1",
"@silverhand/essentials": "2.2.0",
"@silverhand/essentials": "2.3.0",
"@silverhand/jest-config": "1.2.2",
"@silverhand/ts-config": "2.0.3",
"@silverhand/ts-config-react": "2.0.3",

View file

@ -31,7 +31,7 @@ importers:
'@logto/schemas': workspace:*
'@logto/shared': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/inquirer': ^9.0.0
'@types/jest': ^29.1.2
@ -68,7 +68,7 @@ importers:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
chalk: 5.1.2
decamelize: 6.0.0
dotenv: 16.0.0
@ -111,7 +111,7 @@ importers:
'@logto/schemas': workspace:*
'@logto/shared': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/http-proxy': ^1.17.9
'@types/mime-types': ^2.1.1
@ -137,7 +137,7 @@ importers:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0
'@withtyped/server': 0.8.0
chalk: 5.1.2
@ -178,7 +178,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.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@silverhand/ts-config-react': 2.0.3
'@tsconfig/docusaurus': ^1.0.5
@ -252,7 +252,7 @@ importers:
'@parcel/transformer-svg-react': 2.8.3_@parcel+core@2.8.3
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/eslint-config-react': 2.0.1_kz2ighe3mj4zdkvq5whtl3dq4u
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@silverhand/ts-config-react': 2.0.3_typescript@4.9.4
'@tsconfig/docusaurus': 1.0.5
@ -323,7 +323,7 @@ importers:
'@logto/schemas': workspace:*
'@logto/shared': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/debug': ^4.1.7
'@types/etag': ^1.8.1
@ -400,7 +400,7 @@ importers:
'@logto/phrases-ui': link:../phrases-ui
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0
'@withtyped/server': 0.8.0
chalk: 5.1.2
@ -545,7 +545,7 @@ importers:
'@logto/schemas': workspace:*
'@peculiar/webcrypto': ^1.3.3
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/jest': ^29.1.2
'@types/jest-environment-puppeteer': ^5.0.2
@ -573,7 +573,7 @@ importers:
'@logto/schemas': link:../schemas
'@peculiar/webcrypto': 1.3.3
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@types/jest': 29.1.2
'@types/jest-environment-puppeteer': 5.0.2
@ -596,7 +596,7 @@ importers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
eslint: ^8.34.0
lint-staged: ^13.0.0
@ -606,7 +606,7 @@ importers:
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/language-kit': link:../toolkit/language-kit
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
zod: 3.20.2
devDependencies:
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
@ -621,7 +621,7 @@ importers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
buffer: ^5.7.1
eslint: ^8.34.0
@ -632,7 +632,7 @@ importers:
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/language-kit': link:../toolkit/language-kit
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
zod: 3.20.2
devDependencies:
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
@ -651,7 +651,7 @@ importers:
'@logto/phrases': workspace:*
'@logto/phrases-ui': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/inquirer': ^9.0.0
'@types/jest': ^29.1.2
@ -680,7 +680,7 @@ importers:
zod: 3.20.2
devDependencies:
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@types/inquirer': 9.0.3
'@types/jest': 29.1.2
@ -704,7 +704,7 @@ importers:
'@logto/core-kit': workspace:*
'@logto/schemas': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/jest': ^29.1.2
'@types/node': ^18.11.18
@ -720,7 +720,7 @@ importers:
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
chalk: 5.1.2
find-up: 6.3.0
nanoid: 4.0.0
@ -742,7 +742,7 @@ importers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/node': ^18.11.18
eslint: ^8.34.0
@ -754,7 +754,7 @@ importers:
dependencies:
'@logto/core-kit': link:../core-kit
'@logto/language-kit': link:../language-kit
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
optionalDependencies:
zod: 3.20.2
devDependencies:
@ -773,7 +773,7 @@ importers:
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/eslint-config-react': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3
'@types/color': ^3.0.3
'@types/jest': ^29.0.3
@ -800,7 +800,7 @@ importers:
'@jest/types': 29.3.1
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/eslint-config-react': 2.0.1_wfldc7mlde5bb3fdzap5arn6me
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@types/color': 3.0.3
'@types/jest': 29.1.2
@ -860,7 +860,7 @@ importers:
'@react-spring/web': ^9.6.1
'@silverhand/eslint-config': 2.0.1
'@silverhand/eslint-config-react': 2.0.1
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/jest-config': 1.2.2
'@silverhand/ts-config': 2.0.3
'@silverhand/ts-config-react': 2.0.3
@ -918,7 +918,7 @@ importers:
'@react-spring/web': 9.6.1_biqbaboplfbrettd7655fr4n2y
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/eslint-config-react': 2.0.1_kz2ighe3mj4zdkvq5whtl3dq4u
'@silverhand/essentials': 2.2.0
'@silverhand/essentials': 2.3.0
'@silverhand/jest-config': 1.2.2_ky6c64xxalg2hsll4xx3evq2dy
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@silverhand/ts-config-react': 2.0.3_typescript@4.9.4
@ -3623,8 +3623,8 @@ packages:
lodash.pick: 4.4.0
dev: true
/@silverhand/essentials/2.2.0:
resolution: {integrity: sha512-xoj/wAnPUt9ZAzt7QCHhSKZPweZnNJU7tBYrDTf54db6L+++SiXYIyckKDY+vKkGACn9kTAWPF74qSfYt1OQtA==}
/@silverhand/essentials/2.3.0:
resolution: {integrity: sha512-vZ8eT0ew2bTIo86vwcPSduL1o2oXxH9DPnQe8sV3K3g1/sSgCyAj9ULHObZLjg/YNGlp16Wiby1hSs8P9VtU7g==}
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^7}
/@silverhand/jest-config/1.2.2_ky6c64xxalg2hsll4xx3evq2dy: