mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* fix(enterprise): ship enterprise pkg in prod image, full license features, tracing key fallback docker/Dockerfile: COPY packages/enterprise manifest+src into the production stage. Without it, apps/api's workspace link to @snapotter/enterprise dangles and every import() throws (silently caught), so all 19 enterprise features failed closed (enterprise.active=false) regardless of a valid license. scripts/generate-license.mjs: sync PLAN_FEATURES with packages/enterprise/src/license.ts so a --plan enterprise license unlocks all 19 features (was 8) and team unlocks 8. apps/api/src/tracing.ts: accept SNAPOTTER_LICENSE_KEY as a fallback to LICENSE_KEY so distributed_tracing activates with the same key as the rest of the app. * fix(docker): keep scripts/bake-analytics.mjs in build context .dockerignore excluded the whole scripts/ dir (PR #82, V1 hardening), but docker/Dockerfile later added 'COPY scripts/bake-analytics.mjs' for the analytics bake step. A clean production image build therefore fails with 'scripts/bake-analytics.mjs: not found'. The published image build is gated off in CI so this latent break went unnoticed. Exclude scripts/* but re-include the one file the Dockerfile needs. * fix: S3 upload stream, analytics bake reaches API, dedupe retention field, reconcile orphan jobs storage-s3.ts: wrap the upload AsyncIterable in Readable.from() so @aws-sdk/lib-storage accepts it. STORAGE_MODE=s3 file uploads failed with 'Body Data is unsupported format' for every tool because a bare async generator is not a Readable. docker/Dockerfile: COPY the builder-baked analytics baked.ts into the API runtime stage. The API re-copied the committed (off) baked.ts from the build context, so the SNAPOTTER_ANALYTICS build arg had no effect on the API -- and since the SPA reads /api/v1/config/analytics, analytics was off everywhere regardless of the arg. settings-dialog.tsx: remove the duplicate tempFileMaxAgeHours control under Data Retention; it bound the same setting key as the File Management control with a different default, so editing either silently overwrote the other. apps/api/src/index.ts: reconcile orphaned job rows (empty tool_id, never enqueued to BullMQ) at boot so they don't sit in processing/queued forever and inflate the per-user concurrent-job count and the upgrade-check in-flight gate. * fix(web): style the SSO login buttons (they referenced undefined theme tokens) The OIDC/SAML 'Sign in with <provider>' buttons used bg-secondary / text-secondary-foreground, which the web theme never defines (it has primary, background, foreground, muted, border, card, primary-subtle). Those classes resolved to nothing, so the buttons rendered as bare unstyled text on the login page. Restyle: the optional (non-enforced) buttons become white-card outline buttons with a key icon and an orange hover tint, secondary to the primary Login button; the SSO-enforced buttons become solid primary with the icon. * fix: gate S3 behind license, custom-role enterprise perms, wire retention UI, cleanup S3 is a licensed feature, but shipping packages/enterprise in every image removed the implicit gate, so STORAGE_MODE=s3 worked without a license. Enforce isFeatureEnabled('s3_storage') at boot and fail fast if unlicensed. Custom roles can now be granted security:manage / compliance:manage / webhooks:manage (roles.ts ALL_PERMISSIONS + the Roles UI) so admins can build least-privilege compliance/security roles instead of only the built-in admin role. retentionSweep now reads the jobsRetentionDays / auditRetentionDays DB settings the System Settings UI writes (env vars become the fallback default), mirroring how the temp-file sweep reads tempFileMaxAgeHours. Previously those two UI controls were no-ops. Cleanup: drop the never-set snapotter_storage_bytes gauge and the unused MAX_WORKSPACE_SIZE_GB env var; emit tool_client_error to PostHog from the web ErrorBoundary (client crashes were not reaching analytics); add the Python OpenTelemetry packages so the innermost sidecar.<script> span exports; fix the stale 'only local storage' line in the docs; delete two e2e-analytics specs that tested the removed consent UI. * fix(env): restore MAX_WORKSPACE_SIZE_GB default security-auth-hardening.test.ts asserts env.MAX_WORKSPACE_SIZE_GB defaults to 10, so the var is an intentional (tested) default, not dead code. Removing it in the cleanup commit broke that unit test. Keep the declaration.
112 lines
3.5 KiB
JavaScript
112 lines
3.5 KiB
JavaScript
#!/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";
|
|
|
|
// Keep in sync with PLAN_FEATURES in packages/enterprise/src/license.ts.
|
|
// isFeatureEnabled() checks the signed license's own `features` array, so a stale
|
|
// list here silently leaves gated features 403ing even with a valid enterprise key.
|
|
const PLAN_FEATURES = {
|
|
team: [
|
|
"saml_sso",
|
|
"s3_storage",
|
|
"multi_tenancy",
|
|
"audit_export",
|
|
"siem_forwarding",
|
|
"sso_enforcement",
|
|
"upgrade_management",
|
|
"admin_alerts",
|
|
],
|
|
enterprise: [
|
|
"saml_sso",
|
|
"s3_storage",
|
|
"scim",
|
|
"multi_tenancy",
|
|
"webhooks",
|
|
"audit_export",
|
|
"mfa",
|
|
"per_tool_permissions",
|
|
"siem_forwarding",
|
|
"tamper_resistant_audit",
|
|
"legal_hold",
|
|
"gdpr_lifecycle",
|
|
"team_retention_overrides",
|
|
"sso_enforcement",
|
|
"ip_allowlist",
|
|
"config_export_import",
|
|
"upgrade_management",
|
|
"admin_alerts",
|
|
"distributed_tracing",
|
|
],
|
|
};
|
|
|
|
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)");
|
|
}
|