style: apply Biome formatting to enterprise files

This commit is contained in:
SnapOtter
2026-06-14 14:53:44 +08:00
parent 954cfb01a6
commit e7bef34918
23 changed files with 166 additions and 228 deletions
+21 -68
View File
@@ -70,12 +70,8 @@
"name": "api_keys_user_id_users_id_fk", "name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys", "tableFrom": "api_keys",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -209,12 +205,8 @@
"name": "audit_log_actor_id_users_id_fk", "name": "audit_log_actor_id_users_id_fk",
"tableFrom": "audit_log", "tableFrom": "audit_log",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["actor_id"],
"actor_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -378,12 +370,8 @@
"name": "jobs_user_id_users_id_fk", "name": "jobs_user_id_users_id_fk",
"tableFrom": "jobs", "tableFrom": "jobs",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -441,12 +429,8 @@
"name": "pipelines_user_id_users_id_fk", "name": "pipelines_user_id_users_id_fk",
"tableFrom": "pipelines", "tableFrom": "pipelines",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -518,12 +502,8 @@
"name": "roles_created_by_users_id_fk", "name": "roles_created_by_users_id_fk",
"tableFrom": "roles", "tableFrom": "roles",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["created_by"],
"created_by" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -533,9 +513,7 @@
"roles_name_unique": { "roles_name_unique": {
"name": "roles_name_unique", "name": "roles_name_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name"]
"name"
]
} }
}, },
"policies": {}, "policies": {},
@@ -583,12 +561,8 @@
"name": "sessions_user_id_users_id_fk", "name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions", "tableFrom": "sessions",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -660,9 +634,7 @@
"teams_name_unique": { "teams_name_unique": {
"name": "teams_name_unique", "name": "teams_name_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name"]
"name"
]
} }
}, },
"policies": {}, "policies": {},
@@ -753,12 +725,8 @@
"name": "user_files_user_id_users_id_fk", "name": "user_files_user_id_users_id_fk",
"tableFrom": "user_files", "tableFrom": "user_files",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -805,12 +773,8 @@
"name": "user_preferences_user_id_users_id_fk", "name": "user_preferences_user_id_users_id_fk",
"tableFrom": "user_preferences", "tableFrom": "user_preferences",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -818,10 +782,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"user_preferences_user_id_key_pk": { "user_preferences_user_id_key_pk": {
"name": "user_preferences_user_id_key_pk", "name": "user_preferences_user_id_key_pk",
"columns": [ "columns": ["user_id", "key"]
"user_id",
"key"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@@ -929,9 +890,7 @@
"users_username_unique": { "users_username_unique": {
"name": "users_username_unique", "name": "users_username_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["username"]
"username"
]
} }
}, },
"policies": {}, "policies": {},
@@ -943,13 +902,7 @@
"public.job_status": { "public.job_status": {
"name": "job_status", "name": "job_status",
"schema": "public", "schema": "public",
"values": [ "values": ["queued", "processing", "completed", "failed", "canceled"]
"queued",
"processing",
"completed",
"failed",
"canceled"
]
} }
}, },
"schemas": {}, "schemas": {},
+21 -68
View File
@@ -70,12 +70,8 @@
"name": "api_keys_user_id_users_id_fk", "name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys", "tableFrom": "api_keys",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -209,12 +205,8 @@
"name": "audit_log_actor_id_users_id_fk", "name": "audit_log_actor_id_users_id_fk",
"tableFrom": "audit_log", "tableFrom": "audit_log",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["actor_id"],
"actor_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -384,12 +376,8 @@
"name": "jobs_user_id_users_id_fk", "name": "jobs_user_id_users_id_fk",
"tableFrom": "jobs", "tableFrom": "jobs",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -447,12 +435,8 @@
"name": "pipelines_user_id_users_id_fk", "name": "pipelines_user_id_users_id_fk",
"tableFrom": "pipelines", "tableFrom": "pipelines",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -524,12 +508,8 @@
"name": "roles_created_by_users_id_fk", "name": "roles_created_by_users_id_fk",
"tableFrom": "roles", "tableFrom": "roles",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["created_by"],
"created_by" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "set null", "onDelete": "set null",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -539,9 +519,7 @@
"roles_name_unique": { "roles_name_unique": {
"name": "roles_name_unique", "name": "roles_name_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name"]
"name"
]
} }
}, },
"policies": {}, "policies": {},
@@ -589,12 +567,8 @@
"name": "sessions_user_id_users_id_fk", "name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions", "tableFrom": "sessions",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -685,9 +659,7 @@
"teams_name_unique": { "teams_name_unique": {
"name": "teams_name_unique", "name": "teams_name_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name"]
"name"
]
} }
}, },
"policies": {}, "policies": {},
@@ -778,12 +750,8 @@
"name": "user_files_user_id_users_id_fk", "name": "user_files_user_id_users_id_fk",
"tableFrom": "user_files", "tableFrom": "user_files",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -830,12 +798,8 @@
"name": "user_preferences_user_id_users_id_fk", "name": "user_preferences_user_id_users_id_fk",
"tableFrom": "user_preferences", "tableFrom": "user_preferences",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -843,10 +807,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"user_preferences_user_id_key_pk": { "user_preferences_user_id_key_pk": {
"name": "user_preferences_user_id_key_pk", "name": "user_preferences_user_id_key_pk",
"columns": [ "columns": ["user_id", "key"]
"user_id",
"key"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@@ -974,9 +935,7 @@
"users_username_unique": { "users_username_unique": {
"name": "users_username_unique", "name": "users_username_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["username"]
"username"
]
} }
}, },
"policies": {}, "policies": {},
@@ -988,13 +947,7 @@
"public.job_status": { "public.job_status": {
"name": "job_status", "name": "job_status",
"schema": "public", "schema": "public",
"values": [ "values": ["queued", "processing", "completed", "failed", "canceled"]
"queued",
"processing",
"completed",
"failed",
"canceled"
]
} }
}, },
"schemas": {}, "schemas": {},
+1 -5
View File
@@ -16,11 +16,7 @@ export function computeHmac(data: Record<string, unknown>, key: Buffer): string
return createHmac("sha256", key).update(canonicalize(data)).digest("hex"); return createHmac("sha256", key).update(canonicalize(data)).digest("hex");
} }
export function verifyHmac( export function verifyHmac(data: Record<string, unknown>, hmac: string, key: Buffer): boolean {
data: Record<string, unknown>,
hmac: string,
key: Buffer,
): boolean {
const computed = computeHmac(data, key); const computed = computeHmac(data, key);
return computed === hmac; return computed === hmac;
} }
+1 -4
View File
@@ -100,10 +100,7 @@ export async function auditLog(
requestId, requestId,
}; };
const integrity = computeHmac(rowData, hmacKey); const integrity = computeHmac(rowData, hmacKey);
await db await db.update(schema.auditLog).set({ integrity }).where(eq(schema.auditLog.id, id));
.update(schema.auditLog)
.set({ integrity })
.where(eq(schema.auditLog.id, id));
} }
} catch { } catch {
logger.warn({ event }, "Failed to compute audit HMAC"); logger.warn({ event }, "Failed to compute audit HMAC");
+1 -6
View File
@@ -1,9 +1,4 @@
import { import { createCipheriv, createDecipheriv, hkdf as hkdfCb, randomBytes } from "node:crypto";
createCipheriv,
createDecipheriv,
randomBytes,
hkdf as hkdfCb,
} from "node:crypto";
import { promisify } from "node:util"; import { promisify } from "node:util";
const hkdf = promisify(hkdfCb); const hkdf = promisify(hkdfCb);
+6 -1
View File
@@ -50,7 +50,12 @@ export async function deliverWebhook(
lastError = `HTTP ${response.status}`; lastError = `HTTP ${response.status}`;
// Don't retry 4xx errors (client errors = won't succeed on retry) // Don't retry 4xx errors (client errors = won't succeed on retry)
if (response.status >= 400 && response.status < 500) { if (response.status >= 400 && response.status < 500) {
return { success: false, statusCode: response.status, error: lastError, attempts: attempt + 1 }; return {
success: false,
statusCode: response.status,
error: lastError,
attempts: attempt + 1,
};
} }
} catch (err) { } catch (err) {
lastError = err instanceof Error ? err.message : String(err); lastError = err instanceof Error ? err.message : String(err);
+5 -1
View File
@@ -86,7 +86,11 @@ export async function apiKeyRoutes(app: FastifyInstance): Promise<void> {
return reply.status(409).send({ error: "Failed to create API key" }); return reply.status(409).send({ error: "Failed to create API key" });
} }
await auditFromRequest(request)("API_KEY_CREATED", { userId: user.id, keyId: id, keyName: name }); await auditFromRequest(request)("API_KEY_CREATED", {
userId: user.id,
keyId: id,
keyName: name,
});
// Return the raw key ONCE — it cannot be retrieved again // Return the raw key ONCE — it cannot be retrieved again
return reply.status(201).send({ return reply.status(201).send({
+10 -11
View File
@@ -35,9 +35,9 @@ export async function registerSiemRoutes(app: FastifyInstance): Promise<void> {
// Enterprise package not available // Enterprise package not available
} }
if (!featureEnabled) { if (!featureEnabled) {
return reply return reply.status(403).send({
.status(403) error: "SIEM forwarding requires an enterprise license with the siem_forwarding feature",
.send({ error: "SIEM forwarding requires an enterprise license with the siem_forwarding feature" }); });
} }
const [row] = await db const [row] = await db
@@ -65,10 +65,7 @@ export async function registerSiemRoutes(app: FastifyInstance): Promise<void> {
// PUT /api/v1/enterprise/siem/config // PUT /api/v1/enterprise/siem/config
app.put( app.put(
"/api/v1/enterprise/siem/config", "/api/v1/enterprise/siem/config",
async ( async (request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply) => {
request: FastifyRequest<{ Body: unknown }>,
reply: FastifyReply,
) => {
const user = await requirePermission("webhooks:manage")(request, reply); const user = await requirePermission("webhooks:manage")(request, reply);
if (!user) return; if (!user) return;
@@ -81,14 +78,16 @@ export async function registerSiemRoutes(app: FastifyInstance): Promise<void> {
// Enterprise package not available // Enterprise package not available
} }
if (!featureEnabled) { if (!featureEnabled) {
return reply return reply.status(403).send({
.status(403) error: "SIEM forwarding requires an enterprise license with the siem_forwarding feature",
.send({ error: "SIEM forwarding requires an enterprise license with the siem_forwarding feature" }); });
} }
const parsed = configSchema.safeParse(request.body); const parsed = configSchema.safeParse(request.body);
if (!parsed.success) { if (!parsed.success) {
return reply.status(400).send({ error: "Invalid SIEM config", details: parsed.error.issues }); return reply
.status(400)
.send({ error: "Invalid SIEM config", details: parsed.error.issues });
} }
const config = { ...parsed.data }; const config = { ...parsed.data };
+7 -4
View File
@@ -9,10 +9,10 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { env } from "../config.js";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditFromRequest } from "../lib/audit.js"; import { auditFromRequest } from "../lib/audit.js";
import { env } from "../config.js"; import { decrypt, encrypt, isEncrypted } from "../lib/encryption.js";
import { encrypt, decrypt, isEncrypted } from "../lib/encryption.js";
import { requirePermission } from "../permissions.js"; import { requirePermission } from "../permissions.js";
import { requireAuth } from "../plugins/auth.js"; import { requireAuth } from "../plugins/auth.js";
@@ -37,8 +37,11 @@ async function decryptIfNeeded(value: string): Promise<string> {
if (!isEncrypted(value)) return value; if (!isEncrypted(value)) return value;
if (!env.DATA_ENCRYPTION_KEY) return value; if (!env.DATA_ENCRYPTION_KEY) return value;
return ( return (
(await decrypt(value, env.DATA_ENCRYPTION_KEY, env.DATA_ENCRYPTION_KEY_PREVIOUS || undefined)) ?? (await decrypt(
value value,
env.DATA_ENCRYPTION_KEY,
env.DATA_ENCRYPTION_KEY_PREVIOUS || undefined,
)) ?? value
); );
} }
+2 -1
View File
@@ -23,7 +23,8 @@ const ENTERPRISE_DIR = join(ROOT, "packages/enterprise");
// Catches static `from "..."`, side-effect `import "..."`, and dynamic `import("...")` // Catches static `from "..."`, side-effect `import "..."`, and dynamic `import("...")`
// forms. The bare package entry "@snapotter/enterprise" (no trailing slash) stays // forms. The bare package entry "@snapotter/enterprise" (no trailing slash) stays
// allowed; only reaching INTO the package is a violation. // allowed; only reaching INTO the package is a violation.
const INTERNAL_IMPORT = /(from|import)\s*[\s(]\s*["'](@snapotter\/enterprise\/|[./]*packages\/enterprise\/)/; const INTERNAL_IMPORT =
/(from|import)\s*[\s(]\s*["'](@snapotter\/enterprise\/|[./]*packages\/enterprise\/)/;
const APP_IMPORT = /(from|import)\s*[\s(]\s*["'][^"']*apps\/(api|web)\//; const APP_IMPORT = /(from|import)\s*[\s(]\s*["'][^"']*apps\/(api|web)\//;
const violations = []; const violations = [];
+7 -5
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
/** /**
* Export an installed AI feature bundle as a gzipped tar archive for offline * Export an installed AI feature bundle as a gzipped tar archive for offline
* transfer to air-gapped SnapOtter installations. * transfer to air-gapped SnapOtter installations.
@@ -18,11 +20,9 @@
* createRequire, same pattern as tests/global-setup.ts). * createRequire, same pattern as tests/global-setup.ts).
*/ */
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto"; import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const apiRequire = createRequire(join(__dirname, "../apps/api/package.json")); const apiRequire = createRequire(join(__dirname, "../apps/api/package.json"));
@@ -136,7 +136,9 @@ if (modelEntries.length === 0) {
process.stderr.write("Warning: no model files found; archive will only contain bundle.json\n"); process.stderr.write("Warning: no model files found; archive will only contain bundle.json\n");
} }
process.stderr.write(`Exporting bundle "${bundleId}" v${version} (${modelEntries.length} model files)...\n`); process.stderr.write(
`Exporting bundle "${bundleId}" v${version} (${modelEntries.length} model files)...\n`,
);
// ── Create the archive ───────────────────────────────────────────────── // ── Create the archive ─────────────────────────────────────────────────
+4 -1
View File
@@ -19,7 +19,10 @@ mkdirSync(outDir, { recursive: true });
// --- 1. truncated.jpg: a real JPEG cut off at 40% --------------------------- // --- 1. truncated.jpg: a real JPEG cut off at 40% ---------------------------
const realJpeg = readFileSync(join(root, "tests", "fixtures", "sample-photo.jpg")); const realJpeg = readFileSync(join(root, "tests", "fixtures", "sample-photo.jpg"));
writeFileSync(join(outDir, "truncated.jpg"), realJpeg.subarray(0, Math.floor(realJpeg.length * 0.4))); writeFileSync(
join(outDir, "truncated.jpg"),
realJpeg.subarray(0, Math.floor(realJpeg.length * 0.4)),
);
// --- 2. zero-byte.png -------------------------------------------------------- // --- 2. zero-byte.png --------------------------------------------------------
writeFileSync(join(outDir, "zero-byte.png"), Buffer.alloc(0)); writeFileSync(join(outDir, "zero-byte.png"), Buffer.alloc(0));
+52 -13
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
/** /**
* Generates the tiny media/document fixtures committed under tests/fixtures/. * Generates the tiny media/document fixtures committed under tests/fixtures/.
* Requires ffmpeg on PATH (or FFMPEG_PATH) and qpdf (or QPDF_PATH). * Requires ffmpeg on PATH (or FFMPEG_PATH) and qpdf (or QPDF_PATH).
@@ -6,9 +8,7 @@
* node scripts/generate-test-fixtures.mjs * node scripts/generate-test-fixtures.mjs
*/ */
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { spawnSync } from "node:child_process"; import { dirname, join } from "node:path";
import { mkdirSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -38,17 +38,52 @@ function run(args) {
} }
// 1s 64x64 mp4 with video + audio (h264 + aac; needed by extract-audio + mute-video) // 1s 64x64 mp4 with video + audio (h264 + aac; needed by extract-audio + mute-video)
run(["-f", "lavfi", "-i", "testsrc=duration=1:size=64x64:rate=8", run([
"-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-f",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "lavfi",
"-c:a", "aac", "-b:a", "32k", "-shortest", "-i",
join(mediaDir, "tiny.mp4")]); "testsrc=duration=1:size=64x64:rate=8",
"-f",
"lavfi",
"-i",
"sine=frequency=440:duration=1",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
"-b:a",
"32k",
"-shortest",
join(mediaDir, "tiny.mp4"),
]);
// 1s sine mp3 // 1s sine mp3
run(["-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-c:a", "libmp3lame", "-b:a", "32k", run([
join(mediaDir, "tiny.mp3")]); "-f",
"lavfi",
"-i",
"sine=frequency=440:duration=1",
"-c:a",
"libmp3lame",
"-b:a",
"32k",
join(mediaDir, "tiny.mp3"),
]);
// 1s sine wav // 1s sine wav
run(["-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-c:a", "pcm_s16le", "-ar", "8000", run([
join(mediaDir, "tiny.wav")]); "-f",
"lavfi",
"-i",
"sine=frequency=440:duration=1",
"-c:a",
"pcm_s16le",
"-ar",
"8000",
join(mediaDir, "tiny.wav"),
]);
// Minimal OOXML/EPUB containers via archiver (resolved from apps/api). // Minimal OOXML/EPUB containers via archiver (resolved from apps/api).
const apiRequire = createRequire(join(root, "apps/api/package.json")); const apiRequire = createRequire(join(root, "apps/api/package.json"));
@@ -108,7 +143,11 @@ await writeZip(join(docsDir, "tiny.xlsx"), {
// EPUB requires the mimetype entry FIRST and STORED (uncompressed). // EPUB requires the mimetype entry FIRST and STORED (uncompressed).
await writeZip( await writeZip(
join(docsDir, "tiny.epub"), join(docsDir, "tiny.epub"),
{ "META-INF/container.xml": epubContainer, "OEBPS/content.opf": epubOpf, "OEBPS/chapter.xhtml": epubChapter }, {
"META-INF/container.xml": epubContainer,
"OEBPS/content.opf": epubOpf,
"OEBPS/chapter.xhtml": epubChapter,
},
{ name: "mimetype", content: "application/epub+zip" }, { name: "mimetype", content: "application/epub+zip" },
); );
+1 -2
View File
@@ -16,8 +16,7 @@ const apiRequire = createRequire(join(process.cwd(), "apps/api/package.json"));
const pg = apiRequire("pg"); const pg = apiRequire("pg");
const baseUrl = const baseUrl =
process.env.E2E_PG_BASE_URL || process.env.E2E_PG_BASE_URL || "postgres://snapotter:snapotter@localhost:5432/snapotter";
"postgres://snapotter:snapotter@localhost:5432/snapotter";
const dbName = process.argv[2]; const dbName = process.argv[2];
if (!dbName) { if (!dbName) {
+3 -6
View File
@@ -1,18 +1,15 @@
import { vi } from "vitest";
import type { EnterpriseFeature } from "@snapotter/enterprise"; import type { EnterpriseFeature } from "@snapotter/enterprise";
import { vi } from "vitest";
export function mockEnterpriseFeatures(features: EnterpriseFeature[]) { export function mockEnterpriseFeatures(features: EnterpriseFeature[]) {
vi.doMock("@snapotter/enterprise", () => ({ vi.doMock("@snapotter/enterprise", () => ({
isFeatureEnabled: (feature: string) => isFeatureEnabled: (feature: string) => features.includes(feature as EnterpriseFeature),
features.includes(feature as EnterpriseFeature),
getActiveLicense: () => ({ getActiveLicense: () => ({
org: "test-org", org: "test-org",
plan: "enterprise" as const, plan: "enterprise" as const,
features, features,
seats: 100, seats: 100,
expiresAt: new Date( expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
Date.now() + 365 * 24 * 60 * 60 * 1000,
).toISOString(),
issuedAt: new Date().toISOString(), issuedAt: new Date().toISOString(),
}), }),
initEnterprise: vi.fn(), initEnterprise: vi.fn(),
+1 -1
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { db, schema } from "../../apps/api/src/db/index.js"; import { db, schema } from "../../apps/api/src/db/index.js";
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js"; import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
import { eq } from "drizzle-orm";
let testApp: TestApp; let testApp: TestApp;
let adminToken: string; let adminToken: string;
@@ -39,9 +39,7 @@ async function setSetting(key: string, value: string): Promise<void> {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
} }
async function fetchAuditLog( async function fetchAuditLog(action: string): Promise<{ entries: any[]; total: number }> {
action: string,
): Promise<{ entries: any[]; total: number }> {
const res = await testApp.app.inject({ const res = await testApp.app.inject({
method: "GET", method: "GET",
url: `/api/v1/audit-log?action=${action}`, url: `/api/v1/audit-log?action=${action}`,
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js"; import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
let testApp: TestApp; let testApp: TestApp;
+6 -2
View File
@@ -1,5 +1,9 @@
import { describe, it, expect } from "vitest"; import { describe, expect, it } from "vitest";
import { canonicalize, computeHmac, verifyHmac } from "../../../apps/api/src/lib/audit-integrity.js"; import {
canonicalize,
computeHmac,
verifyHmac,
} from "../../../apps/api/src/lib/audit-integrity.js";
describe("audit integrity", () => { describe("audit integrity", () => {
const testKey = Buffer.from("a".repeat(64), "hex"); const testKey = Buffer.from("a".repeat(64), "hex");
+3 -3
View File
@@ -1,9 +1,9 @@
import { describe, it, expect } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
encrypt,
decrypt, decrypt,
isEncrypted,
deriveAuditHmacKey, deriveAuditHmacKey,
encrypt,
isEncrypted,
} from "../../../apps/api/src/lib/encryption.js"; } from "../../../apps/api/src/lib/encryption.js";
describe("encryption", () => { describe("encryption", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { ENTERPRISE_FEATURES, PLAN_FEATURES } from "@snapotter/enterprise"; import { ENTERPRISE_FEATURES, PLAN_FEATURES } from "@snapotter/enterprise";
import { describe, expect, it } from "vitest";
describe("enterprise feature flags", () => { describe("enterprise feature flags", () => {
it("includes all Phase 1 flags", () => { it("includes all Phase 1 flags", () => {
+5 -13
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
describe("enterprise mock", () => { describe("enterprise mock", () => {
beforeEach(() => { beforeEach(() => {
@@ -6,9 +6,7 @@ describe("enterprise mock", () => {
}); });
it("mocks feature enabled", async () => { it("mocks feature enabled", async () => {
const { mockEnterpriseFeatures } = await import( const { mockEnterpriseFeatures } = await import("../../helpers/enterprise-mock.js");
"../../helpers/enterprise-mock.js"
);
mockEnterpriseFeatures(["audit_export", "siem_forwarding"]); mockEnterpriseFeatures(["audit_export", "siem_forwarding"]);
const { isFeatureEnabled } = await import("@snapotter/enterprise"); const { isFeatureEnabled } = await import("@snapotter/enterprise");
expect(isFeatureEnabled("audit_export")).toBe(true); expect(isFeatureEnabled("audit_export")).toBe(true);
@@ -17,18 +15,14 @@ describe("enterprise mock", () => {
}); });
it("mocks no enterprise", async () => { it("mocks no enterprise", async () => {
const { mockNoEnterprise } = await import( const { mockNoEnterprise } = await import("../../helpers/enterprise-mock.js");
"../../helpers/enterprise-mock.js"
);
mockNoEnterprise(); mockNoEnterprise();
const { isFeatureEnabled } = await import("@snapotter/enterprise"); const { isFeatureEnabled } = await import("@snapotter/enterprise");
expect(isFeatureEnabled("audit_export")).toBe(false); expect(isFeatureEnabled("audit_export")).toBe(false);
}); });
it("returns license payload with correct shape", async () => { it("returns license payload with correct shape", async () => {
const { mockEnterpriseFeatures } = await import( const { mockEnterpriseFeatures } = await import("../../helpers/enterprise-mock.js");
"../../helpers/enterprise-mock.js"
);
mockEnterpriseFeatures(["s3_storage", "mfa"]); mockEnterpriseFeatures(["s3_storage", "mfa"]);
const { getActiveLicense } = await import("@snapotter/enterprise"); const { getActiveLicense } = await import("@snapotter/enterprise");
const license = getActiveLicense(); const license = getActiveLicense();
@@ -41,9 +35,7 @@ describe("enterprise mock", () => {
}); });
it("returns null license when no enterprise", async () => { it("returns null license when no enterprise", async () => {
const { mockNoEnterprise } = await import( const { mockNoEnterprise } = await import("../../helpers/enterprise-mock.js");
"../../helpers/enterprise-mock.js"
);
mockNoEnterprise(); mockNoEnterprise();
const { getActiveLicense } = await import("@snapotter/enterprise"); const { getActiveLicense } = await import("@snapotter/enterprise");
expect(getActiveLicense()).toBeNull(); expect(getActiveLicense()).toBeNull();
+4 -6
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("webhook delivery", () => { describe("webhook delivery", () => {
beforeEach(() => { beforeEach(() => {
@@ -15,11 +15,9 @@ describe("webhook delivery", () => {
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js"); const { deliverWebhook } = await import("../../../apps/api/src/lib/webhook-delivery.js");
const result = await deliverWebhook( const result = await deliverWebhook("https://siem.example.com/input", "Bearer test-token", [
"https://siem.example.com/input", { event: "LOGIN_SUCCESS", timestamp: "2026-01-01T00:00:00Z" },
"Bearer test-token", ]);
[{ event: "LOGIN_SUCCESS", timestamp: "2026-01-01T00:00:00Z" }],
);
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.attempts).toBe(1); expect(result.attempts).toBe(1);