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:
SnapOtter
2026-06-06 20:17:49 +08:00
parent 06d1822491
commit 3b84fab765
15 changed files with 658 additions and 27 deletions
+9 -14
View File
@@ -10,8 +10,6 @@
* POST /api/v1/files/save-result — Save a tool processing result (new version)
*/
import { randomUUID } from "node:crypto";
import { createReadStream } from "node:fs";
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
import { and, desc, eq, like, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
@@ -24,9 +22,10 @@ import {
deleteStoredFile,
deleteThumbnail,
getCachedThumbnail,
getStoredFilePath,
readStoredFile,
saveFile,
saveThumbnail,
streamStoredFile,
} from "../lib/file-storage.js";
import { validateImageBuffer } from "../lib/file-validation.js";
import { sanitizeFilename } from "../lib/filename.js";
@@ -375,14 +374,12 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
return reply.status(404).send({ error: "File not found" });
}
const filePath = getStoredFilePath(file.storedName);
const stream = createReadStream(filePath);
stream.on("error", () => {
if (!reply.raw.headersSent) {
reply.status(404).send({ error: "File not found on disk" });
}
});
let stream;
try {
stream = await streamStoredFile(file.storedName);
} catch {
return reply.status(404).send({ error: "File not found in storage" });
}
return reply
.header("Content-Type", file.mimeType)
@@ -422,10 +419,8 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
.send(cached);
}
const filePath = getStoredFilePath(file.storedName);
try {
const rawBuffer = await readFile(filePath);
const rawBuffer = await readStoredFile(file.storedName);
const validation = await validateImageBuffer(rawBuffer, file.originalName);
let decoded: Buffer<ArrayBuffer> = Buffer.from(rawBuffer);
if (validation.valid && validation.format === "heif") {