Rename to limit id

This commit is contained in:
Owen
2026-06-29 15:22:35 -04:00
parent 4718c489d3
commit ff89a64453
24 changed files with 159 additions and 154 deletions
+26 -25
View File
@@ -1,4 +1,4 @@
export enum FeatureId { export enum LimitId {
USERS = "users", USERS = "users",
SITES = "sites", SITES = "sites",
EGRESS_DATA_MB = "egressDataMb", EGRESS_DATA_MB = "egressDataMb",
@@ -8,21 +8,23 @@ export enum FeatureId {
TIER1 = "tier1" TIER1 = "tier1"
} }
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> { export async function getFeatureDisplayName(
featureId: LimitId
): Promise<string> {
switch (featureId) { switch (featureId) {
case FeatureId.USERS: case LimitId.USERS:
return "Users"; return "Users";
case FeatureId.SITES: case LimitId.SITES:
return "Sites"; return "Sites";
case FeatureId.EGRESS_DATA_MB: case LimitId.EGRESS_DATA_MB:
return "Egress Data (MB)"; return "Egress Data (MB)";
case FeatureId.DOMAINS: case LimitId.DOMAINS:
return "Domains"; return "Domains";
case FeatureId.REMOTE_EXIT_NODES: case LimitId.REMOTE_EXIT_NODES:
return "Remote Exit Nodes"; return "Remote Exit Nodes";
case FeatureId.ORGINIZATIONS: case LimitId.ORGINIZATIONS:
return "Organizations"; return "Organizations";
case FeatureId.TIER1: case LimitId.TIER1:
return "Home Lab"; return "Home Lab";
default: default:
return featureId; return featureId;
@@ -30,15 +32,16 @@ export async function getFeatureDisplayName(featureId: FeatureId): Promise<strin
} }
// this is from the old system // this is from the old system
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data export const FeatureMeterIds: Partial<Record<LimitId, string>> = {
// right now we are not charging for any data
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW" // [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
}; };
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = { export const FeatureMeterIdsSandbox: Partial<Record<LimitId, string>> = {
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ" // [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
}; };
export function getFeatureMeterId(featureId: FeatureId): string | undefined { export function getFeatureMeterId(featureId: LimitId): string | undefined {
if ( if (
process.env.ENVIRONMENT == "prod" && process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true" process.env.SANDBOX_MODE !== "true"
@@ -49,22 +52,20 @@ export function getFeatureMeterId(featureId: FeatureId): string | undefined {
} }
} }
export function getFeatureIdByMetricId( export function getFeatureIdByMetricId(metricId: string): LimitId | undefined {
metricId: string return (Object.entries(FeatureMeterIds) as [LimitId, string][]).find(
): FeatureId | undefined {
return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find(
([_, v]) => v === metricId ([_, v]) => v === metricId
)?.[0]; )?.[0];
} }
export type FeaturePriceSet = Partial<Record<FeatureId, string>>; export type FeaturePriceSet = Partial<Record<LimitId, string>>;
export const tier1FeaturePriceSet: FeaturePriceSet = { export const tier1FeaturePriceSet: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G" [LimitId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
}; };
export const tier1FeaturePriceSetSandbox: FeaturePriceSet = { export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT" [LimitId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
}; };
export function getTier1FeaturePriceSet(): FeaturePriceSet { export function getTier1FeaturePriceSet(): FeaturePriceSet {
@@ -79,11 +80,11 @@ export function getTier1FeaturePriceSet(): FeaturePriceSet {
} }
export const tier2FeaturePriceSet: FeaturePriceSet = { export const tier2FeaturePriceSet: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN" [LimitId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
}; };
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = { export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR" [LimitId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
}; };
export function getTier2FeaturePriceSet(): FeaturePriceSet { export function getTier2FeaturePriceSet(): FeaturePriceSet {
@@ -98,11 +99,11 @@ export function getTier2FeaturePriceSet(): FeaturePriceSet {
} }
export const tier3FeaturePriceSet: FeaturePriceSet = { export const tier3FeaturePriceSet: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv" [LimitId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
}; };
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = { export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs" [LimitId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
}; };
export function getTier3FeaturePriceSet(): FeaturePriceSet { export function getTier3FeaturePriceSet(): FeaturePriceSet {
@@ -116,7 +117,7 @@ export function getTier3FeaturePriceSet(): FeaturePriceSet {
} }
} }
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined { export function getFeatureIdByPriceId(priceId: string): LimitId | undefined {
// Check all feature price sets // Check all feature price sets
const allPriceSets = [ const allPriceSets = [
getTier1FeaturePriceSet(), getTier1FeaturePriceSet(),
@@ -125,7 +126,7 @@ export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
]; ];
for (const priceSet of allPriceSets) { for (const priceSet of allPriceSets) {
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find( const entry = (Object.entries(priceSet) as [LimitId, string][]).find(
([_, price]) => price === priceId ([_, price]) => price === priceId
); );
if (entry) { if (entry) {
+5 -5
View File
@@ -1,19 +1,19 @@
import Stripe from "stripe"; import Stripe from "stripe";
import { FeatureId, FeaturePriceSet } from "./features"; import { LimitId, FeaturePriceSet } from "./features";
import { usageService } from "./usageService"; import { usageService } from "./usageService";
export async function getLineItems( export async function getLineItems(
featurePriceSet: FeaturePriceSet, featurePriceSet: FeaturePriceSet,
orgId: string, orgId: string
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> { ): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
const users = await usageService.getUsage(orgId, FeatureId.USERS); const users = await usageService.getUsage(orgId, LimitId.USERS);
return Object.entries(featurePriceSet).map(([featureId, priceId]) => { return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
let quantity: number | undefined; let quantity: number | undefined;
if (featureId === FeatureId.USERS) { if (featureId === LimitId.USERS) {
quantity = users?.instantaneousValue || 1; quantity = users?.instantaneousValue || 1;
} else if (featureId === FeatureId.TIER1) { } else if (featureId === LimitId.TIER1) {
quantity = 1; quantity = 1;
} }
+23 -23
View File
@@ -1,70 +1,70 @@
import { FeatureId } from "./features"; import { LimitId } from "./features";
export type LimitSet = Partial<{ export type LimitSet = Partial<{
[key in FeatureId]: { [key in LimitId]: {
value: number | null; // null indicates no limit value: number | null; // null indicates no limit
description?: string; description?: string;
}; };
}>; }>;
export const freeLimitSet: LimitSet = { export const freeLimitSet: LimitSet = {
[FeatureId.SITES]: { value: 5, description: "Basic limit" }, [LimitId.SITES]: { value: 5, description: "Basic limit" },
[FeatureId.USERS]: { value: 5, description: "Basic limit" }, [LimitId.USERS]: { value: 5, description: "Basic limit" },
[FeatureId.DOMAINS]: { value: 5, description: "Basic limit" }, [LimitId.DOMAINS]: { value: 5, description: "Basic limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" }, [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
[FeatureId.ORGINIZATIONS]: { value: 1, description: "Basic limit" }, [LimitId.ORGINIZATIONS]: { value: 1, description: "Basic limit" }
}; };
export const tier1LimitSet: LimitSet = { export const tier1LimitSet: LimitSet = {
[FeatureId.USERS]: { value: 7, description: "Home limit" }, [LimitId.USERS]: { value: 7, description: "Home limit" },
[FeatureId.SITES]: { value: 10, description: "Home limit" }, [LimitId.SITES]: { value: 10, description: "Home limit" },
[FeatureId.DOMAINS]: { value: 10, description: "Home limit" }, [LimitId.DOMAINS]: { value: 10, description: "Home limit" },
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" }, [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
[FeatureId.ORGINIZATIONS]: { value: 1, description: "Home limit" }, [LimitId.ORGINIZATIONS]: { value: 1, description: "Home limit" }
}; };
export const tier2LimitSet: LimitSet = { export const tier2LimitSet: LimitSet = {
[FeatureId.USERS]: { [LimitId.USERS]: {
value: 50, value: 50,
description: "Team limit" description: "Team limit"
}, },
[FeatureId.SITES]: { [LimitId.SITES]: {
value: 50, value: 50,
description: "Team limit" description: "Team limit"
}, },
[FeatureId.DOMAINS]: { [LimitId.DOMAINS]: {
value: 50, value: 50,
description: "Team limit" description: "Team limit"
}, },
[FeatureId.REMOTE_EXIT_NODES]: { [LimitId.REMOTE_EXIT_NODES]: {
value: 3, value: 3,
description: "Team limit" description: "Team limit"
}, },
[FeatureId.ORGINIZATIONS]: { [LimitId.ORGINIZATIONS]: {
value: 1, value: 1,
description: "Team limit" description: "Team limit"
} }
}; };
export const tier3LimitSet: LimitSet = { export const tier3LimitSet: LimitSet = {
[FeatureId.USERS]: { [LimitId.USERS]: {
value: 250, value: 250,
description: "Business limit" description: "Business limit"
}, },
[FeatureId.SITES]: { [LimitId.SITES]: {
value: 250, value: 250,
description: "Business limit" description: "Business limit"
}, },
[FeatureId.DOMAINS]: { [LimitId.DOMAINS]: {
value: 100, value: 100,
description: "Business limit" description: "Business limit"
}, },
[FeatureId.REMOTE_EXIT_NODES]: { [LimitId.REMOTE_EXIT_NODES]: {
value: 20, value: 20,
description: "Business limit" description: "Business limit"
}, },
[FeatureId.ORGINIZATIONS]: { [LimitId.ORGINIZATIONS]: {
value: 5, value: 5,
description: "Business limit" description: "Business limit"
}, }
}; };
+2 -2
View File
@@ -1,7 +1,7 @@
import { db, limits } from "@server/db"; import { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet"; import { LimitSet } from "./limitSet";
import { FeatureId } from "./features"; import { LimitId } from "./features";
import logger from "@server/logger"; import logger from "@server/logger";
class LimitService { class LimitService {
@@ -38,7 +38,7 @@ class LimitService {
async getOrgLimit( async getOrgLimit(
orgId: string, orgId: string,
featureId: FeatureId featureId: LimitId
): Promise<number | null> { ): Promise<number | null> {
const limitId = `${orgId}-${featureId}`; const limitId = `${orgId}-${featureId}`;
const [limit] = await db const [limit] = await db
+8 -8
View File
@@ -9,7 +9,7 @@ import {
Transaction, Transaction,
orgs orgs
} from "@server/db"; } from "@server/db";
import { FeatureId, getFeatureMeterId } from "./features"; import { LimitId, getFeatureMeterId } from "./features";
import logger from "@server/logger"; import logger from "@server/logger";
import { build } from "@server/build"; import { build } from "@server/build";
import { regionalCache as cache } from "#dynamic/lib/cache"; import { regionalCache as cache } from "#dynamic/lib/cache";
@@ -37,7 +37,7 @@ export class UsageService {
public async add( public async add(
orgId: string, orgId: string,
featureId: FeatureId, featureId: LimitId,
value: number, value: number,
transaction: any = null transaction: any = null
): Promise<Usage | null> { ): Promise<Usage | null> {
@@ -114,7 +114,7 @@ export class UsageService {
private async internalAddUsage( private async internalAddUsage(
orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount
featureId: FeatureId, featureId: LimitId,
value: number, value: number,
trx: Transaction trx: Transaction
): Promise<Usage> { ): Promise<Usage> {
@@ -163,7 +163,7 @@ export class UsageService {
async updateCount( async updateCount(
orgId: string, orgId: string,
featureId: FeatureId, featureId: LimitId,
value?: number, value?: number,
customerId?: string customerId?: string
): Promise<void> { ): Promise<void> {
@@ -227,7 +227,7 @@ export class UsageService {
private async getCustomerId( private async getCustomerId(
orgId: string, orgId: string,
featureId: FeatureId featureId: LimitId
): Promise<string | null> { ): Promise<string | null> {
const orgIdToUse = await this.getBillingOrg(orgId); const orgIdToUse = await this.getBillingOrg(orgId);
@@ -269,7 +269,7 @@ export class UsageService {
public async getUsage( public async getUsage(
orgId: string, orgId: string,
featureId: FeatureId, featureId: LimitId,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<Usage | null> { ): Promise<Usage | null> {
if (noop()) { if (noop()) {
@@ -376,7 +376,7 @@ export class UsageService {
public async checkLimitSet( public async checkLimitSet(
orgId: string, orgId: string,
featureId?: FeatureId, featureId?: LimitId,
usage?: Usage, usage?: Usage,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<boolean> { ): Promise<boolean> {
@@ -424,7 +424,7 @@ export class UsageService {
} else { } else {
currentUsage = await this.getUsage( currentUsage = await this.getUsage(
orgIdToUse, orgIdToUse,
limit.featureId as FeatureId, limit.featureId as LimitId,
trx trx
); );
} }
+9 -7
View File
@@ -24,7 +24,7 @@ import { deletePeer } from "@server/routers/gerbil/peers";
import { OlmErrorCodes } from "@server/routers/olm/error"; import { OlmErrorCodes } from "@server/routers/olm/error";
import { sendTerminateClient } from "@server/routers/client/terminate"; import { sendTerminateClient } from "@server/routers/client/terminate";
import { usageService } from "./billing/usageService"; import { usageService } from "./billing/usageService";
import { FeatureId } from "./billing"; import { LimitId } from "./billing";
export type DeleteOrgByIdResult = { export type DeleteOrgByIdResult = {
deletedNewtIds: string[]; deletedNewtIds: string[];
@@ -140,7 +140,9 @@ export async function deleteOrgById(
.select({ count: count() }) .select({ count: count() })
.from(orgDomains) .from(orgDomains)
.where(eq(orgDomains.domainId, domainId)); .where(eq(orgDomains.domainId, domainId));
logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`); logger.info(
`Found ${orgCount.count} orgs using domain ${domainId}`
);
if (orgCount.count === 1) { if (orgCount.count === 1) {
domainIdsToDelete.push(domainId); domainIdsToDelete.push(domainId);
} }
@@ -152,7 +154,7 @@ export async function deleteOrgById(
.where(inArray(domains.domainId, domainIdsToDelete)); .where(inArray(domains.domainId, domainIdsToDelete));
} }
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here await usageService.add(orgId, LimitId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
await trx.delete(orgs).where(eq(orgs.orgId, orgId)); await trx.delete(orgs).where(eq(orgs.orgId, orgId));
@@ -199,22 +201,22 @@ export async function deleteOrgById(
if (org.billingOrgId) { if (org.billingOrgId) {
usageService.updateCount( usageService.updateCount(
org.billingOrgId, org.billingOrgId,
FeatureId.DOMAINS, LimitId.DOMAINS,
domainCount ?? 0 domainCount ?? 0
); );
usageService.updateCount( usageService.updateCount(
org.billingOrgId, org.billingOrgId,
FeatureId.SITES, LimitId.SITES,
siteCount ?? 0 siteCount ?? 0
); );
usageService.updateCount( usageService.updateCount(
org.billingOrgId, org.billingOrgId,
FeatureId.USERS, LimitId.USERS,
userCount ?? 0 userCount ?? 0
); );
usageService.updateCount( usageService.updateCount(
org.billingOrgId, org.billingOrgId,
FeatureId.REMOTE_EXIT_NODES, LimitId.REMOTE_EXIT_NODES,
remoteExitNodeCount ?? 0 remoteExitNodeCount ?? 0
); );
} }
+3 -3
View File
@@ -14,7 +14,7 @@ import {
} from "@server/db"; } from "@server/db";
import { eq, and, inArray, ne, exists } from "drizzle-orm"; import { eq, and, inArray, ne, exists } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
export async function assignUserToOrg( export async function assignUserToOrg(
org: Org, org: Org,
@@ -61,7 +61,7 @@ export async function assignUserToOrg(
); );
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
await usageService.add(org.orgId, FeatureId.USERS, 1, trx); await usageService.add(org.orgId, LimitId.USERS, 1, trx);
} }
} }
} }
@@ -157,7 +157,7 @@ export async function removeUserFromOrg(
); );
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
await usageService.add(org.orgId, FeatureId.USERS, -1, trx); await usageService.add(org.orgId, LimitId.USERS, -1, trx);
} }
} }
} }
+2 -2
View File
@@ -25,7 +25,7 @@ import {
getTier1FeaturePriceSet, getTier1FeaturePriceSet,
getTier3FeaturePriceSet, getTier3FeaturePriceSet,
getTier2FeaturePriceSet, getTier2FeaturePriceSet,
FeatureId, LimitId,
type FeaturePriceSet type FeaturePriceSet
} from "@server/lib/billing"; } from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems"; import { getLineItems } from "@server/lib/billing/getLineItems";
@@ -214,7 +214,7 @@ export async function changeTier(
} }
// Map to the corresponding feature in the new tier // Map to the corresponding feature in the new tier
const newPriceId = targetPriceSet[FeatureId.USERS]; const newPriceId = targetPriceSet[LimitId.USERS];
if (newPriceId) { if (newPriceId) {
return { return {
@@ -24,7 +24,7 @@ import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { Limit, limits, Usage, usage } from "@server/db"; import { Limit, limits, Usage, usage } from "@server/db";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { GetOrgUsageResponse } from "@server/routers/billing/types"; import { GetOrgUsageResponse } from "@server/routers/billing/types";
const getOrgSchema = z.strictObject({ const getOrgSchema = z.strictObject({
@@ -93,16 +93,16 @@ export async function getOrgUsage(
// Get usage for org // Get usage for org
const usageData = []; const usageData = [];
const sites = await usageService.getUsage(orgId, FeatureId.SITES); const sites = await usageService.getUsage(orgId, LimitId.SITES);
const users = await usageService.getUsage(orgId, FeatureId.USERS); const users = await usageService.getUsage(orgId, LimitId.USERS);
const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS); const domains = await usageService.getUsage(orgId, LimitId.DOMAINS);
const remoteExitNodes = await usageService.getUsage( const remoteExitNodes = await usageService.getUsage(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES LimitId.REMOTE_EXIT_NODES
); );
const organizations = await usageService.getUsage( const organizations = await usageService.getUsage(
orgId, orgId,
FeatureId.ORGINIZATIONS LimitId.ORGINIZATIONS
); );
// const egressData = await usageService.getUsage( // const egressData = await usageService.getUsage(
// orgId, // orgId,
@@ -35,7 +35,7 @@ import logger from "@server/logger";
import { and, eq, inArray, ne } from "drizzle-orm"; import { and, eq, inArray, ne } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes"; import { getNextAvailableSubnet } from "@server/lib/exitNodes";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export const paramsSchema = z.object({ export const paramsSchema = z.object({
@@ -79,7 +79,10 @@ export async function createRemoteExitNode(
const { remoteExitNodeId, secret } = parsedBody.data; const { remoteExitNodeId, secret } = parsedBody.data;
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) { if (
req.user &&
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
) {
return next( return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role") createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
); );
@@ -87,13 +90,13 @@ export async function createRemoteExitNode(
const usage = await usageService.getUsage( const usage = await usageService.getUsage(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES LimitId.REMOTE_EXIT_NODES
); );
if (usage) { if (usage) {
const rejectRemoteExitNodes = await usageService.checkLimitSet( const rejectRemoteExitNodes = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES, LimitId.REMOTE_EXIT_NODES,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -264,7 +267,7 @@ export async function createRemoteExitNode(
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) { if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add( await usageService.add(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES, LimitId.REMOTE_EXIT_NODES,
1, 1,
trx trx
); );
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().min(1), orgId: z.string().min(1),
@@ -117,7 +117,7 @@ export async function deleteRemoteExitNode(
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) { if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add( await usageService.add(
orgId, orgId,
FeatureId.REMOTE_EXIT_NODES, LimitId.REMOTE_EXIT_NODES,
-1, -1,
trx trx
); );
+2 -2
View File
@@ -20,7 +20,7 @@ import { getOrgTierData } from "#dynamic/lib/billing";
import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg"; import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
const deleteMyAccountBody = z.strictObject({ const deleteMyAccountBody = z.strictObject({
password: z.string().optional(), password: z.string().optional(),
@@ -220,7 +220,7 @@ export async function deleteMyAccount(
await trx.delete(users).where(eq(users.userId, userId)); await trx.delete(users).where(eq(users.userId, userId));
// loop through the other orgs and decrement the count // loop through the other orgs and decrement the count
for (const userOrg of otherOrgsTheUserWasIn) { for (const userOrg of otherOrgsTheUserWasIn) {
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx); await usageService.add(userOrg.orgId, LimitId.USERS, -1, trx);
} }
}); });
+4 -4
View File
@@ -17,7 +17,7 @@ import { subdomainSchema } from "@server/lib/schemas";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators";
import { build } from "@server/build"; import { build } from "@server/build";
import config from "@server/lib/config"; import config from "@server/lib/config";
@@ -120,7 +120,7 @@ export async function createOrgDomain(
} }
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.DOMAINS); const usage = await usageService.getUsage(orgId, LimitId.DOMAINS);
if (!usage) { if (!usage) {
return next( return next(
createHttpError( createHttpError(
@@ -132,7 +132,7 @@ export async function createOrgDomain(
const rejectDomains = await usageService.checkLimitSet( const rejectDomains = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.DOMAINS, LimitId.DOMAINS,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -346,7 +346,7 @@ export async function createOrgDomain(
await trx.insert(dnsRecords).values(recordsToInsert); await trx.insert(dnsRecords).values(recordsToInsert);
} }
await usageService.add(orgId, FeatureId.DOMAINS, 1, trx); await usageService.add(orgId, LimitId.DOMAINS, 1, trx);
}); });
if (!returned) { if (!returned) {
+2 -2
View File
@@ -8,7 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
domainId: z.string(), domainId: z.string(),
@@ -77,7 +77,7 @@ export async function deleteAccountDomain(
await trx.delete(domains).where(eq(domains.domainId, domainId)); await trx.delete(domains).where(eq(domains.domainId, domainId));
await usageService.add(orgId, FeatureId.DOMAINS, -1, trx); await usageService.add(orgId, LimitId.DOMAINS, -1, trx);
}); });
return response<DeleteAccountDomainResponse>(res, { return response<DeleteAccountDomainResponse>(res, {
+7 -9
View File
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing/features"; import { LimitId } from "@server/lib/billing/features";
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -171,8 +171,9 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
} }
// PostgreSQL: batch UPDATE … FROM (VALUES …) - single round-trip per chunk. // PostgreSQL: batch UPDATE … FROM (VALUES …) - single round-trip per chunk.
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) => const valuesList = chunk.map(
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)` ([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
); );
const valuesClause = sql.join(valuesList, sql`, `); const valuesClause = sql.join(valuesList, sql`, `);
return dbQueryRows<{ orgId: string; pubKey: string }>(sql` return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
@@ -228,7 +229,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
const totalBandwidth = orgUsageMap.get(orgId)!; const totalBandwidth = orgUsageMap.get(orgId)!;
const bandwidthUsage = await usageService.add( const bandwidthUsage = await usageService.add(
orgId, orgId,
FeatureId.EGRESS_DATA_MB, LimitId.EGRESS_DATA_MB,
totalBandwidth totalBandwidth
); );
if (bandwidthUsage) { if (bandwidthUsage) {
@@ -236,7 +237,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
usageService usageService
.checkLimitSet( .checkLimitSet(
orgId, orgId,
FeatureId.EGRESS_DATA_MB, LimitId.EGRESS_DATA_MB,
bandwidthUsage bandwidthUsage
) )
.catch((error: any) => { .catch((error: any) => {
@@ -247,10 +248,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
}); });
} }
} catch (error) { } catch (error) {
logger.error( logger.error(`Error processing usage for org ${orgId}:`, error);
`Error processing usage for org ${orgId}:`,
error
);
// Continue with other orgs. // Continue with other orgs.
} }
} }
+2 -2
View File
@@ -31,7 +31,7 @@ import {
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build"; import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
@@ -645,7 +645,7 @@ export async function validateOidcCallback(
for (const orgCount of orgUserCounts) { for (const orgCount of orgUserCounts) {
await usageService.updateCount( await usageService.updateCount(
orgCount.orgId, orgCount.orgId,
FeatureId.USERS, LimitId.USERS,
orgCount.userCount orgCount.userCount
); );
} }
+4 -4
View File
@@ -25,7 +25,7 @@ import { getUniqueSiteName } from "@server/db/names";
import moment from "moment"; import moment from "moment";
import { build } from "@server/build"; import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { INSPECT_MAX_BYTES } from "buffer"; import { INSPECT_MAX_BYTES } from "buffer";
import { getNextAvailableClientSubnet } from "@server/lib/ip"; import { getNextAvailableClientSubnet } from "@server/lib/ip";
@@ -169,7 +169,7 @@ export async function registerNewt(
// SaaS billing check // SaaS billing check
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.SITES); const usage = await usageService.getUsage(orgId, LimitId.SITES);
if (!usage) { if (!usage) {
return next( return next(
createHttpError( createHttpError(
@@ -180,7 +180,7 @@ export async function registerNewt(
} }
const rejectSites = await usageService.checkLimitSet( const rejectSites = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.SITES, LimitId.SITES,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -274,7 +274,7 @@ export async function registerNewt(
) )
); );
await usageService.add(orgId, FeatureId.SITES, 1, trx); await usageService.add(orgId, LimitId.SITES, 1, trx);
}); });
} finally { } finally {
await releaseSubnetLock(); await releaseSubnetLock();
+5 -5
View File
@@ -27,7 +27,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { isValidCIDR } from "@server/lib/validators"; import { isValidCIDR } from "@server/lib/validators";
import { createCustomer } from "#dynamic/lib/billing"; import { createCustomer } from "#dynamic/lib/billing";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId, limitsService, freeLimitSet } from "@server/lib/billing"; import { LimitId, limitsService, freeLimitSet } from "@server/lib/billing";
import { build } from "@server/build"; import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { doCidrsOverlap } from "@server/lib/ip"; import { doCidrsOverlap } from "@server/lib/ip";
@@ -202,7 +202,7 @@ export async function createOrg(
if (build == "saas" && billingOrgIdForNewOrg) { if (build == "saas" && billingOrgIdForNewOrg) {
const usage = await usageService.getUsage( const usage = await usageService.getUsage(
billingOrgIdForNewOrg, billingOrgIdForNewOrg,
FeatureId.ORGINIZATIONS LimitId.ORGINIZATIONS
); );
if (!usage) { if (!usage) {
return next( return next(
@@ -214,7 +214,7 @@ export async function createOrg(
} }
const rejectOrgs = await usageService.checkLimitSet( const rejectOrgs = await usageService.checkLimitSet(
billingOrgIdForNewOrg, billingOrgIdForNewOrg,
FeatureId.ORGINIZATIONS, LimitId.ORGINIZATIONS,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -421,7 +421,7 @@ export async function createOrg(
if (customerId) { if (customerId) {
await usageService.updateCount( await usageService.updateCount(
orgId, orgId,
FeatureId.USERS, LimitId.USERS,
1, 1,
customerId customerId
); // Only 1 because we are creating the org ); // Only 1 because we are creating the org
@@ -431,7 +431,7 @@ export async function createOrg(
if (numOrgs) { if (numOrgs) {
usageService.updateCount( usageService.updateCount(
billingOrgIdForNewOrg || orgId, billingOrgIdForNewOrg || orgId,
FeatureId.ORGINIZATIONS, LimitId.ORGINIZATIONS,
numOrgs numOrgs
); );
} }
+4 -4
View File
@@ -19,7 +19,7 @@ import { getNextAvailableClientSubnet, isIpInCidr } from "@server/lib/ip";
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
const createSiteParamsSchema = z.strictObject({ const createSiteParamsSchema = z.strictObject({
@@ -160,7 +160,7 @@ export async function createSite(
} }
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.SITES); const usage = await usageService.getUsage(orgId, LimitId.SITES);
if (!usage) { if (!usage) {
return next( return next(
createHttpError( createHttpError(
@@ -172,7 +172,7 @@ export async function createSite(
const rejectSites = await usageService.checkLimitSet( const rejectSites = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.SITES, LimitId.SITES,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -519,7 +519,7 @@ export async function createSite(
}); });
} }
await usageService.add(orgId, FeatureId.SITES, 1, trx); await usageService.add(orgId, LimitId.SITES, 1, trx);
}); });
} finally { } finally {
await releaseSubnetLock?.(); await releaseSubnetLock?.();
+2 -2
View File
@@ -13,7 +13,7 @@ import { sendToClient } from "#dynamic/routers/ws";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations"; import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import { import {
deleteAssociatedResourcesForSite, deleteAssociatedResourcesForSite,
@@ -177,7 +177,7 @@ export async function deleteSite(
} }
await trx.delete(sites).where(eq(sites.siteId, siteId)); await trx.delete(sites).where(eq(sites.siteId, siteId));
await usageService.add(site.orgId, FeatureId.SITES, -1, trx); await usageService.add(site.orgId, LimitId.SITES, -1, trx);
}); });
if (deleteResources) { if (deleteResources) {
+3 -3
View File
@@ -17,7 +17,7 @@ import { fromError } from "zod-validation-error";
import { checkValidInvite } from "@server/auth/checkValidInvite"; import { checkValidInvite } from "@server/auth/checkValidInvite";
import { verifySession } from "@server/auth/sessions/verifySession"; import { verifySession } from "@server/auth/sessions/verifySession";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { build } from "@server/build"; import { build } from "@server/build";
import { assignUserToOrg } from "@server/lib/userOrg"; import { assignUserToOrg } from "@server/lib/userOrg";
@@ -104,7 +104,7 @@ export async function acceptInvite(
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage( const usage = await usageService.getUsage(
existingInvite.orgId, existingInvite.orgId,
FeatureId.USERS LimitId.USERS
); );
if (!usage) { if (!usage) {
return next( return next(
@@ -117,7 +117,7 @@ export async function acceptInvite(
const rejectUsers = await usageService.checkLimitSet( const rejectUsers = await usageService.checkLimitSet(
existingInvite.orgId, existingInvite.orgId,
FeatureId.USERS, LimitId.USERS,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
+3 -3
View File
@@ -12,7 +12,7 @@ import { and, eq, inArray } from "drizzle-orm";
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db"; import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { build } from "@server/build"; import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { isSubscribed } from "#dynamic/lib/isSubscribed";
@@ -123,7 +123,7 @@ export async function createOrgUser(
} = parsedBody.data; } = parsedBody.data;
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.USERS); const usage = await usageService.getUsage(orgId, LimitId.USERS);
if (!usage) { if (!usage) {
return next( return next(
createHttpError( createHttpError(
@@ -135,7 +135,7 @@ export async function createOrgUser(
const rejectUsers = await usageService.checkLimitSet( const rejectUsers = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.USERS, LimitId.USERS,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
+6 -5
View File
@@ -25,7 +25,7 @@ import SendInviteLink from "@server/emails/templates/SendInviteLink";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing"; import { LimitId } from "@server/lib/billing";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { build } from "@server/build"; import { build } from "@server/build";
import cache from "#dynamic/lib/cache"; import cache from "#dynamic/lib/cache";
@@ -73,7 +73,6 @@ const InviteUserResponseDataSchema = z.object({
expiresAt: z.number() expiresAt: z.number()
}); });
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/create-invite", path: "/org/{orgId}/create-invite",
@@ -94,7 +93,9 @@ registry.registerPath({
description: "Successful response", description: "Successful response",
content: { content: {
"application/json": { "application/json": {
schema: createApiResponseSchema(InviteUserResponseDataSchema) schema: createApiResponseSchema(
InviteUserResponseDataSchema
)
} }
} }
} }
@@ -181,7 +182,7 @@ export async function inviteUser(
} }
if (build == "saas") { if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.USERS); const usage = await usageService.getUsage(orgId, LimitId.USERS);
if (!usage) { if (!usage) {
return next( return next(
createHttpError( createHttpError(
@@ -192,7 +193,7 @@ export async function inviteUser(
} }
const rejectUsers = await usageService.checkLimitSet( const rejectUsers = await usageService.checkLimitSet(
orgId, orgId,
FeatureId.USERS, LimitId.USERS,
{ {
...usage, ...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1 instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -58,7 +58,7 @@ import {
tier2LimitSet, tier2LimitSet,
tier3LimitSet tier3LimitSet
} from "@server/lib/billing/limitSet"; } from "@server/lib/billing/limitSet";
import { FeatureId } from "@server/lib/billing/features"; import { LimitId } from "@server/lib/billing/features";
import TrialBillingBanner from "@app/components/TrialBillingBanner"; import TrialBillingBanner from "@app/components/TrialBillingBanner";
// Plan tier definitions matching the mockup // Plan tier definitions matching the mockup
@@ -161,32 +161,32 @@ const tierLimits: Record<
} }
> = { > = {
basic: { basic: {
users: freeLimitSet[FeatureId.USERS]?.value ?? 0, users: freeLimitSet[LimitId.USERS]?.value ?? 0,
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0, sites: freeLimitSet[LimitId.SITES]?.value ?? 0,
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0, domains: freeLimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: freeLimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, remoteNodes: freeLimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: freeLimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 organizations: freeLimitSet[LimitId.ORGINIZATIONS]?.value ?? 0
}, },
tier1: { tier1: {
users: tier1LimitSet[FeatureId.USERS]?.value ?? 0, users: tier1LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier1LimitSet[FeatureId.SITES]?.value ?? 0, sites: tier1LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier1LimitSet[FeatureId.DOMAINS]?.value ?? 0, domains: tier1LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier1LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, remoteNodes: tier1LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier1LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 organizations: tier1LimitSet[LimitId.ORGINIZATIONS]?.value ?? 0
}, },
tier2: { tier2: {
users: tier2LimitSet[FeatureId.USERS]?.value ?? 0, users: tier2LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier2LimitSet[FeatureId.SITES]?.value ?? 0, sites: tier2LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier2LimitSet[FeatureId.DOMAINS]?.value ?? 0, domains: tier2LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier2LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, remoteNodes: tier2LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier2LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 organizations: tier2LimitSet[LimitId.ORGINIZATIONS]?.value ?? 0
}, },
tier3: { tier3: {
users: tier3LimitSet[FeatureId.USERS]?.value ?? 0, users: tier3LimitSet[LimitId.USERS]?.value ?? 0,
sites: tier3LimitSet[FeatureId.SITES]?.value ?? 0, sites: tier3LimitSet[LimitId.SITES]?.value ?? 0,
domains: tier3LimitSet[FeatureId.DOMAINS]?.value ?? 0, domains: tier3LimitSet[LimitId.DOMAINS]?.value ?? 0,
remoteNodes: tier3LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, remoteNodes: tier3LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0,
organizations: tier3LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 organizations: tier3LimitSet[LimitId.ORGINIZATIONS]?.value ?? 0
}, },
enterprise: { enterprise: {
users: 0, // Custom for enterprise users: 0, // Custom for enterprise