0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): should limit only the number of free tenant (#4205)

* fix(console): update free tenant limit

* fix(console): use TenantResponse type

* chore(console): update dependency on @logto/cloud

* chore: remove import as
This commit is contained in:
Darcy Ye 2023-07-24 14:40:47 +08:00 committed by GitHub
parent 3ef8c06d4a
commit 350d070ef7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 71 additions and 70 deletions

View file

@ -26,7 +26,7 @@
"@fontsource/roboto-mono": "^5.0.0", "@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0", "@jest/types": "^29.5.0",
"@logto/app-insights": "workspace:^1.3.1", "@logto/app-insights": "workspace:^1.3.1",
"@logto/cloud": "0.2.5-2087c06", "@logto/cloud": "0.2.5-70aa370",
"@logto/connector-kit": "workspace:^1.1.1", "@logto/connector-kit": "workspace:^1.1.1",
"@logto/core-kit": "workspace:^2.0.1", "@logto/core-kit": "workspace:^2.0.1",
"@logto/language-kit": "workspace:^1.0.0", "@logto/language-kit": "workspace:^1.0.0",
@ -60,7 +60,7 @@
"@types/react-helmet": "^6.1.6", "@types/react-helmet": "^6.1.6",
"@types/react-modal": "^3.13.1", "@types/react-modal": "^3.13.1",
"@types/react-syntax-highlighter": "^15.5.1", "@types/react-syntax-highlighter": "^15.5.1",
"@withtyped/client": "^0.7.19", "@withtyped/client": "^0.7.21",
"buffer": "^5.7.1", "buffer": "^5.7.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",

View file

@ -1,11 +1,11 @@
import { Theme } from '@logto/schemas'; import { Theme } from '@logto/schemas';
import type { TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import Plus from '@/assets/icons/plus.svg'; import Plus from '@/assets/icons/plus.svg';
import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark.svg'; import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark.svg';
import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg'; import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg';
import { type TenantResponse } from '@/cloud/types/router';
import CreateTenantModal from '@/components/CreateTenantModal'; import CreateTenantModal from '@/components/CreateTenantModal';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button'; import Button from '@/ds-components/Button';
@ -54,7 +54,7 @@ function TenantLandingPageContent({ className }: Props) {
<CreateTenantModal <CreateTenantModal
skipPlanSelection skipPlanSelection
isOpen={isCreateModalOpen} isOpen={isCreateModalOpen}
onClose={async (tenant?: TenantInfo) => { onClose={async (tenant?: TenantResponse) => {
if (tenant) { if (tenant) {
prependTenant(tenant); prependTenant(tenant);
navigateTenant(tenant.id); navigateTenant(tenant.id);

View file

@ -2,6 +2,7 @@ import type router from '@logto/cloud/routes';
import { type GuardedResponse, type RouterRoutes } from '@withtyped/client'; import { type GuardedResponse, type RouterRoutes } from '@withtyped/client';
type GetRoutes = RouterRoutes<typeof router>['get']; type GetRoutes = RouterRoutes<typeof router>['get'];
type GetArrayElementType<T> = T extends Array<infer U> ? U : never;
export type SubscriptionPlanResponse = GuardedResponse< export type SubscriptionPlanResponse = GuardedResponse<
GetRoutes['/api/subscription-plans'] GetRoutes['/api/subscription-plans']
@ -12,3 +13,6 @@ export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/sub
export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>; export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>;
export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>; export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;
// The response of GET /api/tenants is TenantResponse[].
export type TenantResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/tenants']>>;

View file

@ -1,4 +1,4 @@
import { maxFreeTenantLimit } from '@logto/schemas'; import { maxFreeTenantLimit, adminTenantId } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
@ -51,8 +51,13 @@ function PlanCardItem({ plan, onSelect }: Props) {
const isFreePlan = planId === ReservedPlanId.free; const isFreePlan = planId === ReservedPlanId.free;
// Todo: @xiaoyijun filter our all free tenants const isFreeTenantExceeded = useMemo(
const isFreeTenantExceeded = tenants.length >= maxFreeTenantLimit; () =>
/** Should not block admin tenant owners from creating more than three tenants */
!tenants.some(({ id }) => id === adminTenantId) &&
tenants.filter(({ planId }) => planId === ReservedPlanId.free).length >= maxFreeTenantLimit,
[tenants]
);
return ( return (
<div className={styles.container}> <div className={styles.container}>

View file

@ -1,9 +1,9 @@
import { type TenantInfo } from '@logto/schemas/models';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type TenantResponse } from '@/cloud/types/router';
import { ReservedPlanId } from '@/consts/subscriptions'; import { ReservedPlanId } from '@/consts/subscriptions';
import DangerousRaw from '@/ds-components/DangerousRaw'; import DangerousRaw from '@/ds-components/DangerousRaw';
import ModalLayout from '@/ds-components/ModalLayout'; import ModalLayout from '@/ds-components/ModalLayout';
@ -20,7 +20,7 @@ import * as styles from './index.module.scss';
type Props = { type Props = {
tenantData?: CreateTenantData; tenantData?: CreateTenantData;
onClose: (tenant?: TenantInfo) => void; onClose: (tenant?: TenantResponse) => void;
}; };
function SelectTenantPlanModal({ tenantData, onClose }: Props) { function SelectTenantPlanModal({ tenantData, onClose }: Props) {

View file

@ -1,6 +1,6 @@
import type { AdminConsoleKey } from '@logto/phrases'; import type { AdminConsoleKey } from '@logto/phrases';
import { Theme } from '@logto/schemas'; import { Theme } from '@logto/schemas';
import { TenantTag, type TenantInfo } from '@logto/schemas/models'; import { TenantTag } from '@logto/schemas/models';
import { useState } from 'react'; import { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form'; import { Controller, FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@ -10,6 +10,7 @@ import Modal from 'react-modal';
import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark.svg'; import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark.svg';
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg'; import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type TenantResponse } from '@/cloud/types/router';
import { isProduction } from '@/consts/env'; import { isProduction } from '@/consts/env';
import Button from '@/ds-components/Button'; import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
@ -25,7 +26,7 @@ import { type CreateTenantData } from './type';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: (tenant?: TenantInfo) => void; onClose: (tenant?: TenantResponse) => void;
// eslint-disable-next-line react/boolean-prop-naming // eslint-disable-next-line react/boolean-prop-naming
skipPlanSelection?: boolean; skipPlanSelection?: boolean;
}; };

View file

@ -1,7 +1,7 @@
import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import Tick from '@/assets/icons/tick.svg'; import Tick from '@/assets/icons/tick.svg';
import { type TenantResponse } from '@/cloud/types/router';
import PlanName from '@/components/PlanName'; import PlanName from '@/components/PlanName';
import { isProduction } from '@/consts/env'; import { isProduction } from '@/consts/env';
import { DropdownItem } from '@/ds-components/Dropdown'; import { DropdownItem } from '@/ds-components/Dropdown';
@ -14,7 +14,7 @@ import TenantStatusTag from './TenantStatusTag';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
tenantData: TenantInfo; tenantData: TenantResponse;
isSelected: boolean; isSelected: boolean;
onClick: () => void; onClick: () => void;
}; };

View file

@ -101,19 +101,4 @@ $dropdown-item-height: 40px;
height: 20px; height: 20px;
color: var(--color-neutral-50); color: var(--color-neutral-50);
} }
&.disabled {
&:hover {
background: transparent;
}
&:not(:disabled) {
cursor: not-allowed;
}
> div,
> svg {
color: var(--color-placeholder);
}
}
} }

View file

@ -1,11 +1,9 @@
import { adminTenantId, maxFreeTenantLimit } from '@logto/schemas'; import { useContext, useRef, useState } from 'react';
import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames';
import { useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg'; import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
import PlusSign from '@/assets/icons/plus.svg'; import PlusSign from '@/assets/icons/plus.svg';
import { type TenantResponse } from '@/cloud/types/router';
import CreateTenantModal from '@/components/CreateTenantModal'; import CreateTenantModal from '@/components/CreateTenantModal';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import Divider from '@/ds-components/Divider'; import Divider from '@/ds-components/Divider';
@ -27,12 +25,6 @@ export default function TenantSelector() {
navigateTenant, navigateTenant,
} = useContext(TenantsContext); } = useContext(TenantsContext);
const isCreateButtonDisabled = useMemo(
() =>
/** Should not block admin tenant owners from creating more than three tenants */
!tenants.some(({ id }) => id === adminTenantId) && tenants.length >= maxFreeTenantLimit,
[tenants]
);
const anchorRef = useRef<HTMLDivElement>(null); const anchorRef = useRef<HTMLDivElement>(null);
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false); const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
@ -85,11 +77,7 @@ export default function TenantSelector() {
<Divider /> <Divider />
<button <button
tabIndex={0} tabIndex={0}
className={classNames( className={styles.createTenantButton}
isCreateButtonDisabled && styles.disabled,
styles.createTenantButton
)}
disabled={isCreateButtonDisabled}
onClick={() => { onClick={() => {
setShowCreateTenantModal(true); setShowCreateTenantModal(true);
}} }}
@ -103,7 +91,7 @@ export default function TenantSelector() {
</Dropdown> </Dropdown>
<CreateTenantModal <CreateTenantModal
isOpen={showCreateTenantModal} isOpen={showCreateTenantModal}
onClose={async (tenant?: TenantInfo) => { onClose={async (tenant?: TenantResponse) => {
if (tenant) { if (tenant) {
prependTenant(tenant); prependTenant(tenant);
navigateTenant(tenant.id); navigateTenant(tenant.id);

View file

@ -1,10 +1,10 @@
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import { type TenantInfo } from '@logto/schemas/lib/models/tenants.js';
import { trySafe } from '@silverhand/essentials'; import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { useSWRConfig } from 'swr'; import { useSWRConfig } from 'swr';
import { type TenantResponse } from '@/cloud/types/router';
import AppLoading from '@/components/AppLoading'; import AppLoading from '@/components/AppLoading';
// Used in the docs // Used in the docs
// eslint-disable-next-line unused-imports/no-unused-imports // eslint-disable-next-line unused-imports/no-unused-imports
@ -63,7 +63,7 @@ export default function TenantAccess() {
}, [mutate, currentTenantId]); }, [mutate, currentTenantId]);
useEffect(() => { useEffect(() => {
const validate = async ({ indicator }: TenantInfo) => { const validate = async ({ indicator }: TenantResponse) => {
// Test fetching an access token for the current Tenant ID. // Test fetching an access token for the current Tenant ID.
// If failed, it means the user finishes the first auth, ands still needs to auth again to // If failed, it means the user finishes the first auth, ands still needs to auth again to
// fetch the full-scoped (with all available tenants) token. // fetch the full-scoped (with all available tenants) token.

View file

@ -1,11 +1,13 @@
import { defaultManagementApi, defaultTenantId } from '@logto/schemas'; import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
import { type TenantInfo, TenantTag } from '@logto/schemas/models'; import { TenantTag } from '@logto/schemas/models';
import { conditionalArray, noop } from '@silverhand/essentials'; import { conditionalArray, noop } from '@silverhand/essentials';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useMemo, createContext, useState } from 'react'; import { useCallback, useMemo, createContext, useState } from 'react';
import { useMatch, useNavigate } from 'react-router-dom'; import { useMatch, useNavigate } from 'react-router-dom';
import { type TenantResponse } from '@/cloud/types/router';
import { isCloud } from '@/consts/env'; import { isCloud } from '@/consts/env';
import { ReservedPlanId } from '@/consts/subscriptions';
/** /**
* The routes don't start with a tenant ID. * The routes don't start with a tenant ID.
@ -32,20 +34,20 @@ type CurrentTenantStatus = 'pending' | 'validating' | 'validated';
/** @see {@link TenantsProvider} for why `useSWR()` is not applicable for this context. */ /** @see {@link TenantsProvider} for why `useSWR()` is not applicable for this context. */
type Tenants = { type Tenants = {
tenants: readonly TenantInfo[]; tenants: readonly TenantResponse[];
/** Indicates if the tenants data is ready for the first render. */ /** Indicates if the tenants data is ready for the first render. */
isInitComplete: boolean; isInitComplete: boolean;
/** Reset tenants to the given value. It will overwrite the current tenants data and set `isInitComplete` to `true`. */ /** Reset tenants to the given value. It will overwrite the current tenants data and set `isInitComplete` to `true`. */
resetTenants: (tenants: TenantInfo[]) => void; resetTenants: (tenants: TenantResponse[]) => void;
/** Prepend a new tenant to the current tenants data. */ /** Prepend a new tenant to the current tenants data. */
prependTenant: (tenant: TenantInfo) => void; prependTenant: (tenant: TenantResponse) => void;
/** Remove a tenant by ID from the current tenants data. */ /** Remove a tenant by ID from the current tenants data. */
removeTenant: (tenantId: string) => void; removeTenant: (tenantId: string) => void;
/** Update a tenant by ID if it exists in the current tenants data. */ /** Update a tenant by ID if it exists in the current tenants data. */
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => void; updateTenant: (tenantId: string, data: Partial<TenantResponse>) => void;
/** The current tenant ID parsed from the URL. */ /** The current tenant ID parsed from the URL. */
currentTenantId: string; currentTenantId: string;
currentTenant?: TenantInfo; currentTenant?: TenantResponse;
/** /**
* Indicates if the Access Token has been validated for use. Will be reset to `pending` when the current tenant changes. * Indicates if the Access Token has been validated for use. Will be reset to `pending` when the current tenant changes.
* *
@ -65,7 +67,13 @@ const { tenantId, indicator } = defaultManagementApi.resource;
*/ */
const initialTenants = Object.freeze( const initialTenants = Object.freeze(
conditionalArray( conditionalArray(
!isCloud && { id: tenantId, name: `tenant_${tenantId}`, tag: TenantTag.Development, indicator } !isCloud && {
id: tenantId,
name: `tenant_${tenantId}`,
tag: TenantTag.Development,
indicator,
planId: `${ReservedPlanId.free}`, // `planId` is string type.
}
) )
); );
@ -132,18 +140,18 @@ function TenantsProvider({ children }: Props) {
const memorizedContext = useMemo( const memorizedContext = useMemo(
() => ({ () => ({
tenants, tenants,
resetTenants: (tenants: TenantInfo[]) => { resetTenants: (tenants: TenantResponse[]) => {
setTenants(tenants); setTenants(tenants);
setCurrentTenantStatus('pending'); setCurrentTenantStatus('pending');
setIsInitComplete(true); setIsInitComplete(true);
}, },
prependTenant: (tenant: TenantInfo) => { prependTenant: (tenant: TenantResponse) => {
setTenants((tenants) => [tenant, ...tenants]); setTenants((tenants) => [tenant, ...tenants]);
}, },
removeTenant: (tenantId: string) => { removeTenant: (tenantId: string) => {
setTenants((tenants) => tenants.filter((tenant) => tenant.id !== tenantId)); setTenants((tenants) => tenants.filter((tenant) => tenant.id !== tenantId));
}, },
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => { updateTenant: (tenantId: string, data: Partial<TenantResponse>) => {
setTenants((tenants) => setTenants((tenants) =>
tenants.map((tenant) => (tenant.id === tenantId ? { ...tenant, ...data } : tenant)) tenants.map((tenant) => (tenant.id === tenantId ? { ...tenant, ...data } : tenant))
); );

View file

@ -1,7 +1,7 @@
import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { type TenantResponse } from '@/cloud/types/router';
import { contactEmailLink } from '@/consts'; import { contactEmailLink } from '@/consts';
import { tenantTagMap } from '@/containers/AppContent/components/Topbar/TenantSelector/TenantEnvTag'; import { tenantTagMap } from '@/containers/AppContent/components/Topbar/TenantSelector/TenantEnvTag';
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal'; import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
@ -14,7 +14,7 @@ type Props = {
isLoading: boolean; isLoading: boolean;
onClose: () => void; onClose: () => void;
onDelete: () => void; onDelete: () => void;
tenant: Pick<TenantInfo, 'name' | 'tag'>; tenant: Pick<TenantResponse, 'name' | 'tag'>;
}; };
function DeleteModal({ isOpen, isLoading, onClose, onDelete, tenant }: Props) { function DeleteModal({ isOpen, isLoading, onClose, onDelete, tenant }: Props) {

View file

@ -1,4 +1,4 @@
import { type TenantInfo, TenantTag } from '@logto/schemas/models'; import { TenantTag } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
@ -6,6 +6,7 @@ import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type TenantResponse } from '@/cloud/types/router';
import AppError from '@/components/AppError'; import AppError from '@/components/AppError';
import PageMeta from '@/components/PageMeta'; import PageMeta from '@/components/PageMeta';
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar'; import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
@ -18,7 +19,7 @@ import ProfileForm from './ProfileForm';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
import { type TenantSettingsForm } from './types.js'; import { type TenantSettingsForm } from './types.js';
const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => { const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
return { return {
profile: { name: tenant?.name ?? 'My project', tag: tenant?.tag ?? TenantTag.Development }, profile: { name: tenant?.name ?? 'My project', tag: tenant?.tag ?? TenantTag.Development },
}; };

View file

@ -2755,8 +2755,8 @@ importers:
specifier: workspace:^1.3.1 specifier: workspace:^1.3.1
version: link:../app-insights version: link:../app-insights
'@logto/cloud': '@logto/cloud':
specifier: 0.2.5-2087c06 specifier: 0.2.5-70aa370
version: 0.2.5-2087c06(zod@3.20.2) version: 0.2.5-70aa370(zod@3.20.2)
'@logto/connector-kit': '@logto/connector-kit':
specifier: workspace:^1.1.1 specifier: workspace:^1.1.1
version: link:../toolkit/connector-kit version: link:../toolkit/connector-kit
@ -2857,8 +2857,8 @@ importers:
specifier: ^15.5.1 specifier: ^15.5.1
version: 15.5.1 version: 15.5.1
'@withtyped/client': '@withtyped/client':
specifier: ^0.7.19 specifier: ^0.7.21
version: 0.7.19(zod@3.20.2) version: 0.7.21(zod@3.20.2)
buffer: buffer:
specifier: ^5.7.1 specifier: ^5.7.1
version: 5.7.1 version: 5.7.1
@ -7210,8 +7210,8 @@ packages:
jose: 4.14.4 jose: 4.14.4
dev: true dev: true
/@logto/cloud@0.2.5-2087c06(zod@3.20.2): /@logto/cloud@0.2.5-4d5e389(zod@3.20.2):
resolution: {integrity: sha512-v/zisil/t8XrlFxlVic+0O6T2J3FUzB5FDC1w3OYNzhXNbSpmbqlt5vyN9RIwXUGgAKjdhKQjoACpV7HpPFZcQ==} resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
dependencies: dependencies:
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.7.0
@ -7220,12 +7220,12 @@ packages:
- zod - zod
dev: true dev: true
/@logto/cloud@0.2.5-4d5e389(zod@3.20.2): /@logto/cloud@0.2.5-70aa370(zod@3.20.2):
resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==} resolution: {integrity: sha512-V5fIsbotJ8+L6Q+R9PnLjspvuccDKpukpLz/uHRUv4SYDo8U5MAcC680T2TGiROtMjWqnb0vd6WHuqXZ9XWTcw==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
dependencies: dependencies:
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.7.0
'@withtyped/server': 0.12.7(zod@3.20.2) '@withtyped/server': 0.12.8(zod@3.20.2)
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
dev: true dev: true
@ -9855,6 +9855,16 @@ packages:
'@withtyped/shared': 0.2.2 '@withtyped/shared': 0.2.2
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
dev: false
/@withtyped/client@0.7.21(zod@3.20.2):
resolution: {integrity: sha512-N9dvH5nqIwaT7YxaIm83RRQf9AEjxwJ4ugJviZJSxtWy8zLul2/odEMc6epieylFVa6CcLg82yJmRSlqPtJiTw==}
dependencies:
'@withtyped/server': 0.12.8(zod@3.20.2)
'@withtyped/shared': 0.2.2
transitivePeerDependencies:
- zod
dev: true
/@withtyped/server@0.12.7(zod@3.20.2): /@withtyped/server@0.12.7(zod@3.20.2):
resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==} resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}
@ -9873,7 +9883,6 @@ packages:
'@silverhand/essentials': 2.7.0 '@silverhand/essentials': 2.7.0
'@withtyped/shared': 0.2.2 '@withtyped/shared': 0.2.2
zod: 3.20.2 zod: 3.20.2
dev: false
/@withtyped/shared@0.2.2: /@withtyped/shared@0.2.2:
resolution: {integrity: sha512-Vpcj12NqaoZ8M5Z/1kffheI9FBZEm9goed0THmgTcMKXLHjXSRbMZMp0olVxovEgaTIAydshqJOQUXKZMctIZw==} resolution: {integrity: sha512-Vpcj12NqaoZ8M5Z/1kffheI9FBZEm9goed0THmgTcMKXLHjXSRbMZMp0olVxovEgaTIAydshqJOQUXKZMctIZw==}