Files
indiekit-endpoint-microsub/lib/utils/validation.js
Ricardo 4819c229cd feat: restore full microsub implementation with reader UI
Restores complete implementation from feat/endpoint-microsub branch:
- Reader UI with views (reader.njk, channel.njk, feeds.njk, etc.)
- Feed polling, parsing, and normalization
- WebSub subscriber
- SSE realtime updates
- Redis caching
- Search indexing
- Media proxy
- Webmention processing
2026-02-06 20:20:25 +01:00

218 lines
4.6 KiB
JavaScript

/**
* Input validation utilities for Microsub
* @module utils/validation
*/
import { IndiekitError } from "@indiekit/error";
/**
* Valid Microsub actions
*/
export const VALID_ACTIONS = [
"channels",
"timeline",
"follow",
"unfollow",
"search",
"preview",
"mute",
"unmute",
"block",
"unblock",
"events",
];
/**
* Valid channel methods
*/
export const VALID_CHANNEL_METHODS = ["delete", "order"];
/**
* Valid timeline methods
*/
export const VALID_TIMELINE_METHODS = ["mark_read", "mark_unread", "remove"];
/**
* Valid exclude types for channel filtering
*/
export const VALID_EXCLUDE_TYPES = [
"like",
"repost",
"bookmark",
"reply",
"checkin",
];
/**
* Validate action parameter
* @param {string} action - Action to validate
* @throws {IndiekitError} If action is invalid
*/
export function validateAction(action) {
if (!action) {
throw new IndiekitError("Missing required parameter: action", {
status: 400,
});
}
if (!VALID_ACTIONS.includes(action)) {
throw new IndiekitError(`Invalid action: ${action}`, {
status: 400,
});
}
}
/**
* Validate channel UID
* @param {string} channel - Channel UID to validate
* @param {boolean} [required] - Whether channel is required
* @throws {IndiekitError} If channel is invalid
*/
export function validateChannel(channel, required = true) {
if (required && !channel) {
throw new IndiekitError("Missing required parameter: channel", {
status: 400,
});
}
if (channel && typeof channel !== "string") {
throw new IndiekitError("Invalid channel parameter", {
status: 400,
});
}
}
/**
* Validate URL parameter
* @param {string} url - URL to validate
* @param {string} [paramName] - Parameter name for error message
* @param parameterName
* @throws {IndiekitError} If URL is invalid
*/
export function validateUrl(url, parameterName = "url") {
if (!url) {
throw new IndiekitError(`Missing required parameter: ${parameterName}`, {
status: 400,
});
}
try {
new URL(url);
} catch {
throw new IndiekitError(`Invalid URL: ${url}`, {
status: 400,
});
}
}
/**
* Validate entry/entries parameter
* @param {string|Array} entry - Entry ID(s) to validate
* @returns {Array} Array of entry IDs
* @throws {IndiekitError} If entry is invalid
*/
export function validateEntries(entry) {
if (!entry) {
throw new IndiekitError("Missing required parameter: entry", {
status: 400,
});
}
// Normalize to array
const entries = Array.isArray(entry) ? entry : [entry];
if (entries.length === 0) {
throw new IndiekitError("Entry parameter cannot be empty", {
status: 400,
});
}
return entries;
}
/**
* Validate channel name
* @param {string} name - Channel name to validate
* @throws {IndiekitError} If name is invalid
*/
export function validateChannelName(name) {
if (!name || typeof name !== "string") {
throw new IndiekitError("Missing required parameter: name", {
status: 400,
});
}
if (name.length > 100) {
throw new IndiekitError("Channel name must be 100 characters or less", {
status: 400,
});
}
}
/**
* Validate exclude types array
* @param {Array} types - Array of exclude types
* @returns {Array} Validated exclude types
*/
export function validateExcludeTypes(types) {
if (!types || !Array.isArray(types)) {
return [];
}
return types.filter((type) => VALID_EXCLUDE_TYPES.includes(type));
}
/**
* Validate regex pattern
* @param {string} pattern - Regex pattern to validate
* @returns {string|null} Valid pattern or null
*/
export function validateExcludeRegex(pattern) {
if (!pattern || typeof pattern !== "string") {
return;
}
try {
new RegExp(pattern);
return pattern;
} catch {
return;
}
}
/**
* Parse array parameter from request
* Handles both array[] and array[0], array[1] formats
* @param {object} body - Request body
* @param {string} paramName - Parameter name
* @param parameterName
* @returns {Array} Parsed array
*/
export function parseArrayParameter(body, parameterName) {
// Direct array
if (Array.isArray(body[parameterName])) {
return body[parameterName];
}
// Single value
if (body[parameterName]) {
return [body[parameterName]];
}
// Indexed values (param[0], param[1], ...)
const result = [];
let index = 0;
while (body[`${parameterName}[${index}]`] !== undefined) {
result.push(body[`${parameterName}[${index}]`]);
index++;
}
// Array notation (param[])
if (body[`${parameterName}[]`]) {
const values = body[`${parameterName}[]`];
return Array.isArray(values) ? values : [values];
}
return result;
}