security sweep
This commit is contained in:
64
web/src/server/api/schemas/voiceprint.test.ts
Normal file
64
web/src/server/api/schemas/voiceprint.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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)]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user