Files
SnapOtter/packages/enterprise/src/license.ts
T
SnapOtter 3b84fab765 feat: add enterprise licensing and S3 storage backend
Add the enterprise package with Ed25519 license key validation and
feature gating. Enterprise code lives in the public repo under a
proprietary license (Cal.com/PostHog model), protected legally, not
by code hiding.

Implement S3-compatible storage backend as the first enterprise
feature. The file-storage module now delegates to either local
filesystem or S3 based on STORAGE_MODE env var. Works with AWS S3,
Cloudflare R2, DigitalOcean Spaces, MinIO, and any S3-compatible
provider. Workspace files remain local (ephemeral processing).

New env vars: STORAGE_MODE, S3_BUCKET, S3_REGION, S3_ENDPOINT,
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE,
S3_PREFIX, SNAPOTTER_LICENSE_KEY.

Tested against MinIO: 10 S3 integration tests + 82 existing tests
pass with zero regressions.
2026-06-06 20:17:49 +08:00

55 lines
1.4 KiB
TypeScript

import { createPublicKey, verify } from "node:crypto";
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAmbsNwJdTomjfwc7i9+s7xgSq+MIrDxvYPTki2SOwhI8=
-----END PUBLIC KEY-----`;
export const ENTERPRISE_FEATURES = [
"saml_sso",
"s3_storage",
"scim",
"multi_tenancy",
"webhooks",
"audit_export",
"mfa",
"per_tool_permissions",
] as const;
export type EnterpriseFeature = (typeof ENTERPRISE_FEATURES)[number];
export const PLAN_FEATURES: Record<string, readonly EnterpriseFeature[]> = {
team: ["saml_sso", "s3_storage", "multi_tenancy"],
enterprise: ENTERPRISE_FEATURES,
};
export interface LicensePayload {
org: string;
plan: "team" | "enterprise";
features: EnterpriseFeature[];
seats: number;
expiresAt: string;
issuedAt: string;
}
export function validateLicense(key: string): LicensePayload | null {
try {
const dotIndex = key.indexOf(".");
if (dotIndex < 1) return null;
const payloadBytes = Buffer.from(key.slice(0, dotIndex), "base64url");
const signature = Buffer.from(key.slice(dotIndex + 1), "base64url");
const publicKey = createPublicKey(PUBLIC_KEY_PEM);
const valid = verify(null, payloadBytes, publicKey, signature);
if (!valid) return null;
const payload = JSON.parse(payloadBytes.toString("utf-8")) as LicensePayload;
if (new Date(payload.expiresAt) < new Date()) return null;
return payload;
} catch {
return null;
}
}