2025-06-17 21:04:25 +00:00
|
|
|
import { base64Decode } from "./utils.js";
|
2025-04-25 05:28:13 +00:00
|
|
|
import { z } from "zod";
|
2025-06-02 18:16:01 +00:00
|
|
|
import { createLogger } from "@sourcebot/logger";
|
2025-06-06 05:18:52 +00:00
|
|
|
import { verifySignature } from "@sourcebot/crypto";
|
2025-06-17 21:04:25 +00:00
|
|
|
import { env } from "./env.js";
|
|
|
|
|
import { SOURCEBOT_SUPPORT_EMAIL, SOURCEBOT_UNLIMITED_SEATS } from "./constants.js";
|
2025-06-02 18:16:01 +00:00
|
|
|
|
|
|
|
|
const logger = createLogger('entitlements');
|
2025-04-25 05:28:13 +00:00
|
|
|
|
|
|
|
|
const eeLicenseKeyPrefix = "sourcebot_ee_";
|
|
|
|
|
|
|
|
|
|
const eeLicenseKeyPayloadSchema = z.object({
|
|
|
|
|
id: z.string(),
|
2025-05-28 23:08:42 +00:00
|
|
|
seats: z.number(),
|
2025-04-25 05:28:13 +00:00
|
|
|
// ISO 8601 date string
|
2025-05-28 23:08:42 +00:00
|
|
|
expiryDate: z.string().datetime(),
|
2025-06-06 05:18:52 +00:00
|
|
|
sig: z.string(),
|
2025-04-25 05:28:13 +00:00
|
|
|
});
|
|
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
type LicenseKeyPayload = z.infer<typeof eeLicenseKeyPayloadSchema>;
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-06-17 21:04:25 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const planLabels = {
|
|
|
|
|
oss: "OSS",
|
|
|
|
|
"cloud:team": "Team",
|
|
|
|
|
"cloud:demo": "Demo",
|
|
|
|
|
"self-hosted:enterprise": "Enterprise (Self-Hosted)",
|
|
|
|
|
"self-hosted:enterprise-unlimited": "Enterprise (Self-Hosted) Unlimited",
|
|
|
|
|
} as const;
|
|
|
|
|
export type Plan = keyof typeof planLabels;
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const entitlements = [
|
|
|
|
|
"search-contexts",
|
|
|
|
|
"billing",
|
2025-07-19 21:04:41 +00:00
|
|
|
"anonymous-access",
|
2025-06-17 21:04:25 +00:00
|
|
|
"multi-tenancy",
|
|
|
|
|
"sso",
|
2025-06-18 17:50:36 +00:00
|
|
|
"code-nav",
|
2025-06-20 21:57:05 +00:00
|
|
|
"audit",
|
2025-09-20 23:51:14 +00:00
|
|
|
"analytics",
|
2025-10-22 03:12:29 +00:00
|
|
|
"permission-syncing",
|
|
|
|
|
"github-app"
|
2025-06-17 21:04:25 +00:00
|
|
|
] as const;
|
|
|
|
|
export type Entitlement = (typeof entitlements)[number];
|
|
|
|
|
|
|
|
|
|
const entitlementsByPlan: Record<Plan, Entitlement[]> = {
|
2025-07-19 21:04:41 +00:00
|
|
|
oss: ["anonymous-access"],
|
2025-06-17 21:04:25 +00:00
|
|
|
"cloud:team": ["billing", "multi-tenancy", "sso", "code-nav"],
|
2025-10-22 03:12:29 +00:00
|
|
|
"self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics", "permission-syncing", "github-app"],
|
|
|
|
|
"self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics", "permission-syncing", "github-app"],
|
2025-06-17 21:04:25 +00:00
|
|
|
// Special entitlement for https://demo.sourcebot.dev
|
2025-07-19 21:04:41 +00:00
|
|
|
"cloud:demo": ["anonymous-access", "code-nav", "search-contexts"],
|
2025-06-17 21:04:25 +00:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
const decodeLicenseKeyPayload = (payload: string): LicenseKeyPayload => {
|
|
|
|
|
try {
|
|
|
|
|
const decodedPayload = base64Decode(payload);
|
|
|
|
|
const payloadJson = JSON.parse(decodedPayload);
|
2025-06-06 05:18:52 +00:00
|
|
|
const licenseData = eeLicenseKeyPayloadSchema.parse(payloadJson);
|
|
|
|
|
|
2025-06-17 21:04:25 +00:00
|
|
|
const dataToVerify = JSON.stringify({
|
|
|
|
|
expiryDate: licenseData.expiryDate,
|
|
|
|
|
id: licenseData.id,
|
|
|
|
|
seats: licenseData.seats
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isSignatureValid = verifySignature(dataToVerify, licenseData.sig, env.SOURCEBOT_PUBLIC_KEY_PATH);
|
|
|
|
|
if (!isSignatureValid) {
|
|
|
|
|
logger.error('License key signature verification failed');
|
2025-06-06 05:18:52 +00:00
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return licenseData;
|
2025-05-28 23:08:42 +00:00
|
|
|
} catch (error) {
|
2025-06-02 18:16:01 +00:00
|
|
|
logger.error(`Failed to decode license key payload: ${error}`);
|
2025-05-28 23:08:42 +00:00
|
|
|
process.exit(1);
|
2025-04-25 05:28:13 +00:00
|
|
|
}
|
2025-05-28 23:08:42 +00:00
|
|
|
}
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
export const getLicenseKey = (): LicenseKeyPayload | null => {
|
2025-04-25 05:28:13 +00:00
|
|
|
const licenseKey = env.SOURCEBOT_EE_LICENSE_KEY;
|
|
|
|
|
if (licenseKey && licenseKey.startsWith(eeLicenseKeyPrefix)) {
|
|
|
|
|
const payload = licenseKey.substring(eeLicenseKeyPrefix.length);
|
2025-05-28 23:08:42 +00:00
|
|
|
return decodeLicenseKeyPayload(payload);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
export const getPlan = (): Plan => {
|
|
|
|
|
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT) {
|
2025-05-29 00:35:18 +00:00
|
|
|
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo") {
|
|
|
|
|
return "cloud:demo";
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
return "cloud:team";
|
|
|
|
|
}
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
const licenseKey = getLicenseKey();
|
|
|
|
|
if (licenseKey) {
|
|
|
|
|
const expiryDate = new Date(licenseKey.expiryDate);
|
|
|
|
|
if (expiryDate.getTime() < new Date().getTime()) {
|
2025-06-09 19:51:35 +00:00
|
|
|
logger.error(`The provided license key has expired (${expiryDate.toLocaleString()}). Please contact ${SOURCEBOT_SUPPORT_EMAIL} for support.`);
|
2025-05-28 23:08:42 +00:00
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
return licenseKey.seats === SOURCEBOT_UNLIMITED_SEATS ? "self-hosted:enterprise-unlimited" : "self-hosted:enterprise";
|
|
|
|
|
} else {
|
|
|
|
|
return "oss";
|
2025-04-25 05:28:13 +00:00
|
|
|
}
|
2025-05-28 23:08:42 +00:00
|
|
|
}
|
2025-04-25 05:28:13 +00:00
|
|
|
|
2025-05-28 23:08:42 +00:00
|
|
|
export const getSeats = (): number => {
|
2025-07-15 03:14:41 +00:00
|
|
|
const licenseKey = getLicenseKey();
|
2025-05-28 23:08:42 +00:00
|
|
|
return licenseKey?.seats ?? SOURCEBOT_UNLIMITED_SEATS;
|
2025-04-25 05:28:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const hasEntitlement = (entitlement: Entitlement) => {
|
2025-05-28 23:08:42 +00:00
|
|
|
const entitlements = getEntitlements();
|
2025-04-25 05:28:13 +00:00
|
|
|
return entitlements.includes(entitlement);
|
|
|
|
|
}
|
2025-05-28 23:08:42 +00:00
|
|
|
|
|
|
|
|
export const getEntitlements = (): Entitlement[] => {
|
|
|
|
|
const plan = getPlan();
|
|
|
|
|
return entitlementsByPlan[plan];
|
|
|
|
|
}
|