const PUBLIC_RUNTIME_ENTITY_FIELDS = { projects: [ 'id', 'name', 'slug', 'description', 'logo_url', 'favicon_url', 'og_image_url', ], tour_pages: [ 'id', 'projectId', 'environment', 'source_key', 'name', 'slug', 'sort_order', 'background_image_url', 'background_video_url', 'background_audio_url', 'background_loop', 'requires_auth', 'ui_schema_json', ], project_audio_tracks: [ 'id', 'projectId', 'environment', 'source_key', 'name', 'slug', 'url', 'loop', 'volume', 'sort_order', 'is_enabled', ], global_transition_defaults: [ 'id', 'transition_type', 'duration_ms', 'easing', 'overlay_color', ], project_transition_settings: [ 'id', 'projectId', 'environment', 'transition_type', 'duration_ms', 'easing', 'overlay_color', ], }; // Entity-aware path patterns for public runtime access // Entities not listed here default to allowing only '/' const PUBLIC_RUNTIME_ALLOWED_PATHS = { project_transition_settings: [ '/', /^\/project\/[a-fA-F0-9-]+\/env\/(dev|stage|production)$/, ], }; const pickFields = (record, fields) => { if (!record || typeof record !== 'object') { return record; } // Convert Sequelize instance to plain object if needed const plainRecord = typeof record.get === 'function' ? record.get({ plain: true }) : record; return fields.reduce((acc, field) => { if (field in plainRecord && plainRecord[field] !== undefined) { acc[field] = plainRecord[field]; } return acc; }, {}); }; const isPublicRuntimeReadRequest = (req) => { return req.isRuntimePublicRequest === true && req.method === 'GET'; }; const blockNonPublicRuntimeListEndpoints = (entityName) => (req, res, next) => { if (!isPublicRuntimeReadRequest(req)) { return next(); } const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/']; const pathMatches = allowedPaths.some((pattern) => pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern, ); if (!pathMatches) { return res.status(404).send({ message: 'Not found' }); } if (req.query.filetype === 'csv') { return res.status(404).send({ message: 'Not found' }); } return next(); }; const sanitizePublicRuntimeListResponse = (entityName) => { const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] || []; const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/']; return (req, res, next) => { const pathMatches = allowedPaths.some((pattern) => pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern, ); if ( !isPublicRuntimeReadRequest(req) || !pathMatches || fields.length === 0 ) { return next(); } const originalSend = res.send.bind(res); res.send = (body) => { if (!body || typeof body !== 'object') { return originalSend(body); } // Handle list responses with rows array if (Array.isArray(body.rows)) { const sanitizedRows = body.rows.map((row) => pickFields(row, fields)); return originalSend({ ...body, rows: sanitizedRows, }); } // Handle single object responses (e.g., from findOne or project/:id/env/:env) if (!Array.isArray(body) && body !== null) { return originalSend(pickFields(body, fields)); } return originalSend(body); }; return next(); }; }; module.exports = { blockNonPublicRuntimeListEndpoints, sanitizePublicRuntimeListResponse, };