diff --git a/.dockerignore b/.dockerignore index 7452cbb..da592f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ -.git node_modules .DS_Store dist diff --git a/Dockerfile b/Dockerfile index d9e1ac2..bb7d041 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # node:22-slim (22.x LTS), 2026-02-24 FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36 -RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends make git && rm -rf /var/lib/apt/lists/* RUN corepack enable && corepack prepare yarn@1.22.22 --activate WORKDIR /app diff --git a/build.js b/build.js index fdc9a24..54c1179 100644 --- a/build.js +++ b/build.js @@ -11,9 +11,51 @@ function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } +function getBuildInfo() { + const pkg = JSON.parse( + fs.readFileSync(path.join(__dirname, "package.json"), "utf8"), + ); + let commitHash = "unknown"; + try { + commitHash = execSync("git rev-parse --short HEAD", { + encoding: "utf8", + }).trim(); + } catch (_) { + // not a git repo or git not available + } + let commitHashFull = "unknown"; + try { + commitHashFull = execSync("git rev-parse HEAD", { + encoding: "utf8", + }).trim(); + } catch (_) { + // not a git repo or git not available + } + return { + version: pkg.version, + license: pkg.license, + author: pkg.author, + commitHash, + commitHashFull, + buildDate: new Date().toISOString().slice(0, 10), + }; +} + async function build() { console.log("Building AutistMask extension..."); + const buildInfo = getBuildInfo(); + console.log("Build info:", buildInfo); + + const define = { + __BUILD_VERSION__: JSON.stringify(buildInfo.version), + __BUILD_LICENSE__: JSON.stringify(buildInfo.license), + __BUILD_AUTHOR__: JSON.stringify(buildInfo.author), + __BUILD_COMMIT__: JSON.stringify(buildInfo.commitHash), + __BUILD_COMMIT_FULL__: JSON.stringify(buildInfo.commitHashFull), + __BUILD_DATE__: JSON.stringify(buildInfo.buildDate), + }; + // compile tailwind CSS console.log("Compiling Tailwind CSS..."); const tailwindInput = path.join(SRC, "popup", "styles", "main.css"); @@ -38,6 +80,7 @@ async function build() { platform: "browser", target: ["chrome110", "firefox110"], minify: true, + define, }); // bundle background script @@ -49,6 +92,7 @@ async function build() { platform: "browser", target: ["chrome110", "firefox110"], minify: true, + define, }); // bundle content script @@ -60,6 +104,7 @@ async function build() { platform: "browser", target: ["chrome110", "firefox110"], minify: true, + define, }); // bundle inpage script (injected into page context, separate file) @@ -71,6 +116,7 @@ async function build() { platform: "browser", target: ["chrome110", "firefox110"], minify: true, + define, }); // copy popup HTML diff --git a/src/popup/index.html b/src/popup/index.html index 90fb615..a38e901 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -1002,6 +1002,64 @@

+ +
+

About

+

+ AutistMask + — Minimal Ethereum wallet browser extension. +

+
+
+ License: + +
+
+ Author: + +
+
+ Version: + +
+
+ Release date: + +
+
+ Commit: + +
+
+
+ + diff --git a/src/popup/index.js b/src/popup/index.js index 6f8f6d2..8b72d05 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -1,18 +1,19 @@ // AutistMask popup entry point. // Loads state, initializes views, triggers first render. -const { DEBUG } = require("../shared/constants"); const { state, saveState, loadState, currentNetwork, } = require("../shared/state"); +const { isDebug, setRuntimeDebug } = require("../shared/log"); const { refreshPrices } = require("../shared/prices"); const { refreshBalances } = require("../shared/balances"); const { $, showView, + updateDebugBanner, setRenderMain, pushCurrentView, goBack, @@ -209,21 +210,11 @@ async function init() { await loadState(); applyTheme(state.theme); - const net = currentNetwork(); - if (DEBUG || net.isTestnet) { - const banner = document.createElement("div"); - banner.id = "debug-banner"; - if (DEBUG && net.isTestnet) { - banner.textContent = "DEBUG / INSECURE [TESTNET]"; - } else if (net.isTestnet) { - banner.textContent = "[TESTNET]"; - } else { - banner.textContent = "DEBUG / INSECURE"; - } - banner.style.cssText = - "background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;"; - document.body.prepend(banner); - } + // Sync runtime debug flag from persisted state before first render + setRuntimeDebug(state.debugMode); + + // Create the debug/testnet banner if needed (uses runtime debug state) + updateDebugBanner(); // Auto-default active address if ( diff --git a/src/popup/styles/main.css b/src/popup/styles/main.css index f0b3a24..30532d5 100644 --- a/src/popup/styles/main.css +++ b/src/popup/styles/main.css @@ -10,7 +10,7 @@ --color-border: #000000; --color-border-light: #cccccc; --color-hover: #eeeeee; - --color-well: #f5f5f5; + --color-well: #e8e8e8; --color-danger-well: #fef2f2; --color-section: #dddddd; } diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index e4744ef..fc9f2f9 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -1,6 +1,7 @@ // Shared DOM helpers used by all views. const { DEBUG } = require("../../shared/constants"); +const { isDebug } = require("../../shared/log"); const { formatUsd, getPrice, @@ -59,19 +60,37 @@ function showView(name) { clearFlash(); state.currentView = name; saveState(); + updateDebugBanner(name); +} + +// Create or update the debug/insecure warning banner. +// Called on every view switch and after the settings debug toggle changes. +// The banner is shown when the compile-time DEBUG constant is true OR when +// the user has enabled runtime debug mode via the settings easter egg, OR +// when the active network is a testnet. +function updateDebugBanner(viewName) { + const debug = isDebug(); const net = currentNetwork(); - if (DEBUG || net.isTestnet) { - const banner = document.getElementById("debug-banner"); - if (banner) { - if (DEBUG && net.isTestnet) { - banner.textContent = - "DEBUG / INSECURE [TESTNET] (" + name + ")"; - } else if (net.isTestnet) { - banner.textContent = "[TESTNET]"; - } else { - banner.textContent = "DEBUG / INSECURE (" + name + ")"; - } + const show = debug || net.isTestnet; + let banner = document.getElementById("debug-banner"); + if (show) { + if (!banner) { + banner = document.createElement("div"); + banner.id = "debug-banner"; + banner.style.cssText = + "background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;"; + document.body.prepend(banner); } + const suffix = viewName ? " (" + viewName + ")" : ""; + if (debug && net.isTestnet) { + banner.textContent = "DEBUG / INSECURE [TESTNET]" + suffix; + } else if (net.isTestnet) { + banner.textContent = "[TESTNET]" + suffix; + } else { + banner.textContent = "DEBUG / INSECURE" + suffix; + } + } else if (banner) { + banner.remove(); } } @@ -417,6 +436,7 @@ module.exports = { showError, hideError, showView, + updateDebugBanner, setRenderMain, pushCurrentView, goBack, diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index aa42a7a..576c251 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -1,8 +1,10 @@ const { $, showView, + updateDebugBanner, showFlash, escapeHtml, + flashCopyFeedback, goBack, pushCurrentView, } = require("./helpers"); @@ -10,12 +12,23 @@ const { applyTheme } = require("../theme"); const { state, saveState, currentNetwork } = require("../../shared/state"); const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks"); const { onChainSwitch } = require("../../shared/chainSwitch"); -const { log, debugFetch } = require("../../shared/log"); +const { log, debugFetch, setRuntimeDebug } = require("../../shared/log"); const deleteWallet = require("./deleteWallet"); +const { + BUILD_VERSION, + BUILD_LICENSE, + BUILD_AUTHOR, + BUILD_COMMIT, + BUILD_DATE, + GITEA_COMMIT_URL, +} = require("../../shared/buildInfo"); const runtime = typeof browser !== "undefined" ? browser.runtime : chrome.runtime; +let versionClickCount = 0; +let versionClickTimer = null; + function renderSiteList(containerId, siteMap, stateKey) { const container = $(containerId); const hostnames = [...new Set(Object.values(siteMap).flat())]; @@ -142,6 +155,28 @@ function show() { renderSiteLists(); renderWalletListSettings(); + // Populate About well + $("about-license").textContent = BUILD_LICENSE; + // Show only the name part of the author field (strip email) + const authorName = BUILD_AUTHOR.replace(/\s*<[^>]+>/, ""); + $("about-author").textContent = authorName; + $("about-version").textContent = BUILD_VERSION; + $("about-release-date").textContent = BUILD_DATE; + $("about-commit-link").textContent = BUILD_COMMIT; + $("about-commit-link").href = GITEA_COMMIT_URL; + + // Reset version click counter each time settings opens + versionClickCount = 0; + + // Show debug well if debug mode is already enabled + const debugWell = $("settings-debug-well"); + if (state.debugMode) { + debugWell.style.display = ""; + } else { + debugWell.style.display = "none"; + } + $("settings-debug-mode").checked = state.debugMode; + showView("settings"); } @@ -289,6 +324,66 @@ function init(ctx) { ctx.showSettingsAddTokenView, ); + // Bright saturated colors for easter egg flashes (clicks 6–10) + const easterEggColors = [ + "#ff0055", // hot pink + "#00cc44", // vivid green + "#3366ff", // electric blue + "#ff9900", // bright orange + "#aa00ff", // vivid purple + ]; + + // Easter egg: click version 10 times to reveal the debug well. + // Each click does a copy-flash animation. After 5 clicks, each + // additional click flashes a different bright saturated color. + $("about-version").addEventListener("click", () => { + versionClickCount++; + clearTimeout(versionClickTimer); + // Reset counter if user stops clicking for 3 seconds + versionClickTimer = setTimeout(() => { + versionClickCount = 0; + }, 3000); + + const el = $("about-version"); + + if (versionClickCount > 5) { + // Colored flash for clicks 6–10 + const colorIdx = versionClickCount - 6; + const color = easterEggColors[colorIdx % easterEggColors.length]; + el.classList.remove("copy-flash-fade"); + el.style.backgroundColor = color; + el.style.color = "#ffffff"; + setTimeout(() => { + el.style.backgroundColor = ""; + el.style.color = ""; + el.classList.add("copy-flash-fade"); + setTimeout(() => { + el.classList.remove("copy-flash-fade"); + }, 275); + }, 75); + } else { + // Standard copy-flash for clicks 1–5 + flashCopyFeedback(el); + } + + if (versionClickCount >= 10) { + versionClickCount = 0; + clearTimeout(versionClickTimer); + $("settings-debug-well").style.display = ""; + } + }); + + // Debug mode toggle — update runtime flag, persist, and re-render banner + $("settings-debug-mode").addEventListener("change", async () => { + state.debugMode = $("settings-debug-mode").checked; + setRuntimeDebug(state.debugMode); + await saveState(); + updateDebugBanner(state.currentView); + }); + + // Sync runtime debug flag on init + setRuntimeDebug(state.debugMode); + $("btn-settings-back").addEventListener("click", () => { goBack(); }); diff --git a/src/shared/buildInfo.js b/src/shared/buildInfo.js new file mode 100644 index 0000000..63c4b1a --- /dev/null +++ b/src/shared/buildInfo.js @@ -0,0 +1,35 @@ +// Build-time constants injected by esbuild define in build.js. +// These globals are replaced at bundle time with string literals. + +/* global __BUILD_VERSION__, __BUILD_LICENSE__, __BUILD_AUTHOR__, + __BUILD_COMMIT__, __BUILD_COMMIT_FULL__, __BUILD_DATE__ */ + +const BUILD_VERSION = + typeof __BUILD_VERSION__ !== "undefined" ? __BUILD_VERSION__ : "dev"; +const BUILD_LICENSE = + typeof __BUILD_LICENSE__ !== "undefined" ? __BUILD_LICENSE__ : "GPL-3.0"; +const BUILD_AUTHOR = + typeof __BUILD_AUTHOR__ !== "undefined" + ? __BUILD_AUTHOR__ + : "sneak "; +const BUILD_COMMIT = + typeof __BUILD_COMMIT__ !== "undefined" ? __BUILD_COMMIT__ : "unknown"; +const BUILD_COMMIT_FULL = + typeof __BUILD_COMMIT_FULL__ !== "undefined" + ? __BUILD_COMMIT_FULL__ + : "unknown"; +const BUILD_DATE = + typeof __BUILD_DATE__ !== "undefined" ? __BUILD_DATE__ : "unknown"; + +const GITEA_COMMIT_URL = + "https://git.eeqj.de/sneak/AutistMask/commit/" + BUILD_COMMIT_FULL; + +module.exports = { + BUILD_VERSION, + BUILD_LICENSE, + BUILD_AUTHOR, + BUILD_COMMIT, + BUILD_COMMIT_FULL, + BUILD_DATE, + GITEA_COMMIT_URL, +}; diff --git a/src/shared/log.js b/src/shared/log.js index b4a63f9..551fb20 100644 --- a/src/shared/log.js +++ b/src/shared/log.js @@ -1,12 +1,27 @@ // Leveled logger. Outputs to console with [AutistMask] prefix. -// Level is DEBUG when the DEBUG constant is true, INFO otherwise. +// Level is DEBUG when the compile-time DEBUG constant is true or the runtime +// debugMode state flag is enabled. The runtime flag is checked lazily so it +// responds immediately when toggled in settings. const { DEBUG } = require("./constants"); const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; -const threshold = DEBUG ? LEVELS.debug : LEVELS.info; + +// Runtime debug mode flag — set by settings.js when the user toggles debug +// mode via the easter egg. Kept here as a simple mutable reference so it can +// be updated without circular dependency issues with state.js. +let _runtimeDebug = false; + +function setRuntimeDebug(enabled) { + _runtimeDebug = enabled; +} + +function isDebug() { + return DEBUG || _runtimeDebug; +} function emit(level, method, args) { + const threshold = isDebug() ? LEVELS.debug : LEVELS.info; if (LEVELS[level] >= threshold) { console[method]("[AutistMask]", ...args); } @@ -37,4 +52,4 @@ async function debugFetch(url, opts) { return resp; } -module.exports = { log, debugFetch }; +module.exports = { log, debugFetch, setRuntimeDebug, isDebug }; diff --git a/src/shared/state.js b/src/shared/state.js index e8ee1b1..b0192d8 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -29,6 +29,7 @@ const DEFAULT_STATE = { fraudContracts: [], tokenHolderCache: {}, theme: "system", + debugMode: false, }; const state = { @@ -68,6 +69,7 @@ async function saveState() { fraudContracts: state.fraudContracts, tokenHolderCache: state.tokenHolderCache, theme: state.theme, + debugMode: state.debugMode, currentView: state.currentView, selectedWallet: state.selectedWallet, selectedAddress: state.selectedAddress, @@ -128,6 +130,8 @@ async function loadState() { state.fraudContracts = saved.fraudContracts || []; state.tokenHolderCache = saved.tokenHolderCache || {}; state.theme = saved.theme || "system"; + state.debugMode = + saved.debugMode !== undefined ? saved.debugMode : false; state.currentView = saved.currentView || null; state.selectedWallet = saved.selectedWallet !== undefined ? saved.selectedWallet : null;