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:
+
+
+
+
+
+
+
+
Debug
+
+
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;