File size: 8,349 Bytes
9759528
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// CyberMax game Skin Pack (Stoopshake: the Dice & Table Pack): a one-time cosmetic unlock for ONE game (separate from the Fun Pass, which covers every
// game's archive + themes). Buy: counted redirect data.cybermaxtools.com/buy/st-<game>-skins-pack?s=game-<game>
// -> Stripe Payment Link; or the Gumroad listing (licence key). After checkout data.cybermaxtools.com/unlock/<game>-skins
// checks the order (or the Gumroad licence key) and returns to the game with ?unlock=skins#cmx-pro=<code>.
// capture() must run BEFORE funpass.init() so the Fun Pass never mistakes a skin-pack code for a pass code.
// Cosmetic only: skins never change the game, scores or the daily course. (c) 2026 CyberMax.
import { count, store, toast } from './cmx.js';

const DATA = 'https://data.cybermaxtools.com';
const CODE = /^(cmxk1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|cs_(live|test)_[A-Za-z0-9]{10,200}|[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})$/i;
let cfg = { game: '', name: '', price: '$1.99', gumroad: '', skins: 0 };
let state = null;
const subs = [];
const key = () => `cmx-skins:${cfg.game}`;

export const owned = () => !!(state && state.code);
export function onChange(fn) { subs.push(fn); }
const emit = () => subs.forEach((f) => { try { f(owned()); } catch { /* game hook */ } });

// Offline Pro keys (17 Oct 2026): "cmxk1.<payload>.<Ed25519 signature>" from data.cybermaxtools.com/unlock is verified here
// with WebCrypto and this public key, so an unlock needs no Worker call. Expired, foreign-signed or unverifiable keys fall
// back to the online check of the order code inside them, as before.
const CMX_PUB = 'u0FDxbu0d9B9Sz5-yLT6AuL5IagyTlFcbBgEDqZxWtk';
const b64d = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); };
async function cmxKeyOk(k, app, online) {
  const [tag, body, sig] = k.split('.');
  let p;
  try { p = JSON.parse(new TextDecoder().decode(b64d(body))); } catch { return false; }
  if (!p || p.a !== app) return false;
  try {
    const key = await crypto.subtle.importKey('raw', b64d(CMX_PUB), { name: 'Ed25519' }, false, ['verify']);
    if (await crypto.subtle.verify({ name: 'Ed25519' }, key, b64d(sig), new TextEncoder().encode(tag + '.' + body)) && (!p.x || p.x * 1000 > Date.now())) return true;
  } catch { /* no Ed25519 here: online check below */ }
  return p.c ? online(p.c) : false;
}
async function check(code) {
  if (/^cmxk1\./.test(code)) return cmxKeyOk(code, `${cfg.game}-skins`, checkOnline);
  return checkOnline(code);
}
async function checkOnline(code) {
  try {
    const r = await fetch(`${DATA}/unlock/${cfg.game}-skins?json=1&session_id=${encodeURIComponent(code)}`);
    const j = await r.json();
    return j && typeof j.pro === 'boolean' ? j.pro : null;
  } catch { return null; }
}
export async function unlock(code) {
  const ok = await check(code);
  if (ok) {
    state = store(key(), { code, at: new Date().toISOString() });
    count(cfg.game, 'pro', 'skins-unlocked');
    toast(`${cfg.name} Dice & Table Pack is on. New dice, tables and win emotes are yours!`, 3500);
    emit(); render();
  } else if (ok === false) toast('That code is not a Dice & Table Pack order for this game.', 3500);
  else toast('Could not reach the unlock check. Try again in a minute.', 3500);
  return ok;
}

/** Call first: game id, display name, skin count, Gumroad URL. Reads ?unlock=skins#cmx-pro=<code> after checkout. */
export function capture(options) {
  cfg = { ...cfg, ...options };
  state = store(key());
  const q = new URLSearchParams(location.search);
  const m = /[#&]cmx-key=([^&]+)/.exec(location.hash || '') || /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
  if (q.get('unlock') === 'skins' && m) {
    const code = decodeURIComponent(m[1]);
    q.delete('unlock');
    history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : ''));
    if (CODE.test(code)) unlock(code);
  }
}

const buyUrl = (from) => `${DATA}/buy/st-${cfg.game}-skins-pack?s=game-${cfg.game}${from ? `-${from}` : ''}`;
function dialog() {
  let d = document.getElementById('skinpack');
  if (!d) {
    d = document.createElement('dialog'); d.id = 'skinpack'; d.className = 'funpass';
    document.body.appendChild(d);
    d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
  }
  return d;
}
function render() {
  const d = dialog();
  const prev = (cfg.preview || []).map((s) => `<span class="sp-dot" style="background:${s}"></span>`).join('');
  if (owned()) {
    d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${cfg.name} Dice & Table Pack</p><h2>Your Dice & Table Pack is on. Thank you!</h2>
      <p>Pick dice, tables and emotes under STYLE on the home screen.</p><p class="fp-small">Your code (paste it on another device):</p><code class="fp-code">${state.code}</code></div>`;
    return;
  }
  d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${cfg.name} Dice & Table Pack</p>
    <h2>${cfg.title || 'Dice & Table Pack'}, ${cfg.price} once</h2><p class="sp-row">${prev}</p>
    <ul class="fp-perks">${(cfg.perks || []).map((p) => `<li>${p}</li>`).join('')}<li>Cosmetic only: same dice odds and chips for everyone</li><li>Pay once, keep forever, on any device with your code</li></ul>
    <p class="fp-row"><a class="btn pass" href="${buyUrl('')}" rel="nofollow" data-buy>Get the Dice & Table Pack: ${cfg.price}</a>${cfg.gumroad ? ` <a class="btn ghost sm" href="${cfg.gumroad}" rel="nofollow noopener" target="_blank" data-gum>or on Gumroad</a>` : ''}</p>
    <p class="fp-small">Secure Stripe checkout · the skins turn on as soon as you pay · 14-day promise: if it doesn't work as described, we fix it or refund you (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). The <b>Fun Pass</b> is a separate pass for every game's archive and themes.</p>
    <details><summary>Already bought it?</summary><form class="fp-form"><input aria-label="Order code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details></div>`;
  d.querySelector('.fp-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const c = e.target.querySelector('input').value.trim();
    if (CODE.test(c)) unlock(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500);
  });
  d.querySelector('[data-buy]').addEventListener('click', () => count(cfg.game, 'cta', 'skins-buy'));
  d.querySelector('[data-gum]')?.addEventListener('click', () => count(cfg.game, 'cta', 'skins-gumroad'));
}
export function open(from = 'button') {
  render();
  const d = dialog();
  if (d.showModal) d.showModal(); else d.setAttribute('open', '');
  count(cfg.game, 'cta', `skins-open-${from}`);
}

// Direct checkout (17 Oct 2026, Workers cap fix): a click on a data.cybermaxtools.com/buy/<offer> link goes straight to the
// Stripe/Polar checkout (static map https://cybermaxtools.com/store/buy-direct.json); the click is counted by a beacon.
(() => {
  if (typeof document === 'undefined' || window.__cmxBd) return;
  window.__cmxBd = 1;
  let MAP = null;
  try { MAP = JSON.parse(sessionStorage.getItem('cmx-bd') || 'null'); } catch { /* none */ }
  if (!MAP) fetch('https://cybermaxtools.com/store/buy-direct.json').then((r) => r.json()).then((j) => { MAP = j; try { sessionStorage.setItem('cmx-bd', JSON.stringify(j)); } catch { /* full */ } }).catch(() => {});
  document.addEventListener('click', (e) => {
    const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"]');
    if (!a || !MAP || !MAP.offers) return;
    try {
      const u = new URL(a.href);
      const k = /^\/buy\/([a-z0-9-]+)\/?$/.exec(u.pathname);
      const t = k && MAP.offers[k[1]];
      if (!t) return;
      const s = String(u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 80) || 'unknown';
      a.href = t.split(MAP.ph).join(s);
      if (!navigator.webdriver) navigator.sendBeacon(u.href + (u.search ? '&' : '?') + 'b=1');
    } catch { /* keep the counted link */ }
  }, true);
})();