Phase 1 hardened the runtime. Phase 2 surfaced the intelligence. Phase 3 tackles the network reality: CORS walls break direct API inspection, and naive string diffing floods you with false positives from timestamps, request IDs, and key reordering.
In Phase 1 we shipped Web Worker offloading, IndexedDB persistence, and responsive layout. In Phase 2 we elevated quality signals and built a JSON Schema error inspector. Now Phase 3 solves the two hardest network-layer problems for a browser-based API workbench:
- CORS Proxy & Webhook Relay: When an endpoint refuses browser requests, the visualizer transparently falls back to a configurable proxy or webhook—no code changes, no manual copy-paste.
- Smart Structural Diffing: Replaces string/size comparison with semantic key-value delta parsing that ignores timestamps, request IDs, nonces, and key reordering—so you see real data changes, not noise.
Both upgrades are live at webdevelopmentor.com/json-nexus/ and in every share link (/s/{token}/).
Upgrade 1: CORS Proxy & Webhook Relay
The Problem
JSON Nexus Visualizer fetches remote endpoints directly from the browser using credentials: 'omit'. This is secure—your server never acts as an open proxy—but it means the remote endpoint must opt into CORS. Most internal APIs, legacy services, and third-party webhooks don’t. Users hit a hard wall: “Fetch failed: The endpoint must allow browser CORS.”
Workarounds (manual curl, browser extensions, local proxies) break the zero-dependency, zero-install promise of the tool.
The Architecture: Transparent Fallback Chain
// assets/bbs/js/cors-proxy.js
class JNV_CorsProxy {
constructor(options = {}) {
this.proxyUrl = options.proxyUrl || '';
this.webhookUrl = options.webhookUrl || '';
this.enabled = false;
this.lastCorsError = null;
}
configure({ proxyUrl, webhookUrl }) {
if (proxyUrl) this.proxyUrl = proxyUrl;
if (webhookUrl) this.webhookUrl = webhookUrl;
this.enabled = !!(this.proxyUrl || this.webhookUrl);
return this.enabled;
}
async fetchViaProxy(url, options = {}) {
if (!this.proxyUrl) throw new Error('No proxy URL configured');
const proxyUrl = new URL(this.proxyUrl);
proxyUrl.searchParams.set('url', url);
if (options.method) proxyUrl.searchParams.set('method', options.method);
if (options.headers) proxyUrl.searchParams.set('headers', JSON.stringify(options.headers));
const response = await fetch(proxyUrl.toString(), {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error(`Proxy error: HTTP ${response.status}`);
return response.json();
}
async fetchViaWebhook(url, options = {}) {
if (!this.webhookUrl) throw new Error('No webhook URL configured');
const response = await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, method: options.method || 'GET', headers: options.headers, body: options.body })
});
if (!response.ok) throw new Error(`Webhook error: HTTP ${response.status}`);
return response.json();
}
async fetchWithFallback(url, options = {}) {
// 1. Try direct fetch
try {
const response = await fetch(url, { ...options, credentials: 'omit', cache: 'no-store' });
if (response.ok || response.status !== 0) return response;
} catch (err) { this.lastCorsError = err; }
// 2. Try proxy
if (this.proxyUrl) {
try { return await this.fetchViaProxy(url, options); } catch (e) { console.warn('[JNV] Proxy failed:', e); }
}
// 3. Try webhook
if (this.webhookUrl) {
try { return await this.fetchViaWebhook(url, options); } catch (e) { console.warn('[JNV] Webhook failed:', e); }
}
throw this.lastCorsError || new Error('All fetch methods failed');
}
isCorsError(err) {
return err && err.name === 'TypeError' &&
(err.message.includes('CORS') || err.message.includes('cross-origin') || err.message.includes('NetworkError'));
}
}
// Global instance
window.JNV_CorsProxy = new JNV_CorsProxy();
Integration: Transparent Fallback in fetchEndpoint()
The main fetch function now catches CORS errors and routes through the proxy chain automatically:
async function fetchEndpoint() {
const url = els.endpointInput.value.trim();
// ... validation ...
try {
// Try direct fetch first
let response, text;
try {
response = await fetch(parsed.href, { headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' }, credentials: 'omit' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// ... size checks ...
text = new TextDecoder().decode(await response.arrayBuffer());
} catch (err) {
// CORS error? Try proxy fallback
if (window.JNV_CorsProxy && window.JNV_CorsProxy.isCorsError(err)) {
console.log('[JNV] CORS error detected, trying proxy fallback...');
if (window.JNV_CorsProxy.enabled) {
const proxyResponse = await window.JNV_CorsProxy.fetchWithFallback(parsed.href);
text = typeof proxyResponse.json === 'function'
? JSON.stringify(await proxyResponse.json())
: JSON.stringify(proxyResponse);
} else {
throw new Error(`Fetch failed: ${err.message}. Configure a CORS proxy/webhook in settings.`);
}
} else { throw err; }
}
// ... parse and render ...
} catch (err) {
showToast(`Fetch failed: ${err.message}. Configure a CORS proxy/webhook.`, 'error');
}
}
Configuration
The proxy is configured at runtime via the global instance:
// In browser console or via settings UI
window.JNV_CorsProxy.configure({
proxyUrl: 'https://your-cors-proxy.example.com/fetch', // GET ?url=...&method=...&headers=...
webhookUrl: 'https://your-webhook.example.com/fetch' // POST { url, method, headers, body }
});
The proxy endpoint should accept GET ?url=...&method=...&headers=... and return the upstream JSON. The webhook receives a POST with { url, method, headers, body } and returns the upstream JSON.
Upgrade 2: Smart Structural Diffing
The Problem
The original monitor used a simple string/size comparison. Every poll of a live API produced “changes” from:
- Dynamic timestamps:
updated_at,request_id,trace_id,nonce - Key reordering: JSON object keys serialized in different order
- High-precision timestamps:
2026-10-18T10:30:45.123Zvs2026-10-18T10:30:45.456Z - Floating-point drift:
99.999999vs100.000001
Users saw “50 cells changed” on every poll—impossible to spot the one actual data update.
The Solution: Semantic Structural Diffing
// assets/bbs/js/smart-diff.js
const api = {
compareTableModels: computeStructuralDiff,
deepEqual,
normalizeForCompare,
isDynamicField,
chooseKeyColumn
};
// 1. Deep equality with semantic tolerance
function deepEqual(a, b, options = {}) {
const { ignoreKeys = [], timestampTolerance = 0, numericTolerance = 0, ignoreKeyOrder = true } = options;
if (Object.is(a, b)) return true;
if (a === null || b === null) return a === b;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, i) => deepEqual(item, b[i], options));
}
if (typeof a === 'object' && typeof b === 'object') {
const keysA = Object.keys(a).filter(k => !ignoreKeys.includes(k));
const keysB = Object.keys(b).filter(k => !ignoreKeys.includes(k));
if (ignoreKeyOrder) {
if (keysA.length !== keysB.length) return false;
return keysA.every(key => keysB.includes(key) && deepEqual(a[key], b[key], options));
}
return keysA.length === keysB.length && keysA.every((key, i) => key === Object.keys(b)[i] && deepEqual(a[key], b[key], options));
}
if (typeof a === 'number' && typeof b === 'number') {
return numericTolerance > 0 ? Math.abs(a - b) <= numericTolerance : a === b;
}
if (typeof a === 'string' && typeof b === 'string') {
if (timestampTolerance > 0 && isTimestamp(a) && isTimestamp(b)) {
return Math.abs(new Date(a).getTime() - new Date(b).getTime()) <= timestampTolerance;
}
return a === b;
}
return a === b;
}
// 2. Dynamic field detection (stripped before comparison)
function isDynamicField(key, value) {
const dynamicKeys = ['timestamp', 'updated_at', 'updatedAt', 'created_at', 'createdAt',
'last_modified', 'lastModified', 'modified', 'modified_at', 'modifiedAt',
'request_id', 'requestId', 'trace_id', 'traceId', 'span_id', 'spanId',
'nonce', 'token', 'session_id', 'sessionId', 'csrf', '_token',
'version', 'etag', '_version', 'revision', 'hash', 'checksum'];
const lowerKey = key.toLowerCase();
if (dynamicKeys.some(dk => lowerKey.includes(dk.toLowerCase()))) return true;
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,}/.test(value)) return true;
return false;
}
function normalizeForCompare(value) {
if (value === null || value === undefined || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(normalizeForCompare);
const result = {};
for (const [key, val] of Object.entries(value)) {
if (!isDynamicField(key, val)) result[key] = normalizeForCompare(val);
}
return result;
}
// 3. Structural diff entry point
function computeStructuralDiff(previous, current, options = {}) {
if (!previous) return { baseline: true, changedCells: [], newRows: (current?.rows || []).map((_, i) => i), removedRows: [], changedRowCount: 0, changedCellCount: 0 };
const { keyColumn, ignoreKeys = ['timestamp', 'updated_at', 'updatedAt', 'created_at', 'createdAt', ...],
timestampTolerance = 5000, numericTolerance = 0.001 } = options;
// Key-based matching preferred
const effectiveKey = keyColumn || chooseKeyColumn(columns);
if (!effectiveKey) return compareByIndex(previous, current, options);
// ... key-based row matching with normalized comparison ...
}
// Export
globalThis.JSONNexusSmartDiff = { compareTableModels: computeStructuralDiff, deepEqual, normalizeForCompare, isDynamicField, chooseKeyColumn };
Integration: Monitoring Uses Smart Diff First
// In app.js — pollEndpoint()
const summary = window.JSONNexusSmartDiff?.compareTableModels(priorMonitorModel, nextModel) ||
window.JSONNexusMonitor?.compareTableModels(priorMonitorModel, nextModel) ||
{ baseline: !priorMonitorModel, changedCells: [], newRows: [], removedRows: [], changedCellCount: 0, changedRowCount: 0 };
The smart diff runs first; if unavailable (script failed to load), it falls back to the original index-based diff. This ensures zero regression risk.
Verification Checklist
- CORS proxy works: Configure
window.JNV_CorsProxy.configure({ proxyUrl: '...' }); fetch a non-CORS endpoint → loads via proxy. - Webhook fallback: Configure webhook URL; disable proxy; fetch non-CORS endpoint → loads via webhook.
- Direct fetch still works: CORS-enabled endpoint loads directly (no proxy hop).
- Clear error messaging: Non-CORS endpoint with no proxy configured → toast says “Configure a CORS proxy/webhook.”
- Smart diff ignores timestamps: Poll endpoint with
updated_atchanging every poll → changedCellCount = 0. - Smart diff ignores key order: Endpoint returns same keys in different order → no false positives.
- Smart diff catches real changes: Actual data field changes → highlighted correctly.
- Fallback works: Disable smart-diff.js → monitor falls back to original diff.
Phase 3 Architecture Summary
| Layer | File | Purpose |
|---|---|---|
| CORS Proxy | assets/bbs/js/cors-proxy.js | Proxy/webhook fallback chain, CORS error detection, runtime config |
| Smart Diff | assets/bbs/js/smart-diff.js | Semantic deepEqual, dynamic field stripping, key-based row matching |
| Fetch Integration | assets/app.js (fetchEndpoint) | CORS error catch → proxy fallback chain |
| Monitor Integration | assets/app.js (pollEndpoint) | Smart diff primary, original diff fallback |
| Styles | assets/styles.css | No new styles needed (reuses existing monitoring UI) |
| WP Plugin | json-nexus-visualizer.php | Enqueues json-nexus-cors-proxy, json-nexus-smart-diff |
| Templates | visualizer-standalone.php | Includes both new scripts for share links |
Try the Phase 3 Tool
Test the smart diff with a live endpoint that returns dynamic fields, or configure a CORS proxy to fetch a non-CORS API:
Quick smart-diff test: Paste this JSON, enable monitoring at 5s interval, and watch the changed-cell count stay at 0 despite timestamp rotation:
{
"data": [
{ "id": 1, "value": 42, "timestamp": "2026-10-18T10:00:00.000Z", "request_id": "req-1" },
{ "id": 2, "value": 43, "timestamp": "2026-10-18T10:00:01.000Z", "request_id": "req-2" }
],
"meta": { "request_id": "batch-123", "updated_at": "2026-10-18T10:00:00.000Z" }
}
CORS proxy test: In browser console, run:
window.JNV_CorsProxy.configure({
proxyUrl: 'https://api.allorigins.win/raw?url='
});
Then fetch a non-CORS endpoint (e.g., a raw GitHub file or internal API) and watch it load through the proxy.
The complete plugin (GPLv3) includes the PHP PDO SQLite backend, 90-day cryptographic share links, REST monitoring, JSON Schema validation, Phase 1 client-side upgrades, Phase 2 observability, and Phase 3 network resilience. Download the plugin ZIP from the tool page or clone the repository for self-hosted deployment.