Skip to content

Commit e074d50

Browse files
Fix: repair mojibake glyphs + harden Privy broadcast against non-JSON responses
Two unrelated frontend fixes in index.html: 1. Mojibake repair. cp1252 round-trip corruption had mangled several UI glyphs in committed/deployed markup: a-back-arrow -> back arrow globe -> globe with meridians warning -> warning sign + VS16 stopwatch -> stopwatch lock -> closed lock with key hourglass -> hourglass done info -> information source + VS16 Repaired by byte-exact replacement of only the corrupted spans, so already-clean glyphs (checkmark, ellipsis, arrows, approx) are untouched. Verified: 0 mojibake markers remain; inline script still parses. 2. Guard the Privy signAndBroadcast path. It called r.json() unconditionally, so an HTML error response surfaced the opaque "Unexpected token '<', "<!DOCTYPE "... is not valid JSON". Now catches a non-JSON body and throws a clear HTTP-status error instead (mirrors the Keplr broadcast path's .catch guard). Pairs with the server-side JSON error handler so these responses are JSON anyway.
1 parent 9140873 commit e074d50

1 file changed

Lines changed: 28 additions & 21 deletions

File tree

public/index.html

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5472,7 +5472,7 @@ <h1 class="mobile-gate-title">View on Desktop</h1>
54725472

54735473
<!-- Privy / email pane -->
54745474
<div id="privyPane" style="display:none">
5475-
<button class="auth-back" type="button" onclick="backToChooser()">← Back</button>
5475+
<button class="auth-back" type="button" onclick="backToChooser()"> Back</button>
54765476
<div class="import-form-title">Sign in with email</div>
54775477
<div class="import-form-sub">
54785478
Enter your email. Privy will send a one-time code, then create or unlock an embedded Sentinel wallet for your account. Powered by <a href="https://privy.io" target="_blank" rel="noopener" style="color:var(--accent)">Privy</a>.
@@ -5483,7 +5483,7 @@ <h1 class="mobile-gate-title">View on Desktop</h1>
54835483

54845484
<!-- Recovery phrase pane (was: Import pane) -->
54855485
<div id="importPane" style="display:none">
5486-
<button class="auth-back" type="button" onclick="backToChooser()">← Back</button>
5486+
<button class="auth-back" type="button" onclick="backToChooser()"> Back</button>
54875487
<div class="import-form-title">Sign in with recovery phrase</div>
54885488
<div class="import-form-sub">
54895489
Paste your 12 or 24-word mnemonic. It is encrypted with AES-256-GCM and stored only in an httpOnly cookie scoped to your browser. The server never writes plaintext to disk.
@@ -7573,7 +7573,14 @@ <h3 id="securityFaqTitle">
75737573
accountNumber: signDoc.accountNumber,
75747574
}),
75757575
});
7576-
return await r.json();
7576+
// Guard against non-JSON responses (e.g. an HTML error page from a
7577+
// body-parser/413/404 failure) — calling r.json() on HTML throws the
7578+
// opaque "Unexpected token '<', "<!DOCTYPE "... is not valid JSON".
7579+
const out = await r.json().catch(() => null);
7580+
if (out === null) {
7581+
throw new Error(`Server returned a non-JSON response (HTTP ${r.status}). The request may have failed before reaching the signer.`);
7582+
}
7583+
return out;
75777584
}
75787585
if (!window.keplr) throw new Error('Keplr not available');
75797586
const addr = walletAddress || (S.keplrKey && S.keplrKey.bech32Address);
@@ -9383,15 +9390,15 @@ <h3 id="securityFaqTitle">
93839390
<option value="desc" ${S.nodeFilters.priceSort==='desc'?'selected':''}>Price: High → Low</option>
93849391
</select>
93859392
<button class="btn btn-ghost btn-sm" onclick="openCountryPicker()" ${countryList.length === 0 ? 'disabled' : ''}>
9386-
<span style="margin-right:6px">🌐</span>Browse by country${countryList.length ? ` (${countryList.length})` : ''}
9393+
<span style="margin-right:6px">🌐</span>Browse by country${countryList.length ? ` (${countryList.length})` : ''}
93879394
</button>
93889395
<button class="btn btn-ghost btn-sm" onclick="resetNodeFilters()" ${(!S.nodeFilters.search && !S.nodeFilters.country && !S.nodeFilters.protocol && !S.nodeFilters.priceSort && !S.nodeFilters.sortKey) ? 'disabled' : ''}>Reset</button>
93899396
${hasData ? `<span style="margin-left:auto;font-size:12px;color:var(--text-muted)">${fmt(d.total)} nodes${S.mgrPlanId && d.planNodesCount > 0 ? ` &middot; ${fmt(d.planNodesCount)} paid hidden` : ''}</span>` : ''}
93909397
</div>`;
93919398
const chipsInnerHtml = (() => {
93929399
const chips = [];
93939400
if (S.nodeFilters.search) chips.push({ key: 'search', label: `Search: "${escapeHtml(S.nodeFilters.search)}"` });
9394-
if (S.nodeFilters.country) chips.push({ key: 'country', label: `${countryFlag(S.nodeFilters.country) || '🌐'} ${escapeHtml(S.nodeFilters.country)}` });
9401+
if (S.nodeFilters.country) chips.push({ key: 'country', label: `${countryFlag(S.nodeFilters.country) || '🌐'} ${escapeHtml(S.nodeFilters.country)}` });
93959402
if (S.nodeFilters.protocol) chips.push({ key: 'protocol', label: `Protocol: ${escapeHtml(S.nodeFilters.protocol)}` });
93969403
if (S.nodeFilters.priceSort) chips.push({ key: 'priceSort', label: `Price: ${S.nodeFilters.priceSort === 'asc' ? 'Low → High' : 'High → Low'}` });
93979404
if (S.nodeFilters.sortKey) chips.push({ key: 'sortKey', label: `Sort: ${escapeHtml(S.nodeFilters.sortKey)} ${S.nodeFilters.sortDir === 'asc' ? '↑' : '↓'}` });
@@ -9708,12 +9715,12 @@ <h3 id="securityFaqTitle">
97089715
// string parser without needing quote-aware escaping.
97099716
const enc = btoa(unescape(encodeURIComponent(val || '')));
97109717
return `<button type="button" class="country-tile ${active ? 'active' : ''}" onclick="setCountryFilter(decodeURIComponent(escape(atob('${enc}'))))" title="${escapeHtml(label)}">
9711-
<span class="country-tile-flag">${flag || '🌐'}</span>
9718+
<span class="country-tile-flag">${flag || '🌐'}</span>
97129719
<span class="country-tile-name">${escapeHtml(label)}</span>
97139720
<span class="country-tile-count">${count != null ? fmt(count) : ''}</span>
97149721
</button>`;
97159722
};
9716-
const allTile = tile('All countries', '', data.total || null, !S.nodeFilters.country, '🌐');
9723+
const allTile = tile('All countries', '', data.total || null, !S.nodeFilters.country, '🌐');
97179724
const tiles = sorted.map(c => tile(c, c, data.counts[c], S.nodeFilters.country === c, countryFlag(c))).join('');
97189725

97199726
const existing = $('countryPickerOverlay');
@@ -10416,7 +10423,7 @@ <h3 id="securityFaqTitle">
1041610423
// Shared self-subscription status block. EVERY rail delivers bandwidth by
1041710424
// sharing an allocation from a subscription the operator holds — so the
1041810425
// operator must be subscribed to this plan first. This surfaces the same
10419-
// ⚠️/✅ indicator + Self-subscribe button the Free tile shows, so the
10426+
// ⚠️/✅ indicator + Self-subscribe button the Free tile shows, so the
1042010427
// operator can see whether the whole flow is even possible and fix it in
1042110428
// one click before wiring a single webhook. Filled by renderOwnSubStatus().
1042210429
const ownSubBlock = `
@@ -10471,7 +10478,7 @@ <h3 id="securityFaqTitle">
1047110478
setTimeout(() => renderOwnSubStatus(), 0);
1047210479
return `
1047310480
<div class="fiat-detail">
10474-
<button class="fiat-detail-back" onclick="setFiatTile(null)">← All payment rails</button>
10481+
<button class="fiat-detail-back" onclick="setFiatTile(null)"> All payment rails</button>
1047510482
<div class="fiat-detail-hero">
1047610483
<div class="fiat-detail-marks">
1047710484
<span class="fiat-rail-mark fiat-rail-mark-light">${RAIL_LOGOS.stripe}</span>
@@ -10498,7 +10505,7 @@ <h3 id="securityFaqTitle">
1049810505
<div class="fiat-detail-section-sub">Node + Express. Set <code>STRIPE_SECRET_KEY</code> and <code>STRIPE_WEBHOOK_SECRET</code>, register the URL in your Stripe dashboard, ship.</div>
1049910506
${codeBlock('Stripe webhook · Node.js', stripeCode)}
1050010507
<div class="fiat-detail-callout">
10501-
<span class="fiat-detail-callout-icon">⚠️</span>
10508+
<span class="fiat-detail-callout-icon">⚠️</span>
1050210509
<div class="fiat-detail-callout-body"><strong><code>express.raw()</code> is required.</strong> If a JSON parser touches the body before <code>constructEvent</code>, the signature check will always fail.</div>
1050310510
</div>
1050410511
</div>
@@ -10558,7 +10565,7 @@ <h3 id="securityFaqTitle">
1055810565
setTimeout(() => renderOwnSubStatus(), 0);
1055910566
return `
1056010567
<div class="fiat-detail">
10561-
<button class="fiat-detail-back" onclick="setFiatTile(null)">← All payment rails</button>
10568+
<button class="fiat-detail-back" onclick="setFiatTile(null)"> All payment rails</button>
1056210569
<div class="fiat-detail-hero">
1056310570
<div class="fiat-detail-marks">
1056410571
<span class="fiat-rail-mark fiat-rail-mark-light">${RAIL_LOGOS.paypal}</span>
@@ -10775,7 +10782,7 @@ <h3 id="securityFaqTitle">
1077510782
<div class="fiat-detail-section-sub">Zero infrastructure. Public REST. Poll a deposit address every 30s; fire once <code>vout</code> credits the address with the expected sats and the tx has cleared <code>CONFIRMATIONS</code> blocks. Quote each buyer a unique deposit address so you can map the settled payment back to their Sentinel wallet.</div>
1077610783
${codeBlock('mempool.space poller · Node.js', mempoolCode)}
1077710784
<div class="fiat-detail-callout">
10778-
<span class="fiat-detail-callout-icon">⏱</span>
10785+
<span class="fiat-detail-callout-icon"></span>
1077910786
<div class="fiat-detail-callout-body"><strong>Finality.</strong> 3 confirmations ≈ 30 min. Drop to 1 for low-value plans, raise to 6 for high-value.</div>
1078010787
</div>
1078110788
</div>`;
@@ -10786,7 +10793,7 @@ <h3 id="securityFaqTitle">
1078610793
<div class="fiat-detail-section-sub"><code>ethers</code> v6 against any public JSON-RPC (Alchemy, Infura, or <code>ethereum-rpc.publicnode.com</code>). For USDC, filter the <code>Transfer</code> event on the contract by your deposit address. For native ETH, walk every new block. <code>tx.wait(N)</code> blocks until <code>N</code> confirmations land before triggering Plan Manager. Map the depositor to their Sentinel wallet (a unique deposit address per buyer is simplest) before delivering.</div>
1078710794
${codeBlock('ethers v6 · Node.js', ethCode)}
1078810795
<div class="fiat-detail-callout">
10789-
<span class="fiat-detail-callout-icon">⏱</span>
10796+
<span class="fiat-detail-callout-icon"></span>
1079010797
<div class="fiat-detail-callout-body"><strong>Finality.</strong> 3 confirmations ≈ 36 sec. USDC mainnet contract: <code>0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48</code>.</div>
1079110798
</div>
1079210799
</div>`;
@@ -10797,7 +10804,7 @@ <h3 id="securityFaqTitle">
1079710804
<div class="fiat-detail-section-sub"><code>@solana/web3.js</code> v1 against <code>api.mainnet-beta.solana.com</code> at <code>'finalized'</code> — 31 slots, supermajority vote, the strongest guarantee Solana offers. <code>onAccountChange</code> fires on every lamport delta. For USDC, derive the associated token account via <code>@solana/spl-token</code> and watch its logs. Map the payer to their Sentinel wallet (a unique deposit address per buyer is simplest) before delivering.</div>
1079810805
${codeBlock('@solana/web3.js · Node.js', solCode)}
1079910806
<div class="fiat-detail-callout">
10800-
<span class="fiat-detail-callout-icon">⏱</span>
10807+
<span class="fiat-detail-callout-icon"></span>
1080110808
<div class="fiat-detail-callout-body"><strong>Finality.</strong> <code>'finalized'</code> commitment ≈ 13 sec. USDC mainnet mint: <code>EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v</code>.</div>
1080210809
</div>
1080310810
</div>`;
@@ -10806,7 +10813,7 @@ <h3 id="securityFaqTitle">
1080610813
setTimeout(() => renderOwnSubStatus(), 0);
1080710814
return `
1080810815
<div class="fiat-detail">
10809-
<button class="fiat-detail-back" onclick="setFiatTile(null)">← All payment rails</button>
10816+
<button class="fiat-detail-back" onclick="setFiatTile(null)"> All payment rails</button>
1081010817
<div class="fiat-detail-hero">
1081110818
<div class="fiat-detail-marks">
1081210819
<span class="fiat-rail-mark">${RAIL_LOGOS.btc}</span>
@@ -10851,7 +10858,7 @@ <h3 id="securityFaqTitle">
1085110858

1085210859
return `
1085310860
<div class="fiat-detail">
10854-
<button class="fiat-detail-back" onclick="setFiatTile(null)">← All payment rails</button>
10861+
<button class="fiat-detail-back" onclick="setFiatTile(null)"> All payment rails</button>
1085510862
<div class="fiat-detail-hero">
1085610863
<div class="fiat-detail-marks">
1085710864
<span class="fiat-rail-mark">${RAIL_LOGOS.free}</span>
@@ -11084,7 +11091,7 @@ <h3 id="securityFaqTitle">
1108411091
</div>
1108511092

1108611093
<div class="trig-security">
11087-
<span class="trig-security-icon">🔐</span>
11094+
<span class="trig-security-icon">🔐</span>
1108811095
<div>
1108911096
<strong>Bind this endpoint to your backend only.</strong> Plan Manager signs every TX with the
1109011097
wallet you loaded — leaking the URL externally lets anyone burn your fees. Front it with an API
@@ -11302,7 +11309,7 @@ <h3 id="securityFaqTitle">
1130211309
return;
1130311310
}
1130411311
btn.disabled = true;
11305-
btn.innerHTML = '<span class="trig-test-btn-icon">⏳</span> Broadcasting…';
11312+
btn.innerHTML = '<span class="trig-test-btn-icon"></span> Broadcasting…';
1130611313
out.style.display = 'block';
1130711314
out.className = 'trig-test-result trig-test-result-pending';
1130811315
out.innerHTML = '<strong>Broadcasting subscribe TX…</strong> Signing with your loaded wallet.';
@@ -11373,7 +11380,7 @@ <h3 id="securityFaqTitle">
1137311380
if (!data?.subscribed) {
1137411381
el.innerHTML = `
1137511382
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.35);border-radius:var(--radius-sm)">
11376-
<span style="font-size:20px;line-height:1">⚠️</span>
11383+
<span style="font-size:20px;line-height:1">⚠️</span>
1137711384
<div style="flex:1">
1137811385
<div style="font-family:var(--font-display);font-weight:600;font-size:13.5px;color:var(--text)">Not subscribed to plan #${escapeHtml(String(planId))} yet</div>
1137911386
<div style="font-family:var(--font-display);font-size:12.5px;color:var(--text-muted);margin-top:3px">You hold no subscription to this plan. You can self-subscribe now to mint a bandwidth allocation, or just add a subscriber below — that self-subscribes you automatically before sharing their allocation.</div>
@@ -11886,7 +11893,7 @@ <h3 id="securityFaqTitle">
1188611893
const focusMarks = railTab === 'onchain' ? onchainMarksHtml : fiatMarksHtml;
1188711894
const focusHeroHtml = railTab ? `
1188811895
<div class="rail-toggle-bar">
11889-
<button class="rail-toggle-back" onclick="setRailTab(null)">← Back to options</button>
11896+
<button class="rail-toggle-back" onclick="setRailTab(null)"> Back to options</button>
1189011897
<div class="rail-toggle-switch" role="tablist" aria-label="Payment rail">
1189111898
<button class="rail-toggle-option ${railTab === 'onchain' ? 'active' : ''}" onclick="setRailTab('onchain')">On-Chain</button>
1189211899
<button class="rail-toggle-option ${railTab === 'fiat' ? 'active' : ''}" onclick="setRailTab('fiat')">Fiat &amp; Non-IBC</button>
@@ -11967,7 +11974,7 @@ <h3 id="securityFaqTitle">
1196711974
</div>
1196811975
</div>
1196911976
<div class="immutable-banner">
11970-
<span class="immutable-banner-icon">ℹ️</span>
11977+
<span class="immutable-banner-icon">ℹ️</span>
1197111978
<div class="immutable-banner-body">
1197211979
<div class="immutable-banner-title">${plans.length > 0 ? 'Select a plan above to see live pricing.' : 'You don’t have a plan yet.'}</div>
1197311980
<div class="immutable-banner-sub">${plans.length > 0

0 commit comments

Comments
 (0)