security sweep

This commit is contained in:
2026-05-29 09:03:47 -04:00
parent 469c28fa64
commit 3b29de3234
60 changed files with 7148 additions and 413 deletions

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest";
import { safeParse } from "valibot";
import { CreateEnrollmentSchema, AnalyzeAudioSchema } from "./voiceprint";
describe("CreateEnrollmentSchema", () => {
it("accepts valid enrollment with small audio", () => {
const data = {
name: "My Voice",
audioBase64: "dGVzdC1hdWRpby1iYXNlNjQ=",
};
const result = safeParse(CreateEnrollmentSchema, data);
expect(result.success).toBe(true);
});
it("rejects audio payload exceeding maxLength", () => {
// ~3MB base64 string (exceeds 2.6MB default limit)
const largeAudio = "A".repeat(3_000_000);
const data = {
name: "My Voice",
audioBase64: largeAudio,
};
const result = safeParse(CreateEnrollmentSchema, data);
expect(result.success).toBe(false);
});
it("rejects empty audio", () => {
const data = { name: "My Voice", audioBase64: "" };
const result = safeParse(CreateEnrollmentSchema, data);
expect(result.success).toBe(false);
});
it("rejects missing name", () => {
const data = { audioBase64: "dGVzdA==" };
const result = safeParse(CreateEnrollmentSchema, data);
expect(result.success).toBe(false);
});
});
describe("AnalyzeAudioSchema", () => {
it("accepts valid analysis request", () => {
const data = { audioBase64: "dGVzdC1hdWRpby1iYXNlNjQ=" };
const result = safeParse(AnalyzeAudioSchema, data);
expect(result.success).toBe(true);
});
it("accepts analysis with optional enrollmentId", () => {
const data = { audioBase64: "dGVzdA==", enrollmentId: "enr-123" };
const result = safeParse(AnalyzeAudioSchema, data);
expect(result.success).toBe(true);
});
it("rejects audio payload exceeding maxLength", () => {
const largeAudio = "A".repeat(3_000_000);
const data = { audioBase64: largeAudio };
const result = safeParse(AnalyzeAudioSchema, data);
expect(result.success).toBe(false);
});
it("rejects empty audio", () => {
const data = { audioBase64: "" };
const result = safeParse(AnalyzeAudioSchema, data);
expect(result.success).toBe(false);
});
});

View File

@@ -1,29 +1,53 @@
import { object, string, minLength, optional, number, picklist } from "valibot";
import {
object,
string,
minLength,
maxLength,
optional,
number,
picklist,
} from "valibot";
/**
* Maximum allowed base64-encoded audio payload length.
* Default: ~2.6MB base64 (≈2MB decoded). Configurable via VOICEPRINT_MAX_BASE64_LENGTH.
* Formula: maxDecodedBytes * 4/3 ≈ base64 length
*/
const MAX_BASE64_LENGTH = parseInt(
process.env.VOICEPRINT_MAX_BASE64_LENGTH ?? "2621440",
10,
);
/** Maximum decoded audio size in bytes (default 2MB). */
const MAX_DECODED_SIZE = parseInt(
process.env.VOICEPRINT_MAX_DECODED_SIZE ?? "2097152",
10,
);
export const CreateEnrollmentSchema = object({
name: string([minLength(1)]),
audioBase64: string([minLength(1)]),
name: string([minLength(1)]),
audioBase64: string([minLength(1), maxLength(MAX_BASE64_LENGTH)]),
});
export const DeleteEnrollmentSchema = object({
enrollmentId: string([minLength(1)]),
enrollmentId: string([minLength(1)]),
});
export const AnalyzeAudioSchema = object({
audioBase64: string([minLength(1)]),
enrollmentId: optional(string()),
audioBase64: string([minLength(1), maxLength(MAX_BASE64_LENGTH)]),
enrollmentId: optional(string()),
});
export const AnalysisFilterSchema = object({
page: optional(number(), 1),
limit: optional(number(), 20),
verdict: optional(picklist(["NATURAL", "SYNTHETIC", "UNCERTAIN"])),
page: optional(number(), 1),
limit: optional(number(), 20),
verdict: optional(picklist(["NATURAL", "SYNTHETIC", "UNCERTAIN"])),
});
export const AnalysisResultSchema = object({
analysisId: string([minLength(1)]),
analysisId: string([minLength(1)]),
});
export const JobStatusSchema = object({
jobId: string([minLength(1)]),
jobId: string([minLength(1)]),
});