/** * @module validation * Queue validation utilities with constants matching the Catalyst backend. * * These validation functions perform client-side validation before API calls, * providing immediate feedback and reducing unnecessary network requests. */ // ============================================================================ // Validation Constants // ============================================================================ /** Maximum allowed length for queue names. */ export const MAX_QUEUE_NAME_LENGTH = 256; /** Minimum allowed length for queue names. */ export const MIN_QUEUE_NAME_LENGTH = 1; /** Maximum payload size in bytes (1MB). */ export const MAX_PAYLOAD_SIZE = 1048576; /** Maximum description length in characters. */ export const MAX_DESCRIPTION_LENGTH = 1024; /** Maximum number of messages in a single batch operation. */ export const MAX_BATCH_SIZE = 1000; /** Maximum metadata size in bytes (64KB). */ export const MAX_METADATA_SIZE = 65536; /** Maximum partition key length in characters. */ export const MAX_PARTITION_KEY_LENGTH = 256; /** Maximum idempotency key length in characters. */ export const MAX_IDEMPOTENCY_KEY_LENGTH = 256; /** Maximum visibility timeout in seconds (12 hours). */ export const MAX_VISIBILITY_TIMEOUT = 43200; /** Maximum number of retry attempts allowed. */ export const MAX_RETRIES = 100; /** Maximum number of in-flight messages per client. */ export const MAX_IN_FLIGHT = 1000; /** Queue name pattern: starts with letter/underscore, contains lowercase alphanumerics, underscores, hyphens. */ const VALID_QUEUE_NAME_REGEX = /^[a-z_][a-z0-9_-]*$/; /** Message ID pattern: must start with qmsg_ prefix. */ const VALID_MESSAGE_ID_REGEX = /^qmsg_[a-zA-Z0-9]+$/; /** Destination ID pattern: must start with qdest_ prefix. */ const VALID_DESTINATION_ID_REGEX = /^qdest_[a-zA-Z0-9]+$/; /** Source ID pattern: must start with qsrc_ prefix. */ const VALID_SOURCE_ID_REGEX = /^qsrc_[a-zA-Z0-9]+$/; /** Maximum source name length. */ export const MAX_SOURCE_NAME_LENGTH = 256; // ============================================================================ // Validation Error // ============================================================================ // Shared with runtime queue service in core; single definition for platform validation import { QueueValidationError } from '@agentuity/queue'; export { QueueValidationError }; // ============================================================================ // Validation Functions // ============================================================================ /** * Validates a queue name against naming rules. * * Queue names must: * - Be 1-256 characters long * - Start with a lowercase letter or underscore * - Contain only lowercase letters, digits, underscores, and hyphens * * @param name - The queue name to validate * @throws {QueueValidationError} If the name is invalid * * @example * ```typescript * validateQueueName('my_queue'); // OK * validateQueueName('order-queue'); // OK * validateQueueName('Invalid Name!'); // Throws * ``` */ export function validateQueueName(name: string): void { if (!name || name.length < MIN_QUEUE_NAME_LENGTH) { throw new QueueValidationError({ message: 'Queue name cannot be empty', field: 'name', value: name, }); } if (name.length > MAX_QUEUE_NAME_LENGTH) { throw new QueueValidationError({ message: `Queue name must not exceed ${MAX_QUEUE_NAME_LENGTH} characters`, field: 'name', value: name, }); } if (!VALID_QUEUE_NAME_REGEX.test(name)) { throw new QueueValidationError({ message: 'Queue name must start with a letter or underscore and contain only lowercase letters, digits, underscores, and hyphens', field: 'name', value: name, }); } } /** * Validates a queue type. * * @param type - The queue type to validate * @throws {QueueValidationError} If the type is not 'worker' or 'pubsub' */ export function validateQueueType(type: string): void { if (type !== 'worker' && type !== 'pubsub') { throw new QueueValidationError({ message: "Queue type must be 'worker' or 'pubsub'", field: 'queue_type', value: type, }); } } /** * Validates a message payload. * * Payloads must be non-empty JSON and not exceed 1MB when serialized. * * @param payload - The payload to validate (must be JSON-serializable) * @throws {QueueValidationError} If the payload is empty or too large */ export function validatePayload(payload: unknown): void { if (payload === undefined || payload === null) { throw new QueueValidationError({ message: 'Payload cannot be empty', field: 'payload', }); } const serialized = JSON.stringify(payload); const payloadBytes = new TextEncoder().encode(serialized).length; if (payloadBytes > MAX_PAYLOAD_SIZE) { throw new QueueValidationError({ message: `Payload size exceeds ${MAX_PAYLOAD_SIZE} byte limit (${payloadBytes} bytes)`, field: 'payload', value: payloadBytes, }); } } /** * Validates a message ID format. * * Message IDs must start with the `qmsg_` prefix. * * @param id - The message ID to validate * @throws {QueueValidationError} If the ID format is invalid */ export function validateMessageId(id: string): void { if (!id || !VALID_MESSAGE_ID_REGEX.test(id)) { throw new QueueValidationError({ message: 'Invalid message ID format (must start with qmsg_ prefix)', field: 'message_id', value: id, }); } } /** * Validates a destination ID format. * * Destination IDs must start with the `qdest_` prefix. * * @param id - The destination ID to validate * @throws {QueueValidationError} If the ID format is invalid */ export function validateDestinationId(id: string): void { if (!id || !VALID_DESTINATION_ID_REGEX.test(id)) { throw new QueueValidationError({ message: 'Invalid destination ID format (must start with qdest_ prefix)', field: 'destination_id', value: id, }); } } /** * Validates a queue or message description. * * @param description - The description to validate (optional) * @throws {QueueValidationError} If the description exceeds the maximum length */ export function validateDescription(description?: string): void { if (description && description.length > MAX_DESCRIPTION_LENGTH) { throw new QueueValidationError({ message: `Description must not exceed ${MAX_DESCRIPTION_LENGTH} characters`, field: 'description', value: description.length, }); } } /** * Validates a partition key length. * * Partition keys are used for message ordering within a queue. * * @param key - The partition key to validate (optional) * @throws {QueueValidationError} If the key exceeds the maximum length */ export function validatePartitionKey(key?: string): void { if (key && key.length > MAX_PARTITION_KEY_LENGTH) { throw new QueueValidationError({ message: `Partition key must not exceed ${MAX_PARTITION_KEY_LENGTH} characters`, field: 'partition_key', value: key.length, }); } } /** * Validates an idempotency key length. * * Idempotency keys are used to prevent duplicate message processing. * * @param key - The idempotency key to validate (optional) * @throws {QueueValidationError} If the key exceeds the maximum length */ export function validateIdempotencyKey(key?: string): void { if (key && key.length > MAX_IDEMPOTENCY_KEY_LENGTH) { throw new QueueValidationError({ message: `Idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`, field: 'idempotency_key', value: key.length, }); } } /** * Validates a time-to-live value. * * TTL specifies how long a message should be kept before expiring. * * @param ttl - The TTL in seconds to validate (optional) * @throws {QueueValidationError} If the TTL is negative */ export function validateTTL(ttl?: number): void { if (ttl !== undefined && ttl < 0) { throw new QueueValidationError({ message: 'TTL cannot be negative', field: 'ttl', value: ttl, }); } } /** * Validates a visibility timeout. * * Visibility timeout is how long a message is hidden after being received, * giving the consumer time to process it before it becomes visible again. * * @param timeout - The timeout in seconds to validate (optional) * @throws {QueueValidationError} If the timeout is out of valid range (1-43200 seconds) */ export function validateVisibilityTimeout(timeout?: number): void { if (timeout !== undefined) { if (timeout < 1) { throw new QueueValidationError({ message: 'Visibility timeout must be at least 1 second', field: 'visibility_timeout', value: timeout, }); } if (timeout > MAX_VISIBILITY_TIMEOUT) { throw new QueueValidationError({ message: `Visibility timeout must not exceed ${MAX_VISIBILITY_TIMEOUT} seconds (12 hours)`, field: 'visibility_timeout', value: timeout, }); } } } /** * Validates the maximum retry count. * * @param retries - The max retries value to validate (optional) * @throws {QueueValidationError} If retries is negative or exceeds maximum */ export function validateMaxRetries(retries?: number): void { if (retries !== undefined) { if (retries < 0) { throw new QueueValidationError({ message: 'Max retries cannot be negative', field: 'max_retries', value: retries, }); } if (retries > MAX_RETRIES) { throw new QueueValidationError({ message: `Max retries must not exceed ${MAX_RETRIES}`, field: 'max_retries', value: retries, }); } } } /** * Validates the maximum in-flight messages per client. * * This controls how many messages a single consumer can process concurrently. * * @param maxInFlight - The max in-flight value to validate (optional) * @throws {QueueValidationError} If the value is out of valid range (1-1000) */ export function validateMaxInFlight(maxInFlight?: number): void { if (maxInFlight !== undefined) { if (maxInFlight < 1) { throw new QueueValidationError({ message: 'Max in-flight per client must be at least 1', field: 'max_in_flight', value: maxInFlight, }); } if (maxInFlight > MAX_IN_FLIGHT) { throw new QueueValidationError({ message: `Max in-flight per client must not exceed ${MAX_IN_FLIGHT}`, field: 'max_in_flight', value: maxInFlight, }); } } } /** * Validates a message offset. * * Offsets are sequential positions in the queue, starting from 0. * * @param offset - The offset value to validate * @throws {QueueValidationError} If the offset is negative */ export function validateOffset(offset: number): void { if (offset < 0) { throw new QueueValidationError({ message: 'Offset cannot be negative', field: 'offset', value: offset, }); } } /** * Validates a limit for list/consume operations. * * @param limit - The limit value to validate * @throws {QueueValidationError} If the limit is less than 1 or exceeds maximum */ export function validateLimit(limit: number): void { if (limit < 1) { throw new QueueValidationError({ message: 'Limit must be at least 1', field: 'limit', value: limit, }); } if (limit > MAX_BATCH_SIZE) { throw new QueueValidationError({ message: `Limit must not exceed ${MAX_BATCH_SIZE}`, field: 'limit', value: limit, }); } } /** * Validates a batch size for batch operations. * * @param size - The batch size to validate * @throws {QueueValidationError} If the size is less than 1 or exceeds maximum */ export function validateBatchSize(size: number): void { if (size <= 0) { throw new QueueValidationError({ message: 'Batch size must be greater than 0', field: 'batch_size', value: size, }); } if (size > MAX_BATCH_SIZE) { throw new QueueValidationError({ message: `Batch size must not exceed ${MAX_BATCH_SIZE}`, field: 'batch_size', value: size, }); } } /** * Validates a webhook URL for destinations. * * URLs must use HTTP or HTTPS protocol. * * @param url - The URL to validate * @throws {QueueValidationError} If the URL is missing or not HTTP/HTTPS */ export function validateWebhookUrl(url: string): void { if (!url) { throw new QueueValidationError({ message: 'Webhook URL is required', field: 'url', }); } if (!url.startsWith('http://') && !url.startsWith('https://')) { throw new QueueValidationError({ message: 'Webhook URL must be a valid HTTP or HTTPS URL', field: 'url', value: url, }); } } /** * Validates a destination configuration object based on the destination type. * * @param destinationType - The type of destination (http, url, webhook, queue, sandbox, email) * @param config - The destination config object to validate * @throws {QueueValidationError} If the config is invalid */ export function validateDestinationConfig( destinationType: string, config: Record ): void { if (!config) { throw new QueueValidationError({ message: 'Destination config is required', field: 'config', }); } switch (destinationType) { case 'http': case 'url': case 'webhook': { const url = config.url; if (typeof url !== 'string' || !url) { throw new QueueValidationError({ message: 'config.url is required for http/url/webhook destinations', field: 'config.url', }); } validateWebhookUrl(url); const method = config.method; if (method !== undefined) { if (method !== 'POST' && method !== 'PUT' && method !== 'PATCH') { throw new QueueValidationError({ message: 'config.method must be POST, PUT, or PATCH', field: 'config.method', value: method, }); } } const timeoutMs = config.timeout_ms; if (timeoutMs !== undefined) { if (typeof timeoutMs !== 'number' || timeoutMs < 1000 || timeoutMs > 300000) { throw new QueueValidationError({ message: 'config.timeout_ms must be between 1000 and 300000', field: 'config.timeout_ms', value: timeoutMs, }); } } break; } case 'queue': { const queueId = config.queue_id; if (typeof queueId !== 'string' || !queueId) { throw new QueueValidationError({ message: 'config.queue_id is required for queue destinations', field: 'config.queue_id', }); } break; } case 'sandbox': { const sandboxId = config.sandbox_id; if (typeof sandboxId !== 'string' || !sandboxId) { throw new QueueValidationError({ message: 'config.sandbox_id is required for sandbox destinations', field: 'config.sandbox_id', }); } break; } case 'email': { const emailAddress = config.email_address; if (typeof emailAddress !== 'string' || !emailAddress) { throw new QueueValidationError({ message: 'config.email_address is required for email destinations', field: 'config.email_address', }); } break; } default: throw new QueueValidationError({ message: `Unknown destination type: ${destinationType}`, field: 'destination_type', value: destinationType, }); } } /** * Validates a source ID format. * * Source IDs must start with the `qsrc_` prefix. * * @param id - The source ID to validate * @throws {QueueValidationError} If the ID format is invalid * * @example * ```typescript * validateSourceId('qsrc_abc123'); // OK * validateSourceId('invalid'); // Throws * ``` */ export function validateSourceId(id: string): void { if (!id || typeof id !== 'string') { throw new QueueValidationError({ field: 'source_id', value: id, message: 'Source ID is required', }); } if (!VALID_SOURCE_ID_REGEX.test(id)) { throw new QueueValidationError({ field: 'source_id', value: id, message: 'Source ID must start with "qsrc_" prefix and contain only alphanumeric characters', }); } } /** * Validates a source name. * * Source names must be non-empty and not exceed the maximum length. * * @param name - The source name to validate * @throws {QueueValidationError} If the name is invalid * * @example * ```typescript * validateSourceName('my-source'); // OK * validateSourceName(''); // Throws * ``` */ export function validateSourceName(name: string): void { if (!name || typeof name !== 'string') { throw new QueueValidationError({ field: 'source_name', value: name, message: 'Source name is required', }); } if (name.length > MAX_SOURCE_NAME_LENGTH) { throw new QueueValidationError({ field: 'source_name', value: name, message: `Source name must be at most ${MAX_SOURCE_NAME_LENGTH} characters`, }); } }