JSON Nexus Visualizer Phase 3: CORS Proxy Fallback & Smart Structural Diffing

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.123Z vs 2026-10-18T10:30:45.456Z
  • Floating-point drift: 99.999999 vs 100.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

  1. CORS proxy works: Configure window.JNV_CorsProxy.configure({ proxyUrl: '...' }); fetch a non-CORS endpoint → loads via proxy.
  2. Webhook fallback: Configure webhook URL; disable proxy; fetch non-CORS endpoint → loads via webhook.
  3. Direct fetch still works: CORS-enabled endpoint loads directly (no proxy hop).
  4. Clear error messaging: Non-CORS endpoint with no proxy configured → toast says “Configure a CORS proxy/webhook.”
  5. Smart diff ignores timestamps: Poll endpoint with updated_at changing every poll → changedCellCount = 0.
  6. Smart diff ignores key order: Endpoint returns same keys in different order → no false positives.
  7. Smart diff catches real changes: Actual data field changes → highlighted correctly.
  8. Fallback works: Disable smart-diff.js → monitor falls back to original diff.

Phase 3 Architecture Summary

LayerFilePurpose
CORS Proxyassets/bbs/js/cors-proxy.jsProxy/webhook fallback chain, CORS error detection, runtime config
Smart Diffassets/bbs/js/smart-diff.jsSemantic deepEqual, dynamic field stripping, key-based row matching
Fetch Integrationassets/app.js (fetchEndpoint)CORS error catch → proxy fallback chain
Monitor Integrationassets/app.js (pollEndpoint)Smart diff primary, original diff fallback
Stylesassets/styles.cssNo new styles needed (reuses existing monitoring UI)
WP Pluginjson-nexus-visualizer.phpEnqueues json-nexus-cors-proxy, json-nexus-smart-diff
Templatesvisualizer-standalone.phpIncludes 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.

// THE TRANSMISSION LOG : FIELD NOTES

Weekly zero-fluff breakdowns of distributed systems, browser runtime performance, and production post-mortems delivered directly from Stella Sage.