UNPKG

@noony-serverless/core

Version:

A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript

318 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.bodyParser = exports.BodyParserMiddleware = void 0; const core_1 = require("../core"); // Enhanced base64 validation with stricter security checks const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/; const MAX_BASE64_PADDING = 2; const MIN_BASE64_LENGTH = 4; // Base64 minimum valid length // Type guard to check if the body is a PubSub message - optimized version const isPubSubMessage = (body) => { return (!!body && typeof body === 'object' && 'message' in body && typeof body.message === 'object' && 'data' in body.message); }; // Performance constants const MAX_JSON_SIZE = 1024 * 1024; // 1MB default limit const MAX_BASE64_SIZE = 1024 * 1024 * 1.5; // 1.5MB for base64 (accounts for encoding overhead) /** * Async JSON parsing using worker threads for large payloads * Falls back to synchronous parsing for small payloads */ const parseJsonAsync = async (jsonString) => { // Performance optimization: Use sync parsing for small payloads if (jsonString.length < 10000) { // 10KB threshold try { return JSON.parse(jsonString); } catch (error) { throw new core_1.ValidationError('Invalid JSON body', error.message); } } // For larger payloads, use async parsing to avoid blocking return new Promise((resolve, reject) => { // Use setImmediate to make JSON parsing non-blocking setImmediate(() => { try { const result = JSON.parse(jsonString); resolve(result); } catch (error) { reject(new core_1.ValidationError('Invalid JSON body', error.message)); } }); }); }; /** * Enhanced base64 validation with comprehensive security checks */ const validateBase64Format = (base64Data) => { // Check minimum length if (base64Data.length < MIN_BASE64_LENGTH) { throw new core_1.ValidationError('Base64 data too short'); } // Validate base64 alphabet and padding if (!BASE64_REGEX.test(base64Data)) { throw new core_1.ValidationError('Invalid base64 format in Pub/Sub message'); } // Validate padding is only at the end const paddingIndex = base64Data.indexOf('='); if (paddingIndex !== -1) { const paddingCount = base64Data.length - paddingIndex; if (paddingCount > MAX_BASE64_PADDING) { throw new core_1.ValidationError('Invalid base64 padding'); } // Ensure no non-padding characters after padding starts const paddingPart = base64Data.substring(paddingIndex); if (!/^=+$/.test(paddingPart)) { throw new core_1.ValidationError('Invalid characters after base64 padding'); } } // Validate length is multiple of 4 (base64 requirement) if (base64Data.length % 4 !== 0) { throw new core_1.ValidationError('Invalid base64 length - must be multiple of 4'); } }; /** * Secure base64 decoding with comprehensive validation and size limits */ const decodeBase64Async = async (base64Data) => { // Perform comprehensive base64 validation validateBase64Format(base64Data); // Check size limits to prevent memory exhaustion if (base64Data.length > MAX_BASE64_SIZE) { throw new core_1.TooLargeError('Pub/Sub message too large'); } // For small messages, use sync decoding if (base64Data.length < 1000) { try { const decoded = Buffer.from(base64Data, 'base64').toString('utf8'); // Validate decoded content is valid UTF-8 if (decoded.includes('\uFFFD')) { throw new core_1.ValidationError('Invalid UTF-8 content in decoded base64'); } return decoded; } catch (error) { if (error instanceof core_1.ValidationError) { throw error; } throw new core_1.ValidationError('Failed to decode base64 data'); } } // For larger messages, use async decoding to avoid blocking return new Promise((resolve, reject) => { setImmediate(() => { try { const decoded = Buffer.from(base64Data, 'base64').toString('utf8'); // Validate decoded content is valid UTF-8 if (decoded.includes('\uFFFD')) { reject(new core_1.ValidationError('Invalid UTF-8 content in decoded base64')); return; } resolve(decoded); } catch (error) { reject(new core_1.ValidationError('Failed to decode base64 Pub/Sub message')); } }); }); }; // Enhanced async body parser with performance optimizations const parseBody = async (body) => { // Early return for already parsed objects if (typeof body === 'object' && body !== null && !isPubSubMessage(body)) { return body; } if (typeof body === 'string') { // Size check to prevent DoS attacks if (body.length > MAX_JSON_SIZE) { throw new core_1.TooLargeError('Request body too large'); } return await parseJsonAsync(body); } if (isPubSubMessage(body)) { try { const decoded = await decodeBase64Async(body.message.data); return await parseJsonAsync(decoded); } catch (error) { if (error instanceof core_1.ValidationError || error instanceof core_1.TooLargeError) { throw error; } throw new core_1.ValidationError('Invalid Pub/Sub message', error.message); } } return body; }; /** * Enhanced BodyParserMiddleware with async parsing and performance optimizations. * * Features: * - Async JSON parsing for large payloads * - Size limits to prevent DoS attacks * - Base64 decoding for Pub/Sub messages * - Non-blocking parsing using setImmediate * * @template TBody - The expected type of the parsed body. Defaults to unknown if not specified. * @template TUser - The type of the authenticated user (preserves type chain) * @implements {BaseMiddleware} * * @example * Basic usage with typed body parsing: * ```typescript * import { Handler, BodyParserMiddleware } from '@noony-serverless/core'; * * interface UserRequest { * name: string; * email: string; * age: number; * } * * const createUserHandler = new Handler() * .use(new BodyParserMiddleware<UserRequest>()) * .handle(async (context) => { * const userData = context.req.parsedBody as UserRequest; * console.log('User data:', userData.name, userData.email); * return { success: true, data: userData }; * }); * ``` * * @example * Custom size limit configuration: * ```typescript * const largeBodyParser = new BodyParserMiddleware<any>(2 * 1024 * 1024); // 2MB limit * * const uploadHandler = new Handler() * .use(largeBodyParser) * .handle(async (context) => { * const uploadData = context.req.parsedBody; * return { success: true, size: JSON.stringify(uploadData).length }; * }); * ``` * * @example * Google Cloud Pub/Sub message handling: * ```typescript * interface PubSubData { * eventType: string; * timestamp: string; * payload: any; * } * * const pubSubHandler = new Handler() * .use(new BodyParserMiddleware<PubSubData>()) * .handle(async (context) => { * // Automatically decodes base64 Pub/Sub message data * const messageData = context.req.parsedBody as PubSubData; * console.log('Event type:', messageData.eventType); * return { success: true, processed: true }; * }); * ``` */ class BodyParserMiddleware { maxSize; constructor(maxSize = MAX_JSON_SIZE) { this.maxSize = maxSize; } async before(context) { // Check content-length early to avoid processing oversized requests const headers = context.req.headers || {}; const contentLength = headers['content-length']; if (contentLength) { const length = Array.isArray(contentLength) ? contentLength[0] : contentLength; if (length && parseInt(length) > this.maxSize) { throw new core_1.TooLargeError('Request body too large'); } } context.req.parsedBody = await parseBody(context.req.body); } } exports.BodyParserMiddleware = BodyParserMiddleware; /** * Enhanced middleware function for parsing the request body in specific HTTP methods. * * Performance optimizations: * - Early method validation * - Async parsing for large payloads * - Size validation * * @template TBody - The expected type of the parsed request body. * @template TUser - The type of the authenticated user (preserves type chain) * @param maxSize - Maximum allowed body size in bytes (default: 1MB) * @returns {BaseMiddleware} A middleware object containing a `before` hook. * * @example * Basic body parsing with default settings: * ```typescript * import { Handler, bodyParser } from '@noony-serverless/core'; * * const apiHandler = new Handler() * .use(bodyParser<{ name: string; email: string }>()) * .handle(async (context) => { * const body = context.req.parsedBody; * return { success: true, received: body }; * }); * ``` * * @example * Custom size limit for large uploads: * ```typescript * const uploadHandler = new Handler() * .use(bodyParser<any>(5 * 1024 * 1024)) // 5MB limit * .handle(async (context) => { * const uploadData = context.req.parsedBody; * console.log('Upload size:', JSON.stringify(uploadData).length); * return { success: true, uploadId: generateId() }; * }); * ``` * * @example * Combining with validation middleware: * ```typescript * import { z } from 'zod'; * import { bodyParser, validationMiddleware } from '@noony-serverless/core'; * * const userSchema = z.object({ * name: z.string().min(1), * email: z.string().email(), * age: z.number().int().min(18) * }); * * const createUserHandler = new Handler() * .use(bodyParser<z.infer<typeof userSchema>>()) * .use(validationMiddleware(userSchema)) * .handle(async (context) => { * const validatedUser = context.req.validatedBody; * return { success: true, user: validatedUser }; * }); * ``` */ const bodyParser = (maxSize = MAX_JSON_SIZE) => ({ before: async (context) => { const { method, body } = context.req; // Performance optimization: Early return for methods that don't typically have bodies if (!method || !['POST', 'PUT', 'PATCH'].includes(method)) { return; } // Check content-length early const headers = context.req.headers || {}; const contentLength = headers['content-length']; if (contentLength) { const length = Array.isArray(contentLength) ? contentLength[0] : contentLength; if (length && parseInt(length) > maxSize) { throw new core_1.TooLargeError('Request body too large'); } } context.req.parsedBody = await parseBody(body); }, }); exports.bodyParser = bodyParser; //# sourceMappingURL=bodyParserMiddleware.js.map