webspresso 0.0.111 → 0.0.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,33 @@ function setupReduxDevToolsBridge(runtime) {
75
75
  }
76
76
  }
77
77
 
78
+ function escapeHtml(val) {
79
+ return String(val ?? '')
80
+ .replace(/&/g, '&')
81
+ .replace(/</g, '&lt;')
82
+ .replace(/>/g, '&gt;')
83
+ .replace(/"/g, '&quot;')
84
+ .replace(/'/g, '&#39;');
85
+ }
86
+
87
+ function sanitizeHref(url, fallback = '#') {
88
+ if (!url || typeof url !== 'string') return fallback;
89
+ const trimmed = url.trim();
90
+ if (!trimmed || /[\u0000-\u001F\u007F-\u009F]/.test(trimmed)) return fallback;
91
+ if (trimmed.startsWith('/') && !trimmed.startsWith('//') && !trimmed.startsWith('/\\')) {
92
+ return escapeHtml(trimmed);
93
+ }
94
+ try {
95
+ const parsed = new URL(trimmed);
96
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
97
+ return escapeHtml(parsed.href);
98
+ }
99
+ } catch {
100
+ return fallback;
101
+ }
102
+ return fallback;
103
+ }
104
+
78
105
  /**
79
106
  * Format JSON with syntax-colored HTML or safe text.
80
107
  */
@@ -83,13 +110,11 @@ function formatJsonHtml(data) {
83
110
  if (data === null) return '<span style="color:#f43f5e">null</span>';
84
111
  try {
85
112
  const jsonStr = JSON.stringify(data, null, 2);
86
- return jsonStr
87
- .replace(/&/g, '&amp;')
88
- .replace(/</g, '&lt;')
89
- .replace(/>/g, '&gt;')
90
- .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
113
+ return escapeHtml(jsonStr).replace(
114
+ /(&quot;(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\&])*&quot;(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
115
+ (match) => {
91
116
  let cls = 'color:#38bdf8;'; // number
92
- if (/^"/.test(match)) {
117
+ if (/^&quot;/.test(match)) {
93
118
  if (/:$/.test(match)) {
94
119
  cls = 'color:#a78bfa;font-weight:600;'; // key
95
120
  } else {
@@ -101,9 +126,10 @@ function formatJsonHtml(data) {
101
126
  cls = 'color:#f43f5e;'; // null
102
127
  }
103
128
  return `<span style="${cls}">${match}</span>`;
104
- });
129
+ }
130
+ );
105
131
  } catch (_) {
106
- return String(data);
132
+ return escapeHtml(data);
107
133
  }
108
134
  }
109
135
 
@@ -549,12 +575,12 @@ function mountDevtool(runtime, options = {}) {
549
575
  (c) => `
550
576
  <div
551
577
  class="ws-card ws-controller-item"
552
- data-inst-id="${c.instanceId}"
578
+ data-inst-id="${escapeHtml(c.instanceId)}"
553
579
  style="cursor:pointer;padding:8px 10px;margin-bottom:6px;${
554
580
  c.instanceId === current.instanceId ? 'border-color:#f59e0b;background:#27272a;' : ''
555
581
  }">
556
- <div style="font-weight:700;color:#f4f4f5;">${c.name || 'anonymous'}</div>
557
- <div style="font-size:10px;color:#a1a1aa;">&lt;${c.tagName}&gt; • ${c.instanceId}</div>
582
+ <div style="font-weight:700;color:#f4f4f5;">${escapeHtml(c.name || 'anonymous')}</div>
583
+ <div style="font-size:10px;color:#a1a1aa;">&lt;${escapeHtml(c.tagName)}&gt; • ${escapeHtml(c.instanceId)}</div>
558
584
  </div>
559
585
  `
560
586
  )
@@ -565,10 +591,10 @@ function mountDevtool(runtime, options = {}) {
565
591
  <div style="overflow-y:auto;padding-right:4px;">
566
592
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
567
593
  <div>
568
- <h3 style="font-size:14px;font-weight:700;color:#fff;">${current.name || 'anonymous'}</h3>
569
- <p style="font-size:10px;color:#71717a;">Instance: ${current.instanceId} • Connected: ${current.isConnected ? '✅' : '❌'}</p>
594
+ <h3 style="font-size:14px;font-weight:700;color:#fff;">${escapeHtml(current.name || 'anonymous')}</h3>
595
+ <p style="font-size:10px;color:#71717a;">Instance: ${escapeHtml(current.instanceId)} • Connected: ${current.isConnected ? '✅' : '❌'}</p>
570
596
  </div>
571
- <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-ctrl" data-inst-id="${current.instanceId}">
597
+ <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-ctrl" data-inst-id="${escapeHtml(current.instanceId)}">
572
598
  🎯 Highlight in DOM
573
599
  </button>
574
600
  </div>
@@ -577,7 +603,7 @@ function mountDevtool(runtime, options = {}) {
577
603
  <div class="ws-card">
578
604
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
579
605
  <strong style="color:#fbbf24;font-size:11px;">REACTIVE STATE</strong>
580
- <button class="ws-btn ws-btn-sm" id="ws-btn-copy-state" data-inst-id="${current.instanceId}">📋 Copy</button>
606
+ <button class="ws-btn ws-btn-sm" id="ws-btn-copy-state" data-inst-id="${escapeHtml(current.instanceId)}">📋 Copy</button>
581
607
  </div>
582
608
  <div class="ws-code-block">${formatJsonHtml(current.state)}</div>
583
609
  </div>
@@ -614,14 +640,14 @@ function mountDevtool(runtime, options = {}) {
614
640
  <div class="ws-card" style="margin-bottom:10px;">
615
641
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
616
642
  <div>
617
- <strong style="color:#38bdf8;font-size:12px;">${f.name}</strong>
618
- <span style="font-size:10px;color:#71717a;margin-left:6px;">#${f.instanceId}</span>
643
+ <strong style="color:#38bdf8;font-size:12px;">${escapeHtml(f.name)}</strong>
644
+ <span style="font-size:10px;color:#71717a;margin-left:6px;">#${escapeHtml(f.instanceId)}</span>
619
645
  </div>
620
646
  <div style="display:flex;gap:6px;">
621
- <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-frag" data-inst-id="${f.instanceId}">
647
+ <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-frag" data-inst-id="${escapeHtml(f.instanceId)}">
622
648
  🎯 Highlight
623
649
  </button>
624
- <button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-frag" data-frag-name="${f.name}">
650
+ <button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-frag" data-frag-name="${escapeHtml(f.name)}">
625
651
  🔄 Refresh
626
652
  </button>
627
653
  </div>
@@ -631,13 +657,13 @@ function mountDevtool(runtime, options = {}) {
631
657
  <span style="color:#a1a1aa;">Depends On Tags:</span>
632
658
  ${
633
659
  f.dependsOn.length > 0
634
- ? f.dependsOn.map((t) => `<span class="ws-badge ws-badge-info" style="margin-left:4px;">${t}</span>`).join('')
660
+ ? f.dependsOn.map((t) => `<span class="ws-badge ws-badge-info" style="margin-left:4px;">${escapeHtml(t)}</span>`).join('')
635
661
  : '<span style="color:#71717a;margin-left:4px;">(none)</span>'
636
662
  }
637
663
  </div>
638
664
  <div>
639
665
  <span style="color:#a1a1aa;">Version:</span>
640
- <span style="color:#f4f4f5;margin-left:4px;">${f.version}</span>
666
+ <span style="color:#f4f4f5;margin-left:4px;">${escapeHtml(f.version)}</span>
641
667
  </div>
642
668
  </div>
643
669
  ${
@@ -674,11 +700,11 @@ function mountDevtool(runtime, options = {}) {
674
700
  (r) => `
675
701
  <div class="ws-card" style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;margin-bottom:6px;">
676
702
  <div>
677
- <span class="ws-badge ws-badge-${r.method === 'GET' ? 'info' : 'warning'}">${r.method}</span>
678
- <span style="font-weight:600;margin-left:6px;">${r.url}</span>
703
+ <span class="ws-badge ws-badge-${r.method === 'GET' ? 'info' : 'warning'}">${escapeHtml(r.method)}</span>
704
+ <span style="font-weight:600;margin-left:6px;">${escapeHtml(r.url)}</span>
679
705
  </div>
680
706
  <div style="font-size:10px;color:#a1a1aa;">
681
- Policy: <span style="color:#38bdf8;">${r.policy || 'none'}</span> • Status: <span style="color:#34d399;">${r.status}</span>
707
+ Policy: <span style="color:#38bdf8;">${escapeHtml(r.policy || 'none')}</span> • Status: <span style="color:#34d399;">${escapeHtml(r.status)}</span>
682
708
  </div>
683
709
  </div>
684
710
  `
@@ -696,7 +722,7 @@ function mountDevtool(runtime, options = {}) {
696
722
  .map(
697
723
  (c) => `
698
724
  <div class="ws-card" style="padding:10px;margin-bottom:6px;">
699
- <div style="font-weight:600;color:#f4f4f5;font-size:11px;">Chain: ${c.chainId}</div>
725
+ <div style="font-weight:600;color:#f4f4f5;font-size:11px;">Chain: ${escapeHtml(c.chainId)}</div>
700
726
  <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(c)}</div>
701
727
  </div>
702
728
  `
@@ -718,8 +744,8 @@ function mountDevtool(runtime, options = {}) {
718
744
  .map(
719
745
  (t) => `
720
746
  <div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #27272a;font-size:11px;">
721
- <span>${t.fragmentName || t.instanceId}</span>
722
- <span style="color:#fbbf24;">patch: ${t.patchMs}ms ${t.loadMs ? `(load: ${t.loadMs}ms, render: ${t.renderMs}ms)` : ''}</span>
747
+ <span>${escapeHtml(t.fragmentName || t.instanceId)}</span>
748
+ <span style="color:#fbbf24;">patch: ${escapeHtml(t.patchMs)}ms ${t.loadMs ? `(load: ${escapeHtml(t.loadMs)}ms, render: ${escapeHtml(t.renderMs)}ms)` : ''}</span>
723
749
  </div>
724
750
  `
725
751
  )
@@ -758,7 +784,7 @@ function mountDevtool(runtime, options = {}) {
758
784
  .map(
759
785
  (e) => `
760
786
  <div class="ws-card" style="margin-bottom:6px;">
761
- <div style="font-weight:600;color:#38bdf8;font-size:11px;">${e.key}</div>
787
+ <div style="font-weight:600;color:#38bdf8;font-size:11px;">${escapeHtml(e.key)}</div>
762
788
  <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
763
789
  (() => {
764
790
  try {
@@ -784,7 +810,7 @@ function mountDevtool(runtime, options = {}) {
784
810
  .map(
785
811
  (e) => `
786
812
  <div class="ws-card" style="margin-bottom:6px;">
787
- <div style="font-weight:600;color:#a78bfa;font-size:11px;">${e.key}</div>
813
+ <div style="font-weight:600;color:#a78bfa;font-size:11px;">${escapeHtml(e.key)}</div>
788
814
  <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
789
815
  (() => {
790
816
  try {
@@ -815,7 +841,7 @@ function mountDevtool(runtime, options = {}) {
815
841
  <div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Visual database browser, query runner, and table inspector.</div>
816
842
  <div style="font-size:9px;color:#71717a;margin-top:4px;">Run in terminal: <code>npx webspresso studio</code> (port 4567)</div>
817
843
  </div>
818
- <a href="${studioUrl}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
844
+ <a href="${sanitizeHref(studioUrl)}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
819
845
  Open DB Studio ↗
820
846
  </a>
821
847
  </div>
@@ -829,8 +855,8 @@ function mountDevtool(runtime, options = {}) {
829
855
  <div style="font-weight:700;color:#38bdf8;font-size:13px;">⚙️ Admin Panel SPA</div>
830
856
  <div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Mithril.js model CRUD and staff administration cockpit.</div>
831
857
  </div>
832
- <a href="${features.adminPath}" target="_blank" rel="noopener noreferrer" class="ws-btn" id="ws-btn-open-admin" style="text-decoration:none;">
833
- Open ${features.adminPath} ↗
858
+ <a href="${sanitizeHref(features.adminPath)}" target="_blank" rel="noopener noreferrer" class="ws-btn" id="ws-btn-open-admin" style="text-decoration:none;">
859
+ Open ${escapeHtml(features.adminPath)} ↗
834
860
  </a>
835
861
  </div>
836
862
  `
@@ -918,7 +944,7 @@ function mountDevtool(runtime, options = {}) {
918
944
  <!-- Footer -->
919
945
  <div class="ws-footer">
920
946
  <span>Shortcuts: <strong>Alt + W</strong> to toggle • Console: <code>window.__WEBSPRESSO_DEVTOOLS__</code></span>
921
- <span style="color:#a1a1aa;">Webspresso v${PKG_VERSION}</span>
947
+ <span style="color:#a1a1aa;">Webspresso v${escapeHtml(PKG_VERSION)}</span>
922
948
  </div>
923
949
  </div>
924
950
  `;