523d57624e
Hand-off sign-in from InstantAdPay (lib/sso.js: HMAC token, five minutes, single use; no sign-up, no mailer), missions with per-member per-visit proof codes (lib/missions.js: the embed is answered only from the mission's own origin, only after the dwell; hiding place rotates by day and member), weighted-low rewards with a daily cap that queues rather than refuses (lib/rewards.js), the faucet sender on ethers with a low-balance alert (lib/faucet.js), the one-line embed for the sites (public/embed.js), the hunter board, the public ledger, an admin page, and the site's own look. Telegram is behind the OUTBOUND gate like everything else. 13-check end-to-end test in test/run.js. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44 lines
3.4 KiB
JavaScript
44 lines
3.4 KiB
JavaScript
/* PolHunter embed. One line on a mission site:
|
|
<script src="https://polhunter.com/embed.js" data-slots="3" async></script>
|
|
plus, where a code may appear, elements with data-ph-slot="0" ... data-ph-slot="N-1".
|
|
|
|
When a hunter arrives with ?ph=<token> (their own, from their PolHunter board) the token is kept
|
|
in sessionStorage for this tab, and after the mission's dwell the code is fetched from
|
|
polhunter.com and rendered in ONE of the slots (which one is picked per hunter per day). The
|
|
request carries this page's Origin; polhunter.com answers only for the mission's own host and
|
|
only after the dwell. Anyone else loading this page sees nothing at all. */
|
|
(function () {
|
|
var API = 'https://polhunter.com';
|
|
try {
|
|
var q = new URLSearchParams(location.search), t = q.get('ph');
|
|
if (t) { sessionStorage.setItem('ph.token', t); q.delete('ph'); history.replaceState(null, '', location.pathname + (q.toString() ? '?' + q : '') + location.hash); }
|
|
t = sessionStorage.getItem('ph.token'); if (!t) return;
|
|
} catch (e) { return; }
|
|
// slots: explicit [data-ph-slot="i"] elements, or the i-th match of data-selector (e.g. ".video-card"
|
|
// on a training page, ".card" on a programs page), else a fixed badge in the corner
|
|
var me = document.currentScript, slots = Number((me && me.getAttribute('data-slots')) || 1) || 1, sel = me && me.getAttribute('data-selector');
|
|
function slotEl(i) {
|
|
var el = document.querySelector('[data-ph-slot="' + i + '"]'); if (el) return el;
|
|
if (sel) { var all = document.querySelectorAll(sel); if (all.length) { var host = all[i % all.length]; var d0 = document.createElement('div'); d0.style.cssText = 'margin:10px 0'; host.appendChild(d0); return d0; } }
|
|
var d = document.createElement('div'); d.setAttribute('data-ph-slot', String(i)); d.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:99999'; document.body.appendChild(d); return d;
|
|
}
|
|
function render(code, slot) {
|
|
var el = slotEl(slot); el.innerHTML = '';
|
|
var box = document.createElement('div');
|
|
box.setAttribute('role', 'note');
|
|
box.style.cssText = 'display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:14px;background:linear-gradient(135deg,#1a0f3d,#2b1a63);color:#f1eefb;font:600 14px/1.2 system-ui,-apple-system,"Segoe UI",sans-serif;box-shadow:0 8px 30px rgba(130,71,229,.45),0 0 0 1px rgba(255,255,255,.08);letter-spacing:.02em';
|
|
box.innerHTML = '<span style="width:22px;height:22px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#c9a4ff,#8247e5 60%,#4b2a8c);box-shadow:0 0 14px rgba(130,71,229,.9);flex:none"></span><span>PolHunter code <b style="font-family:ui-monospace,Menlo,monospace;font-size:16px;letter-spacing:.14em;color:#f3be43">' + code + '</b></span>';
|
|
el.appendChild(box);
|
|
}
|
|
var tries = 0;
|
|
function ask() {
|
|
tries++; if (tries > 40) return;
|
|
fetch(API + '/api/embed/code?t=' + encodeURIComponent(t), { mode: 'cors', credentials: 'omit' }).then(function (r) { return r.json(); }).then(function (r) {
|
|
if (r && r.code) { render(r.code, Number(r.slot) % slots); try { sessionStorage.removeItem('ph.token'); } catch (e) {} return; }
|
|
if (r && r.wait) { setTimeout(ask, Math.min(r.wait, 10) * 1000); return; }
|
|
// expired / gone / wrong origin: say nothing
|
|
}).catch(function () { setTimeout(ask, 8000); });
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ask); else ask();
|
|
})();
|