@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
318 lines • 11.4 kB
JavaScript
;
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