mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat: report subscription usage updates (#6419)
* feat: report subscription usage updates * refactor: refactor code according to CR
This commit is contained in:
parent
ddfa7aa96e
commit
cec08acb52
15 changed files with 222 additions and 26 deletions
|
@ -95,7 +95,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-3b703da",
|
||||
"@logto/cloud": "0.2.5-50ff8fe",
|
||||
"@silverhand/eslint-config": "6.0.1",
|
||||
"@silverhand/ts-config": "6.0.0",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
|
|
|
@ -7,8 +7,10 @@ import type Queries from '#src/tenants/Queries.js';
|
|||
import assertThat from '#src/utils/assert-that.js';
|
||||
import {
|
||||
getTenantSubscriptionPlan,
|
||||
getTenantSubscriptionQuotaAndUsage,
|
||||
getTenantSubscriptionData,
|
||||
getTenantSubscriptionScopeUsage,
|
||||
reportSubscriptionUpdates,
|
||||
isReportSubscriptionUpdatesUsageKey,
|
||||
} from '#src/utils/subscription/index.js';
|
||||
import { type SubscriptionQuota, type FeatureQuota } from '#src/utils/subscription/types.js';
|
||||
|
||||
|
@ -21,6 +23,11 @@ const notNumber = (): never => {
|
|||
throw new Error('Only support usage query for numeric quota');
|
||||
};
|
||||
|
||||
const shouldReportSubscriptionUpdates = (planId: string, key: keyof SubscriptionQuota): boolean =>
|
||||
EnvSet.values.isDevFeaturesEnabled &&
|
||||
planId === ReservedPlanId.Pro &&
|
||||
isReportSubscriptionUpdatesUsageKey(key);
|
||||
|
||||
export const createQuotaLibrary = (
|
||||
queries: Queries,
|
||||
cloudConnection: CloudConnectionLibrary,
|
||||
|
@ -156,9 +163,16 @@ export const createQuotaLibrary = (
|
|||
return;
|
||||
}
|
||||
|
||||
const { quota: fullQuota, usage: fullUsage } = await getTenantSubscriptionQuotaAndUsage(
|
||||
cloudConnection
|
||||
);
|
||||
const {
|
||||
planId,
|
||||
quota: fullQuota,
|
||||
usage: fullUsage,
|
||||
} = await getTenantSubscriptionData(cloudConnection);
|
||||
|
||||
// Do not block Pro plan from adding add-on resources.
|
||||
if (shouldReportSubscriptionUpdates(planId, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Type `SubscriptionQuota` and type `SubscriptionUsage` are sharing keys, this design helps us to compare the usage with the quota limit in a easier way.
|
||||
const { [key]: limit } = fullQuota;
|
||||
|
@ -227,7 +241,7 @@ export const createQuotaLibrary = (
|
|||
},
|
||||
scopeUsages,
|
||||
] = await Promise.all([
|
||||
getTenantSubscriptionQuotaAndUsage(cloudConnection),
|
||||
getTenantSubscriptionData(cloudConnection),
|
||||
getTenantSubscriptionScopeUsage(cloudConnection, entityName),
|
||||
]);
|
||||
const usage = scopeUsages[entityId] ?? 0;
|
||||
|
@ -262,5 +276,30 @@ export const createQuotaLibrary = (
|
|||
);
|
||||
};
|
||||
|
||||
return { guardKey, guardTenantUsageByKey, guardEntityScopesUsage };
|
||||
const reportSubscriptionUpdatesUsage = async (key: keyof SubscriptionQuota) => {
|
||||
const { isCloud, isIntegrationTest } = EnvSet.values;
|
||||
|
||||
// Cloud only feature, skip in non-cloud environments
|
||||
if (!isCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable in integration tests
|
||||
if (isIntegrationTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { planId } = await getTenantSubscriptionData(cloudConnection);
|
||||
|
||||
if (shouldReportSubscriptionUpdates(planId, key)) {
|
||||
await reportSubscriptionUpdates(cloudConnection, key);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
guardKey,
|
||||
guardTenantUsageByKey,
|
||||
guardEntityScopesUsage,
|
||||
reportSubscriptionUpdatesUsage,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { type Nullable } from '@silverhand/essentials';
|
||||
import type { MiddlewareType } from 'koa';
|
||||
|
||||
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
||||
|
@ -45,3 +46,18 @@ export function newKoaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
|||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
export function koaReportSubscriptionUpdates<StateT, ContextT, ResponseBodyT>({
|
||||
key,
|
||||
quota,
|
||||
methods = ['POST', 'PUT', 'DELETE'],
|
||||
}: NewUsageGuardConfig): MiddlewareType<StateT, ContextT, Nullable<ResponseBodyT>> {
|
||||
return async (ctx, next) => {
|
||||
await next();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (methods.includes(ctx.method.toUpperCase() as Method)) {
|
||||
await quota.reportSubscriptionUpdatesUsage(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -213,6 +213,11 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
|||
}
|
||||
|
||||
ctx.body = application;
|
||||
|
||||
if (rest.type === ApplicationType.MachineToMachine) {
|
||||
await quota.reportSubscriptionUpdatesUsage('machineToMachineLimit');
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
@ -356,6 +361,10 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
|||
await queries.applications.deleteApplicationById(id);
|
||||
ctx.status = 204;
|
||||
|
||||
if (type === ApplicationType.MachineToMachine) {
|
||||
await quota.reportSubscriptionUpdatesUsage('machineToMachineLimit');
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -18,7 +18,10 @@ import { EnvSet } from '#src/env-set/index.js';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
koaReportSubscriptionUpdates,
|
||||
newKoaQuotaGuard,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import { type AllowedKeyPrefix } from '#src/queries/log.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
|
@ -169,6 +172,11 @@ export default function hookRoutes<T extends ManagementApiRouter>(
|
|||
response: Hooks.guard,
|
||||
status: [201, 400],
|
||||
}),
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'hooksLimit',
|
||||
quota,
|
||||
methods: ['POST'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { event, events, enabled, ...rest } = ctx.guard.body;
|
||||
assertThat(events ?? event, new RequestError({ code: 'hook.missing_events', status: 400 }));
|
||||
|
@ -257,6 +265,11 @@ export default function hookRoutes<T extends ManagementApiRouter>(
|
|||
router.delete(
|
||||
'/hooks/:id',
|
||||
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'hooksLimit',
|
||||
quota,
|
||||
methods: ['DELETE'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
await deleteHookById(id);
|
||||
|
|
|
@ -6,13 +6,17 @@ import {
|
|||
type OrganizationRoleKeys,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { condArray } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
koaReportSubscriptionUpdates,
|
||||
newKoaQuotaGuard,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import { organizationRoleSearchKeys } from '#src/queries/organization/index.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
import { parseSearchOptions } from '#src/utils/search.js';
|
||||
|
@ -45,11 +49,17 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
|
|||
unknown,
|
||||
ManagementApiRouterContext
|
||||
>(OrganizationRoles, roles, {
|
||||
middlewares: [
|
||||
middlewares: condArray(
|
||||
EnvSet.values.isDevFeaturesEnabled
|
||||
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||
],
|
||||
EnvSet.values.isDevFeaturesEnabled &&
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'organizationsEnabled',
|
||||
quota,
|
||||
methods: ['POST', 'PUT', 'DELETE'],
|
||||
})
|
||||
),
|
||||
disabled: { get: true, post: true },
|
||||
errorHandler,
|
||||
searchFields: ['name'],
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { OrganizationScopes } from '@logto/schemas';
|
||||
import { condArray } from '@silverhand/essentials';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
newKoaQuotaGuard,
|
||||
koaReportSubscriptionUpdates,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { errorHandler } from '../organization/utils.js';
|
||||
|
@ -19,11 +23,17 @@ export default function organizationScopeRoutes<T extends ManagementApiRouter>(
|
|||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(OrganizationScopes, scopes, {
|
||||
middlewares: [
|
||||
middlewares: condArray(
|
||||
EnvSet.values.isDevFeaturesEnabled
|
||||
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||
],
|
||||
EnvSet.values.isDevFeaturesEnabled &&
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'organizationsEnabled',
|
||||
quota,
|
||||
methods: ['POST', 'PUT', 'DELETE'],
|
||||
})
|
||||
),
|
||||
errorHandler,
|
||||
searchFields: ['name'],
|
||||
});
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { type OrganizationWithFeatured, Organizations, featuredUserGuard } from '@logto/schemas';
|
||||
import { yes } from '@silverhand/essentials';
|
||||
import { condArray, yes } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
newKoaQuotaGuard,
|
||||
koaReportSubscriptionUpdates,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
import { parseSearchOptions } from '#src/utils/search.js';
|
||||
|
||||
|
@ -31,11 +34,17 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
|
|||
] = args;
|
||||
|
||||
const router = new SchemaRouter(Organizations, organizations, {
|
||||
middlewares: [
|
||||
middlewares: condArray(
|
||||
EnvSet.values.isDevFeaturesEnabled
|
||||
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||
],
|
||||
EnvSet.values.isDevFeaturesEnabled &&
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'organizationsEnabled',
|
||||
quota,
|
||||
methods: ['POST', 'PUT', 'DELETE'],
|
||||
})
|
||||
),
|
||||
errorHandler,
|
||||
searchFields: ['name'],
|
||||
disabled: { get: true },
|
||||
|
|
|
@ -7,7 +7,10 @@ import { EnvSet } from '#src/env-set/index.js';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
newKoaQuotaGuard,
|
||||
koaReportSubscriptionUpdates,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { attachScopesToResources } from '#src/utils/resource.js';
|
||||
|
||||
|
@ -87,6 +90,11 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
|||
response: Resources.guard.extend({ scopes: Scopes.guard.array().optional() }),
|
||||
status: [201, 422],
|
||||
}),
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'resourcesLimit',
|
||||
quota,
|
||||
methods: ['POST'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { body } = ctx.guard;
|
||||
const { indicator } = body;
|
||||
|
@ -184,6 +192,11 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
|||
router.delete(
|
||||
'/resources/:id',
|
||||
koaGuard({ params: object({ id: string().min(1) }), status: [204, 400, 404] }),
|
||||
koaReportSubscriptionUpdates({
|
||||
key: 'resourcesLimit',
|
||||
quota,
|
||||
methods: ['DELETE'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
|||
const { deleteConnectorById } = queries.connectors;
|
||||
const {
|
||||
signInExperiences: { validateLanguageInfo },
|
||||
quota: { guardKey, guardTenantUsageByKey },
|
||||
quota: { guardKey, guardTenantUsageByKey, reportSubscriptionUpdatesUsage },
|
||||
} = libraries;
|
||||
const { getLogtoConnectors } = connectors;
|
||||
|
||||
|
@ -122,6 +122,8 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
|||
: rest
|
||||
);
|
||||
|
||||
await reportSubscriptionUpdatesUsage('mfaEnabled');
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,7 +11,10 @@ import { EnvSet } from '#src/env-set/index.js';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||
import koaQuotaGuard, {
|
||||
koaReportSubscriptionUpdates,
|
||||
newKoaQuotaGuard,
|
||||
} from '#src/middleware/koa-quota-guard.js';
|
||||
import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso-connector/type.js';
|
||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||
import { isSupportedSsoConnector, isSupportedSsoProvider } from '#src/sso/utils.js';
|
||||
|
@ -77,6 +80,11 @@ export default function singleSignOnConnectorsRoutes<T extends ManagementApiRout
|
|||
response: SsoConnectors.guard,
|
||||
status: [200, 400, 409, 422],
|
||||
}),
|
||||
koaReportSubscriptionUpdates({
|
||||
quota,
|
||||
key: 'enterpriseSsoLimit',
|
||||
methods: ['POST'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { body } = ctx.guard;
|
||||
const { providerName, connectorName, config, domains, ...rest } = body;
|
||||
|
@ -202,6 +210,11 @@ export default function singleSignOnConnectorsRoutes<T extends ManagementApiRout
|
|||
params: z.object({ id: z.string().min(1) }),
|
||||
status: [204, 404],
|
||||
}),
|
||||
koaReportSubscriptionUpdates({
|
||||
quota,
|
||||
key: 'enterpriseSsoLimit',
|
||||
methods: ['DELETE'],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
|
||||
|
|
|
@ -7,5 +7,6 @@ export const createMockQuotaLibrary = (): QuotaLibrary => {
|
|||
guardKey: jest.fn(),
|
||||
guardTenantUsageByKey: jest.fn(),
|
||||
guardEntityScopesUsage: jest.fn(),
|
||||
reportSubscriptionUpdatesUsage: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { trySafe } from '@silverhand/essentials';
|
||||
|
||||
import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
|
||||
|
||||
import assertThat from '../assert-that.js';
|
||||
|
@ -7,6 +9,8 @@ import {
|
|||
type SubscriptionUsage,
|
||||
type SubscriptionPlan,
|
||||
type Subscription,
|
||||
type ReportSubscriptionUpdatesUsageKey,
|
||||
allReportSubscriptionUpdatesUsageKeys,
|
||||
} from './types.js';
|
||||
|
||||
export const getTenantSubscription = async (
|
||||
|
@ -33,19 +37,21 @@ export const getTenantSubscriptionPlan = async (
|
|||
return plan;
|
||||
};
|
||||
|
||||
export const getTenantSubscriptionQuotaAndUsage = async (
|
||||
export const getTenantSubscriptionData = async (
|
||||
cloudConnection: CloudConnectionLibrary
|
||||
): Promise<{
|
||||
planId: string;
|
||||
quota: SubscriptionQuota;
|
||||
usage: SubscriptionUsage;
|
||||
}> => {
|
||||
const client = await cloudConnection.getClient();
|
||||
const [quota, usage] = await Promise.all([
|
||||
const [{ planId }, quota, usage] = await Promise.all([
|
||||
client.get('/api/tenants/my/subscription'),
|
||||
client.get('/api/tenants/my/subscription/quota'),
|
||||
client.get('/api/tenants/my/subscription/usage'),
|
||||
]);
|
||||
|
||||
return { quota, usage };
|
||||
return { planId, quota, usage };
|
||||
};
|
||||
|
||||
export const getTenantSubscriptionScopeUsage = async (
|
||||
|
@ -60,3 +66,29 @@ export const getTenantSubscriptionScopeUsage = async (
|
|||
|
||||
return scopeUsages;
|
||||
};
|
||||
|
||||
export const reportSubscriptionUpdates = async (
|
||||
cloudConnection: CloudConnectionLibrary,
|
||||
usageKey: keyof SubscriptionQuota
|
||||
): Promise<void> => {
|
||||
if (!isReportSubscriptionUpdatesUsageKey(usageKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await cloudConnection.getClient();
|
||||
// We only report to the Cloud to notify the resource usage updates, and do not care the response. We will see error logs on the Cloud side if there is any issue.
|
||||
await trySafe(
|
||||
client.post('/api/tenants/my/subscription/item-updates', {
|
||||
body: {
|
||||
usageKey,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const isReportSubscriptionUpdatesUsageKey = (
|
||||
value: string
|
||||
): value is ReportSubscriptionUpdatesUsageKey => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return allReportSubscriptionUpdatesUsageKeys.includes(value as ReportSubscriptionUpdatesUsageKey);
|
||||
};
|
||||
|
|
|
@ -3,9 +3,12 @@ import { type RouterRoutes } from '@withtyped/client';
|
|||
import { type z, type ZodType } from 'zod';
|
||||
|
||||
type GetRoutes = RouterRoutes<typeof router>['get'];
|
||||
type PostRoutes = RouterRoutes<typeof router>['post'];
|
||||
|
||||
type RouteResponseType<T extends { search?: unknown; body?: unknown; response?: ZodType }> =
|
||||
z.infer<NonNullable<T['response']>>;
|
||||
type RouteRequestBodyType<T extends { search?: unknown; body?: ZodType; response?: unknown }> =
|
||||
z.infer<NonNullable<T['body']>>;
|
||||
|
||||
export type SubscriptionPlan = RouteResponseType<GetRoutes['/api/subscription-plans']>[number];
|
||||
|
||||
|
@ -37,3 +40,18 @@ export type SubscriptionQuota = Omit<
|
|||
export type SubscriptionUsage = RouteResponseType<
|
||||
GetRoutes['/api/tenants/:tenantId/subscription/usage']
|
||||
>;
|
||||
|
||||
export type ReportSubscriptionUpdatesUsageKey = RouteRequestBodyType<
|
||||
PostRoutes['/api/tenants/my/subscription/item-updates']
|
||||
>['usageKey'];
|
||||
|
||||
// Have to manually define this variable since we can only get the literal union from the @logto/cloud/routes module.
|
||||
export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
|
||||
'tokenLimit',
|
||||
'machineToMachineLimit',
|
||||
'resourcesLimit',
|
||||
'mfaEnabled',
|
||||
'organizationsEnabled',
|
||||
'tenantMembersLimit',
|
||||
'enterpriseSsoLimit',
|
||||
]) satisfies readonly ReportSubscriptionUpdatesUsageKey[];
|
||||
|
|
|
@ -2890,8 +2890,8 @@ importers:
|
|||
version: 3.23.8
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-3b703da
|
||||
version: 0.2.5-3b703da(zod@3.23.8)
|
||||
specifier: 0.2.5-50ff8fe
|
||||
version: 0.2.5-50ff8fe(zod@3.23.8)
|
||||
'@silverhand/eslint-config':
|
||||
specifier: 6.0.1
|
||||
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
|
||||
|
@ -5022,6 +5022,10 @@ packages:
|
|||
resolution: {integrity: sha512-VCevQnxP5910s/cDYAxoJRim9iH1yN/La0HAlOP6FhVGtZofYwTTfT9AQXC+dZScgydpcFWo4k/6MYOFRtZCLg==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
||||
'@logto/cloud@0.2.5-50ff8fe':
|
||||
resolution: {integrity: sha512-EMIGnx3swILEcSvYsAlPg9E1srtPcZxHxVH+D/dTrg8ctHbRAJkFbeuQFhwHGvs1dfgULd9MKtaAkL2qckExMw==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
||||
'@logto/cloud@0.2.5-923c26f':
|
||||
resolution: {integrity: sha512-NAK9/T7HxEfE2djO6VTekMziOXH6NtbAzwumZcZo0bqIUDGiKlUvted/KY6iqpCdfFOF4aIyKp+pvlQIjj1T6Q==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
@ -14660,6 +14664,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@logto/cloud@0.2.5-50ff8fe(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.1
|
||||
'@withtyped/server': 0.13.6(zod@3.23.8)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@logto/cloud@0.2.5-923c26f(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.1
|
||||
|
|
Loading…
Reference in a new issue