Skip to content

Commit 26dce02

Browse files
remove the concept of a plan and just rely on entitlements at the app layer
1 parent 949634f commit 26dce02

File tree

12 files changed

+36
-104
lines changed

12 files changed

+36
-104
lines changed

packages/backend/src/ee/syncSearchContexts.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import micromatch from "micromatch";
22
import { createLogger } from "@sourcebot/shared";
33
import { PrismaClient } from "@sourcebot/db";
44
import { repoMetadataSchema, SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared";
5-
import { getPlan, hasEntitlement } from "../entitlements.js";
5+
import { hasEntitlement } from "../entitlements.js";
66
import { SearchContext } from "@sourcebot/schemas/v3/index.type";
77

88
const logger = createLogger('sync-search-contexts');
@@ -18,8 +18,7 @@ export const syncSearchContexts = async (params: SyncSearchContextsParams) => {
1818

1919
if (!await hasEntitlement("search-contexts")) {
2020
if (contexts) {
21-
const plan = await getPlan();
22-
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
21+
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
2322
}
2423
return false;
2524
}

packages/backend/src/entitlements.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {
22
Entitlement,
3-
Plan,
4-
getPlan as _getPlan,
53
getSeats as _getSeats,
64
hasEntitlement as _hasEntitlement,
75
getEntitlements as _getEntitlements,
@@ -15,11 +13,6 @@ const getLicense = async () => {
1513
});
1614
}
1715

18-
export const getPlan = async (): Promise<Plan> => {
19-
const license = await getLicense();
20-
return _getPlan(license);
21-
}
22-
2316
export const getSeats = async (): Promise<number> => {
2417
const license = await getLicense();
2518
return _getSeats(license);

packages/db/prisma/migrations/20260412062214_add_license_table/migration.sql renamed to packages/db/prisma/migrations/20260417011834_add_license_table/migration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CREATE TABLE "License" (
33
"id" TEXT NOT NULL,
44
"orgId" INTEGER NOT NULL,
55
"activationCode" TEXT NOT NULL,
6-
"plan" TEXT,
6+
"entitlements" TEXT[],
77
"seats" INTEGER,
88
"status" TEXT,
99
"lastSyncAt" TIMESTAMP(3),

packages/db/prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ model License {
300300
orgId Int @unique
301301
org Org @relation(fields: [orgId], references: [id])
302302
activationCode String
303-
plan String?
303+
entitlements String[]
304304
seats Int?
305305
status String?
306306
lastSyncAt DateTime?

packages/shared/src/entitlements.ts

Lines changed: 18 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ const eeLicenseKeyPayloadSchema = z.object({
2020

2121
type LicenseKeyPayload = z.infer<typeof eeLicenseKeyPayloadSchema>;
2222

23-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
24-
const planLabels = {
25-
oss: "OSS",
26-
"self-hosted:enterprise": "Enterprise (Self-Hosted)",
27-
"self-hosted:enterprise-unlimited": "Enterprise (Self-Hosted) Unlimited",
28-
} as const;
29-
export type Plan = keyof typeof planLabels;
30-
3123
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3224
const entitlements = [
3325
"search-contexts",
@@ -44,45 +36,10 @@ const entitlements = [
4436
] as const;
4537
export type Entitlement = (typeof entitlements)[number];
4638

47-
const entitlementsByPlan: Record<Plan, Entitlement[]> = {
48-
oss: [
49-
"anonymous-access",
50-
],
51-
"self-hosted:enterprise": [
52-
"search-contexts",
53-
"sso",
54-
"code-nav",
55-
"audit",
56-
"analytics",
57-
"permission-syncing",
58-
"github-app",
59-
"chat-sharing",
60-
"org-management",
61-
"oauth",
62-
],
63-
"self-hosted:enterprise-unlimited": [
64-
"anonymous-access",
65-
"search-contexts",
66-
"sso",
67-
"code-nav",
68-
"audit",
69-
"analytics",
70-
"permission-syncing",
71-
"github-app",
72-
"chat-sharing",
73-
"org-management",
74-
"oauth",
75-
],
76-
} as const;
77-
78-
const isValidPlan = (plan: string): plan is Plan => {
79-
return plan in entitlementsByPlan;
80-
}
81-
8239
const ACTIVE_LICENSE_STATUSES = ['active', 'trialing', 'past_due'] as const;
8340

84-
const isLicenseActive = (license: License | null): boolean => {
85-
if (!license?.status) {
41+
const isLicenseActive = (license: License): boolean => {
42+
if (!license.status) {
8643
return false;
8744
}
8845
return ACTIVE_LICENSE_STATUSES.includes(license.status as typeof ACTIVE_LICENSE_STATUSES[number]);
@@ -122,25 +79,6 @@ export const getOfflineLicenseKey = (): LicenseKeyPayload | null => {
12279
return null;
12380
}
12481

125-
export const getPlan = (license: License | null): Plan => {
126-
const licenseKey = getOfflineLicenseKey();
127-
if (licenseKey) {
128-
const expiryDate = new Date(licenseKey.expiryDate);
129-
if (expiryDate.getTime() < new Date().getTime()) {
130-
logger.error(`The provided license key has expired (${expiryDate.toLocaleString()}). Please contact ${SOURCEBOT_SUPPORT_EMAIL} for support.`);
131-
process.exit(1);
132-
}
133-
134-
return licenseKey.seats === SOURCEBOT_UNLIMITED_SEATS ? "self-hosted:enterprise-unlimited" : "self-hosted:enterprise";
135-
}
136-
else if (license?.plan && isValidPlan(license.plan) && isLicenseActive(license)) {
137-
return license.plan;
138-
}
139-
else {
140-
return "oss";
141-
}
142-
}
143-
14482
export const getSeats = (license: License | null): number => {
14583
const licenseKey = getOfflineLicenseKey();
14684
if (licenseKey) {
@@ -160,6 +98,20 @@ export const hasEntitlement = (entitlement: Entitlement, license: License | null
16098
}
16199

162100
export const getEntitlements = (license: License | null): Entitlement[] => {
163-
const plan = getPlan(license);
164-
return entitlementsByPlan[plan];
101+
const licenseKey = getOfflineLicenseKey();
102+
if (licenseKey) {
103+
const expiryDate = new Date(licenseKey.expiryDate);
104+
if (expiryDate.getTime() < new Date().getTime()) {
105+
logger.error(`The provided license key has expired (${expiryDate.toLocaleString()}). Please contact ${SOURCEBOT_SUPPORT_EMAIL} for support.`);
106+
process.exit(1);
107+
}
108+
109+
return entitlements as unknown as Entitlement[];
110+
}
111+
else if (license && isLicenseActive(license)) {
112+
return license.entitlements as unknown as Entitlement[];
113+
}
114+
else {
115+
return [];
116+
}
165117
}

packages/shared/src/index.server.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
export {
22
hasEntitlement,
33
getOfflineLicenseKey,
4-
getPlan,
54
getSeats,
65
getEntitlements,
76
} from "./entitlements.js";
87
export type {
9-
Plan,
108
Entitlement,
119
} from "./entitlements.js";
1210
export type {

packages/web/src/actions.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createLogger } from "@sourcebot/shared";
1414
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
1515
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
1616
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
17-
import { getPlan, hasEntitlement } from "@/lib/entitlements";
17+
import { hasEntitlement } from "@/lib/entitlements";
1818
import { StatusCodes } from "http-status-codes";
1919
import { cookies } from "next/headers";
2020
import { createTransport } from "nodemailer";
@@ -1200,8 +1200,7 @@ export const setAnonymousAccessStatus = async (enabled: boolean): Promise<Servic
12001200
return await withMinimumOrgRole(role, OrgRole.OWNER, async () => {
12011201
const hasAnonymousAccessEntitlement = await hasEntitlement("anonymous-access");
12021202
if (!hasAnonymousAccessEntitlement) {
1203-
const plan = await getPlan();
1204-
console.error(`Anonymous access isn't supported in your current plan: ${plan}. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
1203+
console.error(`Anonymous access isn't supported in your current plan. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
12051204
return {
12061205
statusCode: StatusCodes.FORBIDDEN,
12071206
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,

packages/web/src/app/(app)/settings/license/page.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,25 @@ import { OrgRole } from "@sourcebot/db";
33
import { ActivationCodeCard } from "./activationCodeCard";
44
import { PurchaseButton } from "./purchaseButton";
55
import { ManageSubscriptionButton } from "./manageSubscriptionButton";
6-
import { BasicSettingsCard } from "../components/settingsCard";
7-
import { getPlan } from "@/lib/entitlements";
6+
import { SettingsCard } from "../components/settingsCard";
7+
import { getEntitlements } from "@/lib/entitlements";
88

99
export default authenticatedPage(async ({ prisma, org }) => {
1010
const license = await prisma.license.findUnique({
1111
where: { orgId: org.id },
1212
});
1313

14-
const plan = await getPlan();
14+
const entitlements = await getEntitlements();
1515

1616
return (
1717
<div className="flex flex-col gap-6">
1818
<div>
1919
<h3 className="text-lg font-medium">License</h3>
2020
<p className="text-sm text-muted-foreground">Manage your license.</p>
2121
</div>
22-
<BasicSettingsCard
23-
name="Current plan"
24-
description="Your active Sourcebot plan."
25-
>
26-
<span className="text-sm font-medium">{plan}</span>
27-
</BasicSettingsCard>
22+
<SettingsCard>
23+
<span className="text-sm font-medium">{entitlements.join(", ")}</span>
24+
</SettingsCard>
2825
<ActivationCodeCard isActivated={!!license} />
2926
<div className="flex gap-3">
3027
<PurchaseButton />

packages/web/src/ee/features/lighthouse/servicePing.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ export const syncWithLighthouse = async (orgId: number) => {
4545

4646
// If we have a license and Lighthouse returned license data, sync it
4747
if (license && response.license) {
48-
const { plan, seats, status } = response.license;
48+
const { entitlements, seats, status } = response.license;
4949

5050
await __unsafePrisma.license.update({
5151
where: { orgId: SINGLE_TENANT_ORG_ID },
5252
data: {
53-
plan,
53+
entitlements,
5454
seats,
5555
status,
5656
lastSyncAt: new Date(),
5757
},
5858
});
5959

60-
logger.info(`License synced: plan=${plan}, seats=${seats}, status=${status}`);
60+
logger.info(`License synced: entitlements=${entitlements.join(',')}, seats=${seats}, status=${status}`);
6161
}
6262
};
6363

packages/web/src/ee/features/lighthouse/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type ServicePingRequest = z.infer<typeof servicePingRequestSchema>;
1010

1111
export const servicePingResponseSchema = z.object({
1212
license: z.object({
13-
plan: z.string(),
13+
entitlements: z.string().array(),
1414
seats: z.number(),
1515
status: z.string(),
1616
}).optional(),

0 commit comments

Comments
 (0)