mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
ENTERPRISE_FEATURES,
|
||||
type EnterpriseFeature,
|
||||
type LicensePayload,
|
||||
validateLicense,
|
||||
} from "./license.js";
|
||||
|
||||
let activeLicense: LicensePayload | null = null;
|
||||
|
||||
export function initEnterprise(licenseKey: string | undefined): {
|
||||
valid: boolean;
|
||||
license: LicensePayload | null;
|
||||
} {
|
||||
if (!licenseKey) {
|
||||
return { valid: false, license: null };
|
||||
}
|
||||
activeLicense = validateLicense(licenseKey);
|
||||
return { valid: activeLicense !== null, license: activeLicense };
|
||||
}
|
||||
|
||||
export function isFeatureEnabled(feature: EnterpriseFeature): boolean {
|
||||
if (!activeLicense) return false;
|
||||
return activeLicense.features.includes(feature);
|
||||
}
|
||||
|
||||
export function getActiveLicense(): LicensePayload | null {
|
||||
return activeLicense;
|
||||
}
|
||||
|
||||
export { ENTERPRISE_FEATURES, type EnterpriseFeature, type LicensePayload };
|
||||
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user