From 15036d34c2246bb72515e58fd423460ed446fb06 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Fri, 23 Jan 2026 09:53:21 +0100 Subject: [PATCH] Prepare for version 1.2.0 --- package.json | 2 +- server.js | 383 ++++++++++++++++++++++--- src/App.js | 532 ++++++++--------------------------- src/App.test.js | 60 ++-- src/components/ApiKeyPage.js | 102 +++++++ src/components/Footer.js | 28 ++ src/components/Header.js | 73 +++++ src/components/MainPage.js | 299 ++++++++++++++++++++ src/setupTests.js | 13 + 9 files changed, 1028 insertions(+), 464 deletions(-) create mode 100644 src/components/ApiKeyPage.js create mode 100644 src/components/Footer.js create mode 100644 src/components/Header.js create mode 100644 src/components/MainPage.js diff --git a/package.json b/package.json index 18a313f..480d6c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jmespath-playground", - "version": "1.1.7", + "version": "1.2.0", "description": "A React-based web application for testing JMESPath expressions against JSON data", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 6822478..fc30466 100644 --- a/server.js +++ b/server.js @@ -1,37 +1,171 @@ const express = require('express'); const path = require('path'); +const crypto = require('crypto'); const { v4: uuidv4 } = require('uuid'); +// Environment configuration +const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100; +const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; // 1MB +const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour + +// Utility functions for encryption +function generateSalt() { + return crypto.randomBytes(16); +} + +function isLocalhostRequest(req) { + // Get client IP with fallback options + const forwarded = req.get('X-Forwarded-For'); + const ip = forwarded ? forwarded.split(',')[0].trim() : + req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + '127.0.0.1'; + + const host = req.get('host') || ''; + + // Check for localhost IP addresses (IPv4 and IPv6) + const localhostIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost']; + const isLocalIP = localhostIPs.includes(ip) || ip.startsWith('127.') || ip === '::1'; + + // Check for localhost hostnames + const isLocalHost = host.startsWith('localhost:') || host.startsWith('127.0.0.1:') || host === 'localhost' || host === '127.0.0.1'; + + return isLocalIP || isLocalHost; +} + +function encrypt(data, key) { + try { + const algorithm = 'aes-256-gcm'; + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, iv); + cipher.setAAD(Buffer.from('session-data')); + + let encrypted = cipher.update(JSON.stringify(data), 'utf8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + + const authTag = cipher.getAuthTag(); + + return { + iv: iv.toString('hex'), + data: encrypted.toString('hex'), + tag: authTag.toString('hex') + }; + } catch (error) { + console.error('โš ๏ธ Encryption exception:', { + message: error.message, + algorithm: 'aes-256-gcm', + keyLength: key ? key.length : 'undefined', + timestamp: new Date().toISOString() + }); + throw new Error(`Encryption failed: ${error.message}`); + } +} + +function decrypt(encryptedObj, key) { + try { + const algorithm = 'aes-256-gcm'; + const iv = Buffer.from(encryptedObj.iv, 'hex'); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + decipher.setAAD(Buffer.from('session-data')); + decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex')); + + let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8'); + decrypted += decipher.final('utf8'); + + return JSON.parse(decrypted); + } catch (error) { + console.error('โš ๏ธ Decryption exception:', { + message: error.message, + algorithm: 'aes-256-gcm', + keyLength: key ? key.length : 'undefined', + hasIV: !!encryptedObj.iv, + hasTag: !!encryptedObj.tag, + hasData: !!encryptedObj.data, + timestamp: new Date().toISOString() + }); + throw new Error(`Decryption failed: ${error.message}`); + } +} + +// For localhost requests, use a consistent API key so sessions persist +const LOCALHOST_API_KEY = 'localhost0123456789abcdef0123456789'; + +function isValidApiKey(apiKey) { + return typeof apiKey === 'string' && /^[0-9a-f]{32}$/i.test(apiKey); +} + +function getSessionId(apiKey) { + return crypto.createHash('sha256').update(apiKey).digest('hex'); +} + +function generateSalt() { + return crypto.randomBytes(32); +} + +function deriveKey(apiKey, salt) { + return crypto.pbkdf2Sync(apiKey, salt, 10000, 32, 'sha256'); +} + // Create Express app function createApp() { const app = express(); + // Trust proxy to get real client IP (needed for localhost detection) + app.set('trust proxy', true); + // Middleware - app.use(express.json()); + app.use(express.json({ limit: MAX_SAMPLE_SIZE })); app.use(express.static(path.join(__dirname, 'build'))); - // In-memory storage - let sampleData = { - "people": [ - { - "name": "John Doe", - "age": 30, - "city": "New York" - }, - { - "name": "Jane Smith", - "age": 25, - "city": "Los Angeles" - } - ], - "total": 2 - }; + // Session storage + const sessions = new Map(); - let stateGuid = uuidv4(); + // Cleanup expired sessions + function cleanupExpiredSessions() { + const now = Date.now(); + for (const [sessionId, session] of sessions.entries()) { + if (now - session.createdAt > MAX_SESSION_TTL) { + sessions.delete(sessionId); + console.log(`๐Ÿงน Cleaned up expired session: ${sessionId.substring(0, 8)}...`); + } + } + } + + // Run cleanup every 5 minutes + setInterval(cleanupExpiredSessions, 5 * 60 * 1000); // API endpoints app.post('/api/v1/upload', (req, res) => { try { + // Check if request is from localhost - if so, skip API key validation + const isFromLocalhost = isLocalhostRequest(req); + let apiKey = req.headers['x-api-key']; + + if (!isFromLocalhost) { + // Validate API key header for remote clients + if (!apiKey || !isValidApiKey(apiKey)) { + return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); + } + } else { + // For localhost requests, use consistent API key for session persistence + if (!apiKey || !isValidApiKey(apiKey)) { + apiKey = LOCALHOST_API_KEY; + } + } + + // Cleanup expired sessions before checking limits + cleanupExpiredSessions(); + + // Check session limits + if (sessions.size >= MAX_SESSIONS) { + return res.status(429).json({ + error: 'Maximum number of sessions reached. Please try again later.', + maxSessions: MAX_SESSIONS, + currentSessions: sessions.size + }); + } + const uploadedData = req.body; // Validate that it's valid JSON @@ -39,38 +173,209 @@ function createApp() { return res.status(400).json({ error: 'Invalid JSON data' }); } - // Store the sample data and generate new state GUID - sampleData = uploadedData; - stateGuid = uuidv4(); + // Check data size + const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8'); + if (dataSize > MAX_SAMPLE_SIZE) { + return res.status(413).json({ + error: 'Sample data too large', + maxSize: MAX_SAMPLE_SIZE, + receivedSize: dataSize + }); + } - res.json({ message: 'Sample data uploaded successfully', state: stateGuid }); + const sessionId = getSessionId(apiKey); + const salt = generateSalt(); + const key = deriveKey(apiKey, salt); + const stateGuid = uuidv4(); + + // Encrypt and store session data + const encryptedData = encrypt(uploadedData, key); + + sessions.set(sessionId, { + salt: salt.toString('hex'), + encryptedData, + state: stateGuid, + createdAt: Date.now(), + accessed: false + }); + + console.log(`๐Ÿ“ Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`); + + res.json({ + message: 'Sample data uploaded successfully', + state: stateGuid, + sessionId: sessionId.substring(0, 8) + '...' + }); } catch (error) { - res.status(500).json({ error: 'Failed to upload sample data' }); + console.error('โš ๏ธ Upload endpoint exception occurred:', { + message: error.message, + stack: error.stack, + sessionCount: sessions.size, + timestamp: new Date().toISOString() + }); + + // Provide more specific error messages based on error type + if (error.name === 'SyntaxError') { + return res.status(400).json({ + error: 'Invalid JSON data format', + details: 'The uploaded data could not be parsed as valid JSON' + }); + } else if (error.message.includes('encrypt')) { + return res.status(500).json({ + error: 'Encryption failed', + details: 'Failed to encrypt session data. Please try again with a new API key.' + }); + } else if (error.message.includes('PBKDF2')) { + return res.status(500).json({ + error: 'Key derivation failed', + details: 'Failed to derive encryption key from API key' + }); + } else { + return res.status(500).json({ + error: 'Upload processing failed', + details: 'An unexpected error occurred while processing your upload. Please try again.' + }); + } } }); app.get('/api/v1/sample', (req, res) => { try { - const dataToReturn = sampleData; + // Check if request is from localhost - if so, skip API key validation + const isFromLocalhost = isLocalhostRequest(req); + let apiKey = req.headers['x-api-key']; + + if (!isFromLocalhost) { + // Validate API key header for remote clients + if (!apiKey || !isValidApiKey(apiKey)) { + return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); + } + } else { + // For localhost requests, use consistent API key for session persistence + if (!apiKey || !isValidApiKey(apiKey)) { + apiKey = LOCALHOST_API_KEY; + } + } - // Security: Clear the sample data after it's retrieved (one-time use) - sampleData = null; - console.log('๐Ÿ“ค Sample data retrieved and cleared from server memory'); + const sessionId = getSessionId(apiKey); + const session = sessions.get(sessionId); - res.json(dataToReturn); + if (!session) { + return res.json(null); + } + + // Decrypt data + const salt = Buffer.from(session.salt, 'hex'); + const key = deriveKey(apiKey, salt); + const decryptedData = decrypt(session.encryptedData, key); + + // Remove session after first access (one-time use) + sessions.delete(sessionId); + console.log(`๐Ÿ“ค Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`); + + res.json(decryptedData); } catch (error) { - res.status(500).json({ error: 'Failed to retrieve sample data' }); + console.error('โš ๏ธ Sample retrieval exception occurred:', { + message: error.message, + stack: error.stack, + sessionCount: sessions.size, + timestamp: new Date().toISOString() + }); + + // Provide more specific error messages based on error type + if (error.message.includes('decrypt')) { + return res.status(500).json({ + error: 'Decryption failed', + details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.' + }); + } else if (error.message.includes('JSON')) { + return res.status(500).json({ + error: 'Data corruption detected', + details: 'The stored session data appears to be corrupted and cannot be parsed.' + }); + } else if (error.name === 'TypeError') { + return res.status(500).json({ + error: 'Session data format error', + details: 'The session data format is invalid or corrupted.' + }); + } else { + return res.status(500).json({ + error: 'Sample retrieval failed', + details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.' + }); + } } }); app.get('/api/v1/state', (req, res) => { try { - res.json({ state: stateGuid }); + // Check if request is from localhost - if so, skip API key validation + const isFromLocalhost = isLocalhostRequest(req); + let apiKey = req.headers['x-api-key']; + + if (!isFromLocalhost) { + // Validate API key header for remote clients + if (!apiKey || !isValidApiKey(apiKey)) { + return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); + } + } else { + // For localhost requests, use consistent API key for session persistence + if (!apiKey || !isValidApiKey(apiKey)) { + apiKey = LOCALHOST_API_KEY; + } + } + + const sessionId = getSessionId(apiKey); + const session = sessions.get(sessionId); + + if (!session) { + // Return null state when no session exists + return res.json({ state: null }); + } + + res.json({ state: session.state }); } catch (error) { - res.status(500).json({ error: 'Failed to retrieve state' }); + console.error('โš ๏ธ State retrieval exception occurred:', { + message: error.message, + stack: error.stack, + sessionCount: sessions.size, + timestamp: new Date().toISOString() + }); + + // Provide more specific error messages + if (error.message.includes('API key')) { + return res.status(403).json({ + error: 'API key processing failed', + details: 'Failed to process the provided API key' + }); + } else { + return res.status(500).json({ + error: 'State retrieval failed', + details: 'An unexpected error occurred while retrieving session state. Please try again.' + }); + } } }); + // Health endpoint (no auth required) + app.get('/api/v1/health', (req, res) => { + cleanupExpiredSessions(); // Cleanup on health check + res.json({ + status: 'healthy', + sessions: { + current: sessions.size, + max: MAX_SESSIONS, + available: MAX_SESSIONS - sessions.size + }, + limits: { + maxSessions: MAX_SESSIONS, + maxSampleSize: MAX_SAMPLE_SIZE, + maxSessionTTL: MAX_SESSION_TTL + }, + uptime: process.uptime() + }); + }); + // Serve React app for all other routes app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'build', 'index.html')); @@ -101,11 +406,17 @@ if (require.main === module) { const HOST = listenAddr; app.listen(PORT, HOST, () => { - console.log(`Server running on http://${HOST}:${PORT}`); - console.log(`API endpoints:`); - console.log(` POST http://${HOST}:${PORT}/api/v1/upload`); - console.log(` GET http://${HOST}:${PORT}/api/v1/sample`); - console.log(` GET http://${HOST}:${PORT}/api/v1/state`); + console.log(`๐Ÿš€ JMESPath Playground Server running on http://${HOST}:${PORT}`); + console.log(`๐Ÿ“Š Configuration:`); + console.log(` Max Sessions: ${MAX_SESSIONS}`); + console.log(` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`); + console.log(` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`); + console.log(`๐Ÿ”— API endpoints:`); + console.log(` POST http://${HOST}:${PORT}/api/v1/upload (requires X-API-Key)`); + console.log(` GET http://${HOST}:${PORT}/api/v1/sample (requires X-API-Key)`); + console.log(` GET http://${HOST}:${PORT}/api/v1/state (requires X-API-Key)`); + console.log(` GET http://${HOST}:${PORT}/api/v1/health (public)`); + console.log(`๐Ÿ” Security: AES-256-GCM encryption with PBKDF2 key derivation`); }); } diff --git a/src/App.js b/src/App.js index 1eee32b..952286f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,35 +1,47 @@ import React, { useState, useEffect } from 'react'; -import jmespath from 'jmespath'; -import { VERSION } from './version'; +import Header from './components/Header'; +import Footer from './components/Footer'; +import MainPage from './components/MainPage'; +import ApiKeyPage from './components/ApiKeyPage'; import './App.css'; +// Utility function to generate a cryptographically secure API key +function generateApiKey() { + const array = new Uint8Array(16); + + // Use crypto.getRandomValues if available (browser), fallback for tests + if (typeof crypto !== 'undefined' && crypto.getRandomValues) { + crypto.getRandomValues(array); + } else { + // Fallback for test environments - not cryptographically secure + for (let i = 0; i < array.length; i++) { + array[i] = Math.floor(Math.random() * 256); + } + } + + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); +} + // JMESPath Testing Tool - Main Application Component function App() { - const [jmespathExpression, setJmespathExpression] = useState('people[0].name'); + const [currentPage, setCurrentPage] = useState('main'); // 'main' or 'apikey' const [theme, setTheme] = useState(() => { // Load theme from localStorage or default to 'auto' return localStorage.getItem('theme') || 'auto'; }); - const [jsonData, setJsonData] = useState(`{ - "people": [ - { - "name": "John Doe", - "age": 30, - "city": "New York" - }, - { - "name": "Jane Smith", - "age": 25, - "city": "Los Angeles" - } - ], - "total": 2 -}`); - const [result, setResult] = useState(''); - const [error, setError] = useState(''); - const [jsonError, setJsonError] = useState(''); const [showReloadButton, setShowReloadButton] = useState(false); const [currentStateGuid, setCurrentStateGuid] = useState(null); + const [sampleData, setSampleData] = useState(null); + const [apiKey, setApiKey] = useState(() => { + // Load API key from localStorage or generate new one + const stored = localStorage.getItem('jmespath-api-key'); + if (stored && /^[0-9a-f]{32}$/i.test(stored)) { + return stored; + } + const newKey = generateApiKey(); + localStorage.setItem('jmespath-api-key', newKey); + return newKey; + }); // Theme management useEffect(() => { @@ -51,63 +63,82 @@ function App() { }; applyTheme(theme); + + // Save theme preference localStorage.setItem('theme', theme); }, [theme]); - // API polling for state changes and initial sample data load - useEffect(() => { - // Initial load: get both state and sample data - const loadInitialData = async () => { - try { - // Load sample data first - const sampleResponse = await fetch('/api/v1/sample'); - if (sampleResponse.ok) { - const sampleData = await sampleResponse.json(); - setJsonData(JSON.stringify(sampleData, null, 2)); - } + // Check if we're running on localhost + const isRunningOnLocalhost = () => { + const hostname = window.location.hostname; + return hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname.startsWith('127.') || + hostname === '::1'; + }; - // Then load state GUID - const stateResponse = await fetch('/api/v1/state'); - if (stateResponse.ok) { - const stateData = await stateResponse.json(); - setCurrentStateGuid(stateData.state); - } - } catch (error) { - console.debug('API not available:', error); - } + // Get headers for API requests (omit API key for localhost) + const getApiHeaders = () => { + const headers = { + 'Accept': 'application/json' }; + + // Only send API key for non-localhost requests + if (!isRunningOnLocalhost()) { + headers['X-API-Key'] = apiKey; + } + + return headers; + }; - loadInitialData(); - - // Poll for state changes every 3 seconds - const interval = setInterval(async () => { - try { - const response = await fetch('/api/v1/state'); - if (response.ok) { - const data = await response.json(); - if (currentStateGuid && data.state !== currentStateGuid) { - setShowReloadButton(true); - } - } - } catch (error) { - console.debug('API not available:', error); - } - }, 3000); + // Load sample data from API on startup and setup periodic state checking + useEffect(() => { + loadSampleData(); + // Check for state changes every 5 seconds + const interval = setInterval(checkStateChange, 5000); return () => clearInterval(interval); - }, [currentStateGuid]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [apiKey]); + + // Check if state has changed (new data uploaded) + const checkStateChange = async () => { + try { + const response = await fetch('/api/v1/state', { + headers: getApiHeaders() + }); + + if (response.ok) { + const stateData = await response.json(); + if (stateData.state && stateData.state !== currentStateGuid) { + setShowReloadButton(true); + } + } + } catch (error) { + // Silently handle state check errors + console.log('State check failed:', error); + } + }; // Load sample data from API const loadSampleData = async () => { try { setShowReloadButton(false); - const response = await fetch('/api/v1/sample'); + const response = await fetch('/api/v1/sample', { + headers: getApiHeaders() + }); + if (response.ok) { const data = await response.json(); - setJsonData(JSON.stringify(data, null, 2)); + if (data) { + setSampleData(data); + console.log('Sample data loaded:', data); + } // Update current state GUID - const stateResponse = await fetch('/api/v1/state'); + const stateResponse = await fetch('/api/v1/state', { + headers: getApiHeaders() + }); if (stateResponse.ok) { const stateData = await stateResponse.json(); setCurrentStateGuid(stateData.state); @@ -118,367 +149,50 @@ function App() { } }; + // Regenerate API key + const regenerateApiKey = () => { + const newKey = generateApiKey(); + setApiKey(newKey); + localStorage.setItem('jmespath-api-key', newKey); + setShowReloadButton(false); + setCurrentStateGuid(null); + }; + const handleThemeChange = (newTheme) => { setTheme(newTheme); }; - const evaluateExpression = () => { - try { - // Clear previous errors - setError(''); - setJsonError(''); - - // Validate and parse JSON - let parsedData; - try { - parsedData = JSON.parse(jsonData); - } catch (jsonErr) { - setJsonError(`Invalid JSON: ${jsonErr.message}`); - setResult(''); - return; - } - - // Evaluate JMESPath expression - const queryResult = jmespath.search(parsedData, jmespathExpression); - - // Format the result - if (queryResult === null || queryResult === undefined) { - setResult('null'); - } else { - setResult(JSON.stringify(queryResult, null, 2)); - } - } catch (jmesErr) { - setError(`JMESPath Error: ${jmesErr.message}`); - setResult(''); - } - }; - - // Auto-evaluate when inputs change - useEffect(() => { - if (jmespathExpression && jsonData) { - evaluateExpression(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [jmespathExpression, jsonData]); - - const handleJmespathChange = (e) => { - setJmespathExpression(e.target.value); - }; - - const handleJsonChange = (e) => { - setJsonData(e.target.value); - }; - - const formatJson = () => { - try { - const parsed = JSON.parse(jsonData); - setJsonData(JSON.stringify(parsed, null, 2)); - } catch (err) { - // If JSON is invalid, don't format - } - }; - - const clearAll = () => { - setJmespathExpression(''); - setJsonData(''); - setResult(''); - setError(''); - setJsonError(''); - }; - - const loadSample = () => { - setJmespathExpression('people[*].name'); - setJsonData(`{ - "people": [ - { - "name": "Alice Johnson", - "age": 28, - "city": "Chicago", - "skills": ["JavaScript", "React", "Node.js"] - }, - { - "name": "Bob Wilson", - "age": 35, - "city": "Seattle", - "skills": ["Python", "Django", "PostgreSQL"] - }, - { - "name": "Carol Davis", - "age": 32, - "city": "Austin", - "skills": ["Java", "Spring", "MySQL"] - } - ], - "total": 3, - "department": "Engineering" -}`); - }; - - const loadFromDisk = () => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.json'; - fileInput.onchange = (event) => { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const content = e.target.result; - // Handle .json files as regular JSON - JSON.parse(content); // Validate JSON - setJsonData(content); - setJsonError(''); - } catch (err) { - setJsonError(`Invalid JSON file: ${err.message}`); - } - }; - reader.readAsText(file); - } - }; - fileInput.click(); - }; - - const loadLogFile = () => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.log'; - fileInput.onchange = (event) => { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const content = e.target.result; - const lines = content.split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - - const jsonObjects = []; - for (const line of lines) { - try { - const obj = JSON.parse(line); - jsonObjects.push(obj); - } catch (lineError) { - throw new Error(`Invalid JSON on line: "${line.substring(0, 50)}..." - ${lineError.message}`); - } - } - - const jsonContent = JSON.stringify(jsonObjects, null, 2); - setJsonData(jsonContent); - setJsonError(''); - } catch (err) { - setJsonError(`Invalid log file: ${err.message}`); - } - }; - reader.readAsText(file); - } - }; - fileInput.click(); + const handlePageChange = (newPage) => { + setCurrentPage(newPage); }; return (
- {/* Top Section: Title only */} -
-
-
-
-

JMESPath Testing Tool

- {/* Theme switcher */} -
-
- - - -
-
-
-
-
-
+
{/* Main Content Section - flex-grow to fill space */}
- {/* Description paragraph */} -
-
-

- Validate and test JMESPath expressions against JSON data in real-time. - Enter your JMESPath query and JSON data below to see the results instantly. -

-
-
- - {/* Middle Section: JMESPath Expression Input */} -
-
-
-
-
- - JMESPath Expression -
-
- - - - - -
-
-
- -
- {error || 'Expression is correct'} - {showReloadButton && ( - - )} -
-
-
-
-
- - {/* Lower Middle Sections: JSON Data (left) and Query Result (right) */} -
- {/* Lower Middle Left Section: JSON Data Input */} -
-
-
-
- - JSON Data -
-
-
-
-