(() => { "use strict"; const API = { list: "/api/keys", register: "/api/keys/register", delete: (id) => `/api/keys/${encodeURIComponent(id)}`, reveal: (id) => `/api/keys/${encodeURIComponent(id)}/access-key`, }; let bound = false; function qs(sel, root = document) { return root.querySelector(sel); } function esc(v) { return String(v ?? "").replace(/[&<>"']/g, ch => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'", }[ch])); } async function readJson(res) { const text = await res.text(); if (!text) return {}; try { return JSON.parse(text); } catch { return { error: text }; } } function formEl() { const version = qs("#key-version"); return version ? version.closest("form") : null; } function listEl() { return qs(".key-list"); } function setMessage(text, kind = "") { const form = formEl(); if (!form) return; let box = qs("#key-profile-status"); if (!box) { box = document.createElement("p"); box.id = "key-profile-status"; box.className = "key-status-message"; form.insertAdjacentElement("afterend", box); } box.textContent = text || ""; box.dataset.kind = kind; } function normalizeSerial(raw, gameVersion) { const value = String(raw || "").trim(); if (gameVersion === "dc_v2") { let v = value.toUpperCase(); if (v.startsWith("0X")) v = v.slice(2); if (/^[0-9A-F]{1,8}$/.test(v)) { return v.padStart(8, "0"); } return ""; } if (gameVersion === "pc_v2") { if (/^[0-9]{1,10}$/.test(value)) { const n = Number(value); if (Number.isSafeInteger(n) && n >= 0 && n <= 0xFFFFFFFF) { return n.toString(16).toUpperCase().padStart(8, "0"); } } return ""; } if (gameVersion === "gc_v3") { if (/^[0-9]{2}-[0-9]{4}-[0-9]{4}$/.test(value)) { const n = Number(value.replaceAll("-", "")); if (Number.isSafeInteger(n) && n >= 0 && n <= 0xFFFFFFFF) { return n.toString(16).toUpperCase().padStart(8, "0"); } } return ""; } return ""; } function setupForm(form) { const version = qs("#key-version", form); if (version) { version.innerHTML = [ '', '', '', ].join(""); } updateSerialHint(form); const versionSelect = qs("#key-version", form); if (versionSelect && !versionSelect.dataset.hintBound) { versionSelect.dataset.hintBound = "1"; versionSelect.addEventListener("change", () => updateSerialHint(form)); } const keyPassword = qs("#key-password", form); if (keyPassword) { const label = qs('label[for="key-password"]', form); if (label) label.remove(); keyPassword.remove(); } const serial = qs("#key-serial", form); if (serial && !qs("#key-display-serial", form)) { const displayLabel = document.createElement("label"); displayLabel.id = "key-display-serial-label"; displayLabel.setAttribute("for", "key-display-serial"); displayLabel.textContent = "Confirm Serial Number"; const displayInput = document.createElement("input"); displayInput.id = "key-display-serial"; displayInput.name = "display_serial"; displayInput.type = "text"; displayInput.autocomplete = "off"; displayInput.placeholder = "confirm serial number"; serial.insertAdjacentElement("afterend", displayInput); serial.insertAdjacentElement("afterend", displayLabel); } const access = qs("#key-access", form); if (access && !qs("#key-password", form)) { const passwordLabel = document.createElement("label"); passwordLabel.id = "key-password-label"; passwordLabel.setAttribute("for", "key-password"); passwordLabel.textContent = "GC Password"; passwordLabel.style.display = "none"; const passwordInput = document.createElement("input"); passwordInput.id = "key-password"; passwordInput.name = "password"; passwordInput.type = "text"; passwordInput.autocomplete = "off"; passwordInput.placeholder = "GC password"; passwordInput.style.display = "none"; access.insertAdjacentElement("afterend", passwordInput); access.insertAdjacentElement("afterend", passwordLabel); } const button = qs('button[type="submit"]', form) || qs("button", form); if (button) { button.type = "button"; } } function syncDotClass(status) { const value = String(status || "").toLowerCase(); return (value === "synced" || value === "current") ? "is-synced" : "is-syncing"; } function renderSyncStatus(label, status) { const safeStatus = status || "unknown"; return ` ${esc(label)}: ${esc(safeStatus)} `; } function renderKeySyncSummary(data) { const box = qs("#key-sync-summary"); if (!box) return; const sync = data.sync_status || "unknown"; const us = data.regions?.us?.status || "unknown"; const eu = data.regions?.eu?.status || "unknown"; box.innerHTML = ` ${renderSyncStatus("Key sync", sync)} ${renderSyncStatus("US", us)} ${renderSyncStatus("EU", eu)} `; } async function loadKeys() { const res = await fetch("/api/keys", { credentials: "same-origin", }); const data = await readJson(res); if (!res.ok || data.ok === false) { throw new Error(data.error || data.detail || `HTTP ${res.status}`); } renderKeys(data); return data; } function renderKeys(data) { const list = listEl(); if (!list) return; const keys = Array.isArray(data.keys) ? data.keys : []; const sync = data.sync_status || "unknown"; const us = data.regions?.us?.status || "unknown"; const eu = data.regions?.eu?.status || "unknown"; renderKeySyncSummary(data); const summary = ""; if (!keys.length) { list.innerHTML = summary + `

No V2 / V3 keys registered

Add a DC V2, PC V2, or GC V3 key profile above.

`; return; } list.innerHTML = summary + keys.map(key => { const version = key.game_version_label || key.game_version || "Key"; const label = key.label || ""; const serial = key.serial_number_hex || ""; let displaySerial = key.display_serial || serial; if (!key.display_serial && serial) { const n = Number.parseInt(serial, 16); if (Number.isFinite(n)) { if (key.game_version === "pc_v2") { displaySerial = String(n); } else if (key.game_version === "gc_v3") { const dec = String(n).padStart(10, "0"); displaySerial = `${dec.slice(0, 2)}-${dec.slice(2, 6)}-${dec.slice(6, 10)}`; } } } const isGC = key.game_version === "gc_v3"; return `

${esc(version)}

Registered profile ${esc(serial)}

Serial: ${esc(displaySerial)}

${label ? `

Label: ${esc(label)}

` : ""}

Access key: ••••••••••••

${isGC ? `

Password: ••••••••••••

` : ""}
`; }).join(""); } function updateSerialHint(form) { const gameVersion = qs("#key-version", form)?.value || ""; const serial = qs("#key-serial", form); const displaySerial = qs("#key-display-serial", form); const access = qs("#key-access", form); const password = qs("#key-password", form); const passwordLabel = qs("#key-password-label", form); if (!serial) return; if (gameVersion === "dc_v2") { serial.placeholder = "DC V2 serial number"; if (displaySerial) displaySerial.placeholder = "confirm DC V2 serial number"; if (access) access.placeholder = "access key"; } else if (gameVersion === "pc_v2") { serial.placeholder = "PC V2 decimal serial number"; if (displaySerial) displaySerial.placeholder = "confirm PC V2 decimal serial number"; if (access) access.placeholder = "access key"; } else if (gameVersion === "gc_v3") { serial.placeholder = "GC V3 serial number"; if (displaySerial) displaySerial.placeholder = "confirm GC V3 serial number"; if (access) access.placeholder = "access key"; } if (passwordLabel) { passwordLabel.style.display = gameVersion === "gc_v3" ? "" : "none"; } if (password) { password.style.display = gameVersion === "gc_v3" ? "" : "none"; if (gameVersion !== "gc_v3") { password.value = ""; } } } async function registerKey() { const form = formEl(); if (!form) return; const button = qs("button", form); const gameVersion = qs("#key-version", form)?.value || ""; const label = qs("#key-label", form)?.value.trim() || ""; const rawSerial = qs("#key-serial", form)?.value.trim() || ""; const displaySerial = qs("#key-display-serial", form)?.value.trim() || ""; const accessKey = qs("#key-access", form)?.value.trim() || ""; const password = qs("#key-password", form)?.value.trim() || ""; if (!["dc_v2", "pc_v2", "gc_v3"].includes(gameVersion)) { setMessage("Choose DC V2, PC V2, or GC V3.", "warn"); return; } const serialChecks = { dc_v2: /^(?:0x)?[0-9A-Fa-f]{1,8}$/, pc_v2: /^[0-9]{1,10}$/, gc_v3: /^[0-9]{2}-[0-9]{4}-[0-9]{4}$/, }; if (!serialChecks[gameVersion]?.test(rawSerial)) { const examples = { dc_v2: "DC V2 serial must be hex, like 4E62F237.", pc_v2: "PC V2 serial must be digits only.", gc_v3: "GC V3 serial must use the dashed format: NN-NNNN-NNNN.", }; setMessage(examples[gameVersion] || "Enter a valid serial number.", "warn"); return; } const serial = rawSerial; if (!serial) { setMessage("Enter a valid serial number.", "warn"); return; } if (!displaySerial) { setMessage("Confirm serial number is required.", "warn"); return; } if (displaySerial !== rawSerial) { setMessage("Serial number confirmation does not match.", "warn"); return; } if (!accessKey) { setMessage("Enter the key.", "warn"); return; } if (gameVersion === "gc_v3" && !password) { setMessage("Enter the GC password.", "warn"); return; } if (button) button.disabled = true; setMessage("Registering key profile...", "pending"); try { const res = await fetch("/api/keys/register", { method: "POST", credentials: "same-origin", headers: { "Accept": "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ game_version: gameVersion, label, serial_number: serial, display_serial: displaySerial, access_key: accessKey, password, }), }); const data = await readJson(res); if (!res.ok) { throw new Error(data.error || `Register failed: ${res.status}`); } form.reset(); setupForm(form); setMessage(`Saved. Sync status: ${data.sync_status}. Refresh status in a few seconds.`, "ok"); await loadKeys(); } catch (err) { setMessage(err.message || String(err), "warn"); } finally { if (button) button.disabled = false; } } async function revealAccessKey(id, button) { if (!id || !button) return; const card = button.closest("article, .key-card, .key-row, .registered-key"); const values = Array.from(card?.querySelectorAll(`[data-key-secret="${String(id).replace(/"/g, "\\\"")}"]`) || []); if (!values.length) { setMessage("Could not find this key on the page. Refresh and try again.", "warn"); return; } if (button.dataset.visible === "1") { values.forEach(value => value.textContent = "••••••••••••"); button.textContent = "show"; button.dataset.visible = "0"; return; } button.disabled = true; try { const res = await fetch(API.reveal(id), { credentials: "same-origin", headers: { "Accept": "application/json" }, }); const data = await readJson(res); if (!res.ok) { throw new Error(data.error || `Reveal failed: ${res.status}`); } values.forEach(value => { const kind = value.dataset.keySecretKind || "access_key"; value.textContent = data.key?.[kind] || ""; }); button.textContent = "hide"; button.dataset.visible = "1"; } catch (err) { setMessage(err.message || String(err), "warn"); } finally { button.disabled = false; } } async function deleteKey(id, button) { if (!id) return; const card = button?.closest("article, .key-card, .key-row, .registered-key"); const gameVersion = card?.dataset?.keyVersion || ""; let confirmMessage = "Delete this V2 key profile from this website account?"; if (gameVersion === "pc_v2") { confirmMessage += "\n\nPC V2 serial/key is forever tied to your local character saves. Please be sure you have your keys backed up before removing this key from your profile."; } if (!window.confirm(confirmMessage)) return; button.disabled = true; setMessage("Deleting key profile...", "pending"); try { const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, { method: "DELETE", credentials: "same-origin", headers: { "Accept": "application/json" }, }); const data = await readJson(res); if (!res.ok) { throw new Error(data.error || `Delete failed: ${res.status}`); } setMessage(`Deleted. Sync status: ${data.sync_status}.`, "ok"); await loadKeys(); } catch (err) { setMessage(err.message || String(err), "warn"); button.disabled = false; } } function bind() { if (bound) return true; const form = formEl(); const list = listEl(); if (!form || !list) return false; setupForm(form); const button = qs("button", form); if (button) { button.addEventListener("click", registerKey); } form.addEventListener("submit", (ev) => { ev.preventDefault(); registerKey(); }); list.addEventListener("click", (ev) => { const revealButton = ev.target.closest(".key-reveal-button"); if (revealButton) { revealAccessKey(revealButton.dataset.keyId, revealButton); return; } const button = ev.target.closest(".key-delete-button"); if (!button) return; deleteKey(button.dataset.keyId, button); }); bound = true; loadKeys().catch(err => { list.innerHTML = `

Could not load keys

${esc(err.message || err)}

`; }); return true; } function start() { [0, 500, 1500, 3000].forEach(ms => { window.setTimeout(bind, ms); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start); } else { start(); } })();