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,86 @@
|
||||
#!/usr/bin/env node
|
||||
import { createPrivateKey, generateKeyPairSync, sign } from "node:crypto";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const PRIVATE_KEY_PATH = ".license-signing-key";
|
||||
|
||||
const PLAN_FEATURES = {
|
||||
team: ["saml_sso", "s3_storage", "multi_tenancy"],
|
||||
enterprise: [
|
||||
"saml_sso",
|
||||
"s3_storage",
|
||||
"scim",
|
||||
"multi_tenancy",
|
||||
"webhooks",
|
||||
"audit_export",
|
||||
"mfa",
|
||||
"per_tool_permissions",
|
||||
],
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === "keygen") {
|
||||
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
||||
});
|
||||
writeFileSync(PRIVATE_KEY_PATH, privateKey, "utf-8");
|
||||
console.log("Private key saved to", PRIVATE_KEY_PATH);
|
||||
console.log("\nPublic key (paste into packages/enterprise/src/license.ts):\n");
|
||||
console.log(publicKey);
|
||||
} else if (command === "sign") {
|
||||
const args = process.argv.slice(3);
|
||||
const get = (name) => {
|
||||
const i = args.indexOf(`--${name}`);
|
||||
return i >= 0 ? args[i + 1] : undefined;
|
||||
};
|
||||
|
||||
const org = get("org");
|
||||
const plan = get("plan") || "enterprise";
|
||||
const seats = parseInt(get("seats") || "0", 10);
|
||||
const expires = get("expires");
|
||||
|
||||
if (!org || !expires) {
|
||||
console.error(
|
||||
"Usage: generate-license.mjs sign --org <name> --expires <YYYY-MM-DD> [--plan team|enterprise] [--seats N]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(PRIVATE_KEY_PATH)) {
|
||||
console.error(`Private key not found at ${PRIVATE_KEY_PATH}. Run 'generate-license.mjs keygen' first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const features = PLAN_FEATURES[plan] || PLAN_FEATURES.enterprise;
|
||||
const payload = {
|
||||
org,
|
||||
plan,
|
||||
features,
|
||||
seats,
|
||||
expiresAt: new Date(expires).toISOString(),
|
||||
issuedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const payloadBytes = Buffer.from(JSON.stringify(payload), "utf-8");
|
||||
const privateKeyPem = readFileSync(PRIVATE_KEY_PATH, "utf-8");
|
||||
const privateKey = createPrivateKey(privateKeyPem);
|
||||
const signature = sign(null, payloadBytes, privateKey);
|
||||
|
||||
const licenseKey = `${payloadBytes.toString("base64url")}.${signature.toString("base64url")}`;
|
||||
|
||||
console.log("License payload:", JSON.stringify(payload, null, 2));
|
||||
console.log("\nLicense key:\n");
|
||||
console.log(licenseKey);
|
||||
} else {
|
||||
console.log("SnapOtter License Key Generator\n");
|
||||
console.log("Commands:");
|
||||
console.log(" keygen Generate a new Ed25519 signing keypair");
|
||||
console.log(" sign Sign a license key\n");
|
||||
console.log("Sign options:");
|
||||
console.log(" --org <name> Organization name (required)");
|
||||
console.log(" --expires <YYYY-MM-DD> Expiration date (required)");
|
||||
console.log(" --plan <team|enterprise> License plan (default: enterprise)");
|
||||
console.log(" --seats <N> Seat count (default: 0 = unlimited)");
|
||||
}
|
||||
Reference in New Issue
Block a user