Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
129 lines
3.4 KiB
TypeScript
129 lines
3.4 KiB
TypeScript
/**
|
|
* Input validation utilities for security and data integrity
|
|
*/
|
|
|
|
/**
|
|
* Validate Jellyfin item ID format
|
|
* Item IDs should be non-empty alphanumeric strings with optional dashes/underscores
|
|
*/
|
|
export function validateItemId(itemId: string): void {
|
|
if (!itemId || typeof itemId !== "string") {
|
|
throw new Error("Invalid itemId: must be a non-empty string");
|
|
}
|
|
|
|
if (itemId.length > 50) {
|
|
throw new Error("Invalid itemId: exceeds maximum length of 50 characters");
|
|
}
|
|
|
|
// Jellyfin item IDs are typically UUIDs or numeric IDs
|
|
if (!/^[a-zA-Z0-9\-_]+$/.test(itemId)) {
|
|
throw new Error("Invalid itemId: contains invalid characters");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate image type to prevent path traversal attacks
|
|
*/
|
|
export function validateImageType(imageType: string): void {
|
|
if (!imageType || typeof imageType !== "string") {
|
|
throw new Error("Invalid imageType: must be a non-empty string");
|
|
}
|
|
|
|
// Only allow known image types
|
|
const validImageTypes = [
|
|
"Primary",
|
|
"Backdrop",
|
|
"Banner",
|
|
"Disc",
|
|
"Box",
|
|
"Logo",
|
|
"Thumb",
|
|
"Art",
|
|
"Chapter",
|
|
"Keyframe",
|
|
];
|
|
|
|
if (!validImageTypes.includes(imageType)) {
|
|
throw new Error(`Invalid imageType: "${imageType}" is not a valid image type`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate media source ID format
|
|
*/
|
|
export function validateMediaSourceId(mediaSourceId: string): void {
|
|
if (!mediaSourceId || typeof mediaSourceId !== "string") {
|
|
throw new Error("Invalid mediaSourceId: must be a non-empty string");
|
|
}
|
|
|
|
if (mediaSourceId.length > 50) {
|
|
throw new Error("Invalid mediaSourceId: exceeds maximum length");
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9\-_]+$/.test(mediaSourceId)) {
|
|
throw new Error("Invalid mediaSourceId: contains invalid characters");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate URL path segment to prevent directory traversal
|
|
* Disallows: "..", ".", and characters that could enable attacks
|
|
*/
|
|
export function validateUrlPathSegment(segment: string): void {
|
|
if (!segment || typeof segment !== "string") {
|
|
throw new Error("Invalid path segment: must be a non-empty string");
|
|
}
|
|
|
|
if (segment === ".." || segment === ".") {
|
|
throw new Error("Invalid path segment: directory traversal not allowed");
|
|
}
|
|
|
|
// Reject path separators and null bytes
|
|
if (/[/\\%]/.test(segment)) {
|
|
throw new Error("Invalid path segment: contains invalid characters");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate numeric parameter (width, height, quality, etc.)
|
|
*/
|
|
export function validateNumericParam(
|
|
value: unknown,
|
|
min = 0,
|
|
max = 10000,
|
|
name = "parameter",
|
|
): number {
|
|
// Must be an actual number, not a string that looks like a number
|
|
if (typeof value !== "number") {
|
|
throw new Error(`Invalid ${name}: must be an integer`);
|
|
}
|
|
|
|
if (!Number.isInteger(value)) {
|
|
throw new Error(`Invalid ${name}: must be an integer`);
|
|
}
|
|
|
|
if (value < min || value > max) {
|
|
throw new Error(`Invalid ${name}: must be between ${min} and ${max}`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Sanitize query parameter value - allows alphanumeric, dash, underscore
|
|
*/
|
|
export function validateQueryParamValue(value: string, maxLength = 100): void {
|
|
if (typeof value !== "string") {
|
|
throw new Error("Query parameter value must be a string");
|
|
}
|
|
|
|
if (value.length > maxLength) {
|
|
throw new Error(`Query parameter exceeds maximum length of ${maxLength}`);
|
|
}
|
|
|
|
// Allow only safe characters in query params
|
|
if (!/^[a-zA-Z0-9\-_.~]+$/.test(value)) {
|
|
throw new Error("Query parameter contains invalid characters");
|
|
}
|
|
}
|