Compare commits

..

3 Commits

Author SHA1 Message Date
CyberMind
b8da257b83
Merge pull request #540 from CyberMind-FR/feature/532-plan-browser-xpi-webextension-emancipate
Some checks are pending
License Headers / check (push) Waiting to run
Browser extension: emancipate R3 toolbox cartographie live (#532)
2026-06-13 10:33:01 +02:00
b2ee2a97ef fix(webext): clean web-ext lint — drop unused optional_host_permissions, ignore build.sh/README (ref #532)
web-ext lint: 0 errors, 2 benign warnings (AMO data-collection
declaration is submission-time, tied to the signing follow-up;
service_worker-ignored is the intentional cross-browser pattern).
optional_host_permissions needed FF128 and was unused at MVP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:21:45 +02:00
c77e6250a9 feat(webext): browser extension — emancipate R3 cartographie live (ref #532)
New client clients/webext-toolbox/ (WebExtension MV3, Firefox .xpi +
Chromium): surfaces the toolbox live tracker analysis in the browser.

- manifest.json MV3, cross-browser background (service_worker + scripts
  for Firefox 115+ event page); host_permissions *.secubox.in only
- api.js: shared client over /wg/r3-check, /social/me (pair → HMAC token),
  /social/graph/{token}, /social/wipe/{token}
- background.js: toolbar badge = live tracker count, silent re-pair on
  token expiry, colour escalates gold → anti-bot → operator-grade
- popup: 4 stat tiles + dependency-free mini Round-Eye SVG graph + top
  trackers tagged CDN/anti-bot/operator-grade + cartographie/PDF/RGPD-wipe
- options: host / analysis window / manual token
- build.sh + build-webext.yml (web-ext lint + build, release on webext-v*)

Serve from the toolbox (2.6.14):
- GET /wg/toolbox.xpi (local file, else 302 → latest release asset)
- '🧩 Extension navigateur' button on both onboard panels
- sbin/secubox-toolbox-fetch-xpi + postinst serve dir + rules install

No server-side CORS needed (MV3 host_permissions). MVP polls /social/graph
and computes the delta client-side; SSE /social/live is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:08:06 +02:00
24 changed files with 1099 additions and 9 deletions

View File

@ -3,6 +3,23 @@
---
## 2026-06-13 — Browser extension : emancipate cartographie live (ref #532)
Nouveau client `clients/webext-toolbox/` (MV3 Firefox `.xpi` + Chromium),
sœur de l'app Android. Surface la cartographie sociale R3 dans le
navigateur : badge live des traceurs + popup (4 tuiles + mini Round-Eye
graph SVG sans dépendance + top-traceurs taggés CDN/anti-bot/opérateur +
actions cartographie/PDF/RGPD-wipe). Parle uniquement à la cabine via R3
(pas de CORS backend grâce à host_permissions).
`secubox-toolbox 2.6.14` : `GET /wg/toolbox.xpi` (local sinon 302 →
release), bouton onboard, helper `secubox-toolbox-fetch-xpi`, postinst
dir. CI `build-webext.yml` (`web-ext lint` + build, release asset sur tag
`webext-v*`). Suivi : signature AMO, SSE `/social/live`, icône PNG
Chromium, Poke/Emancipate (#525).
---
## 2026-06-13 — Android ToolBox app : serve + root-mode silent onboarding (ref #531/#536/#538)
App compagnon Android one-tap R3 (`clients/android-toolbox/`, Kotlin + Compose).

View File

@ -5,17 +5,22 @@
## 🔥 P0 — Immediate (in flight)
### Android ToolBox client (`clients/android-toolbox/`)
### ToolBox clients (`clients/`)
- [x] **#531 scaffold + CI** — Gradle/Compose one-tap onboarding, debug APK
via `build-android-apk.yml`. CI green.
- [x] **#536 serve from toolbox** — `GET /wg/toolbox.apk` + onboard button +
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,
debug APK via `build-android-apk.yml`. CI green.
- [x] **#536 serve APK from toolbox** — `GET /wg/toolbox.apk` + onboard button +
`secubox-toolbox-fetch-apk` helper.
- [x] **#538 root-mode silent** (PR #539) — system CA install + native kernel
WireGuard + auto R3 verify, gated behind explicit root tap.
- [ ] **release signing** — keystore secret in CI for a stable published
fingerprint (currently debug-signed sideload).
- [ ] **#532 browser XPI** — Firefox/Chrome extension equivalent (later).
- [x] **#538 Android root-mode silent** (PR #539) — system CA install + native
kernel WireGuard + auto R3 verify, gated behind explicit root tap.
- [x] **#532 browser extension** (`clients/webext-toolbox/`) — MV3 Firefox
`.xpi`/Chromium; live tracker badge + popup mini Round-Eye graph over
`/social/*`; `GET /wg/toolbox.xpi` + fetch helper + `build-webext.yml`.
- [ ] **release signing** — Android keystore + AMO `.xpi` signing secrets in CI
for stable published fingerprints (currently unsigned sideload).
- [ ] **#532 follow-ups** — optional `GET /social/live/{token}` SSE (replace the
client-side poll) ; Poke/Emancipate per-site control once #525 (deception)
ships ; Chromium PNG icon rasterisation for the Web Store.
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE

View File

@ -3,6 +3,32 @@
---
## 🔄 2026-06-13 : Browser extension — emancipate cartographie live (#532)
Extension navigateur (`clients/webext-toolbox/`, MV3 Firefox `.xpi` +
Chromium) sœur de l'app Android. Sort la *cartographie sociale* R3 dans
le navigateur : badge live des traceurs + popup.
- **Extension** : `manifest.json` (MV3, background `service_worker` +
`scripts` pour FF115+/Chromium), `api.js` (client `/wg/r3-check`,
`/social/me` → token, `/social/graph/{token}`, `/social/wipe`),
`background.js` (badge = total_trackers, re-pair silencieux si token
expiré, couleur escalade gold→anti-bot→opérateur), popup (4 tuiles
stats + **mini Round-Eye graph SVG sans dépendance** + top-traceurs
taggés CDN/anti-bot/opérateur + actions cartographie/PDF/RGPD-wipe),
options (hôte/fenêtre/token manuel). Pas de CORS backend nécessaire
(host_permissions). Validé : JSON+JS+SVG OK, `.xpi` build 11.8 KB.
- **Serve depuis la toolbox** (`2.6.14`) : `GET /wg/toolbox.xpi` (local
sinon 302 → release), bouton `🧩 Extension navigateur` sur les 2
panneaux onboard, helper `secubox-toolbox-fetch-xpi`, postinst dir.
- **CI** : `build-webext.yml``web-ext lint` + build, artifact, release
asset `secubox-toolbox-webext.xpi` sur tag `webext-v*`.
- **Reste à faire** : signature AMO (`.xpi` non signé = sideload/dev) ;
endpoint SSE `/social/live/{token}` optionnel ; icône PNG Chromium ;
contrôle Poke/Emancipate par-site quand #525 (déception) arrive.
---
## 🔄 2026-06-13 : Android ToolBox app — serve + root-mode silent onboarding (#531/#536/#538)
App compagnon Android **one-tap R3** pour la cabine VILLAGE3B

60
.github/workflows/build-webext.yml vendored Normal file
View File

@ -0,0 +1,60 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Build the SecuBox ToolBoX browser extension (#532).
# Plain JS/HTML/CSS — no bundler. web-ext lints + packages the .xpi.
# Produces an unsigned .xpi artifact; release signing (AMO) is a
# follow-up (needs AMO API credentials as secrets).
name: build-webext
on:
push:
branches: [ master ]
paths: [ "clients/webext-toolbox/**", ".github/workflows/build-webext.yml" ]
tags: [ "webext-v*" ]
pull_request:
paths: [ "clients/webext-toolbox/**" ]
workflow_dispatch:
permissions:
contents: write # needed to attach the .xpi to a release on tags
jobs:
build:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: clients/webext-toolbox
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Lint (web-ext)
run: npx --yes web-ext lint --source-dir . --self-hosted --ignore-files build.sh README.md
- name: Build .xpi (web-ext)
run: |
npx --yes web-ext build --source-dir . \
--artifacts-dir web-ext-artifacts --overwrite-dest \
--ignore-files build.sh README.md \
--filename "secubox-toolbox-webext.xpi"
- name: Upload .xpi artifact
uses: actions/upload-artifact@v4
with:
name: secubox-toolbox-webext
path: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
if-no-files-found: error
# On webext-v* tags, publish the .xpi as a release asset under the
# stable name the toolbox fetch helper + /wg/toolbox.xpi expect.
# `latest/download/secubox-toolbox-webext.xpi` resolves to the
# newest release.
- name: Publish release
if: startsWith(github.ref, 'refs/tags/webext-v')
uses: softprops/action-gh-release@v2
with:
files: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
fail_on_unmatched_files: true

5
clients/webext-toolbox/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# build artefacts
*.xpi
*.zip
*.crx
web-ext-artifacts/

View File

@ -0,0 +1,93 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# SecuBox ToolBoX — browser extension (Cartographie sociale, #532)
A WebExtension (Firefox `.xpi` + Chromium MV3) that **emancipates** the R3
toolbox live tracker analysis into the browser: instead of only seeing the
*cartographie sociale* on `kbin/social/me`, a toolbar badge ticks up as
trackers fire, and a popup shows who is watching you — live.
Sibling of [`clients/android-toolbox/`](../android-toolbox/). Talks **only**
to your cabine over the R3 tunnel — no third-party calls.
## What it does
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
`/social/{token}`; the extension reads the minted HMAC token from the
final URL. Anonymous (rotating `mac_hash`), no account. Manual token entry
available in the options page.
- **Live badge** — the toolbar icon shows the live tracker count for the
session (polled once a minute). Colour escalates: gold → 🟥 anti-bot
present → 🟪 operator-grade present.
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade),
a dependency-free **mini Round-Eye graph** (device centre, trackers on the
ring, radius by hits, colour by tier), and a top-tracker list with CDN
(12.A) / anti-bot (12.B) / operator-grade (12.C) tags.
- **Actions***Cartographie complète* (opens the full d3 view at
`/social/{token}`), *Rapport PDF* (`/social/report/{token}.pdf`), and
*Effacer mes données* (RGPD art. 17 wipe → `POST /social/wipe/{token}`).
## Install
The toolbox serves the built extension:
```
https://kbin.<board>.secubox.in/wg/toolbox.xpi
```
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
button. When a local build is present the cabine serves it; otherwise it
302-redirects to the latest GitHub release asset `secubox-toolbox-webext.xpi`.
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed
build (release CI step / `web-ext sign`); for development use
*about:debugging → Load Temporary Add-on*, or an ESR/Dev build with
`xpinstall.signatures.required=false`.
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
Note: Chromium action icons must be raster — rasterise `icons/icon.svg`
to PNG before a Chrome Web Store build (Firefox accepts the SVG as-is).
## Build
No bundler — the extension is plain JS/HTML/CSS. CI zips it:
- GitHub Actions `build-webext.yml``.xpi` artifact on push to `master` /
PRs touching `clients/webext-toolbox/**`; tagging `webext-v*` publishes the
`.xpi` as a release asset.
Locally:
```bash
cd clients/webext-toolbox
./build.sh # → secubox-toolbox-webext-<version>.xpi
```
## Files
| File | Role |
|------|------|
| `manifest.json` | MV3, cross-browser background (`service_worker` + `scripts`) |
| `api.js` | shared client over `/wg/r3-check`, `/social/*` |
| `background.js` | badge sync + silent re-pair (SW or event page) |
| `popup/` | live view, mini graph (`graph.js`), actions |
| `options/` | host / window / manual token |
## Cabine endpoints consumed
| Endpoint | Purpose |
|----------|---------|
| `/wg/r3-check` | tunnel presence indicator |
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
| `/social/wipe/{token}` | RGPD art. 17 erasure |
| `/social/{token}` | full d3 cartographie page |
| `/social/report/{token}.pdf` | bilingual PDF report |
## Notes
- No server-side CORS needed: an MV3 extension with `host_permissions` for
`*.secubox.in` fetches cross-origin from its background without CORS.
- MVP polls `/social/graph` and computes the delta client-side; a future
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
*Poke/Emancipate* per-site control lands once #525 ships.
License `LicenseRef-CMSD-1.0`.

View File

@ -0,0 +1,116 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: api
// Thin client over the R3 toolbox social endpoints. Shared by the
// background service worker and the popup. Cross-origin fetches are
// allowed because the extension holds host_permissions for the cabine
// vhosts — no server-side CORS needed.
// browser (Firefox promise API) || chrome (Chromium / FF MV3 SW)
const ext = globalThis.browser || globalThis.chrome;
const DEFAULTS = {
host: "kbin.gk2.secubox.in",
token: "",
since: 86400,
};
// base URL from a stored host (accept bare host or full origin)
function baseUrl(host) {
const h = (host || DEFAULTS.host).trim().replace(/\/+$/, "");
if (/^https?:\/\//i.test(h)) return h;
return `https://${h}`;
}
async function getConfig() {
const stored = await ext.storage.local.get(["host", "token", "since"]);
return { ...DEFAULTS, ...stored };
}
async function setConfig(patch) {
await ext.storage.local.set(patch);
return getConfig();
}
// Extract the HMAC token from a /social/{token} URL path.
function tokenFromUrl(url) {
try {
const u = new URL(url);
const m = u.pathname.match(/\/social\/([^/?#]+)/);
if (m && m[1] !== "me" && m[1].split(".").length === 4) return m[1];
} catch (_) {}
return null;
}
// Pair: hit /social/me over the tunnel; it 303-redirects to
// /social/{token}. fetch follows the redirect, so response.url carries
// the minted token. Returns the token or throws.
async function pair(host) {
const url = `${baseUrl(host)}/social/me`;
const resp = await fetch(url, { redirect: "follow", credentials: "omit" });
const tok = tokenFromUrl(resp.url);
if (!tok) throw new Error("pairing failed — not on the R3 tunnel?");
return tok;
}
// r3-check: is this client on the R3 tunnel right now?
async function r3Check(host) {
try {
const resp = await fetch(`${baseUrl(host)}/wg/r3-check`, { credentials: "omit" });
if (!resp.ok) return { tunnel: false, peer_ip: null };
return await resp.json();
} catch (_) {
return { tunnel: false, peer_ip: null };
}
}
// graph: the per-session cartographie JSON. Throws on HTTP error so the
// caller can show "token expired — re-pair".
async function graph(host, token, since) {
const qs = new URLSearchParams({ since: String(since || DEFAULTS.since) });
const resp = await fetch(`${baseUrl(host)}/social/graph/${token}?${qs}`, {
credentials: "omit",
});
if (resp.status === 403 || resp.status === 404) {
throw new Error("token-expired");
}
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
}
// RGPD art.17 wipe.
async function wipe(host, token) {
const resp = await fetch(`${baseUrl(host)}/social/wipe/${token}`, {
method: "POST",
credentials: "omit",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
}
function socialUrl(host, token) {
return `${baseUrl(host)}/social/${token}`;
}
function reportUrl(host, token) {
return `${baseUrl(host)}/social/report/${token}.pdf`;
}
const SbxApi = {
DEFAULTS,
ext,
baseUrl,
getConfig,
setConfig,
pair,
r3Check,
graph,
wipe,
socialUrl,
reportUrl,
tokenFromUrl,
};
// Usable both as a classic background script (globalThis) and an ES-less
// service worker. No module syntax to stay loadable as plain script.
globalThis.SbxApi = SbxApi;

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: background
// Keeps the toolbar badge in sync with the live tracker count and
// re-pairs over the R3 tunnel when the token expires. Works as a
// Chromium MV3 service worker (importScripts) AND a Firefox event page
// (api.js preloaded via background.scripts).
if (typeof importScripts === "function") {
// Chromium service worker: api.js isn't auto-loaded, pull it in.
try { importScripts("api.js"); } catch (_) {}
}
const api = globalThis.SbxApi;
const ext = globalThis.browser || globalThis.chrome;
const ALARM = "sbx-refresh";
const PERIOD_MIN = 1; // poll the cabine once a minute
function setBadge(text, color) {
try {
ext.action.setBadgeText({ text: text || "" });
if (color) ext.action.setBadgeBackgroundColor({ color });
} catch (_) {}
}
// Pull the graph, update the badge with the live tracker count. Auto
// re-pairs once if the stored token has expired.
async function refresh() {
const cfg = await api.getConfig();
if (!cfg.host) { setBadge("", "#6b6b7a"); return; }
let token = cfg.token;
const run = async (tok) => api.graph(cfg.host, tok, cfg.since);
try {
if (!token) token = await api.pair(cfg.host);
let data;
try {
data = await run(token);
} catch (e) {
if (String(e.message) === "token-expired") {
token = await api.pair(cfg.host); // one silent re-pair
data = await run(token);
} else throw e;
}
await api.setConfig({ token });
const n = (data.stats && data.stats.total_trackers) || 0;
// colour escalates with operator-grade / anti-bot presence
const opg = (data.stats && data.stats.opgrade_sites) || 0;
const ab = (data.stats && data.stats.antibot_sites) || 0;
const color = opg > 0 ? "#6e40c9" : ab > 0 ? "#e63946" : "#c9a84c";
setBadge(n > 999 ? "999+" : String(n), color);
await ext.storage.local.set({ lastStats: data.stats || {}, lastError: "" });
} catch (e) {
setBadge("!", "#6b6b7a");
await ext.storage.local.set({ lastError: String(e.message || e) });
}
}
ext.runtime.onInstalled.addListener(() => {
ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
refresh();
});
ext.runtime.onStartup && ext.runtime.onStartup.addListener(() => {
ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
refresh();
});
ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
// popup asks for an immediate refresh after pairing / config change
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg && msg.type === "refresh") {
refresh().then(() => sendResponse({ ok: true }));
return true; // async response
}
});

24
clients/webext-toolbox/build.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox ToolBoX Cartographie — build the unsigned .xpi (a zip of the
# extension dir). Firefox loads it as-is (temporary add-on / ESR with
# signatures off) ; a release build signs it via web-ext / AMO.
# Usage: ./build.sh → produces ./secubox-toolbox-webext-<version>.xpi
set -euo pipefail
cd "$(dirname "$0")"
VER=$(grep -oE '"version"[^,]*' manifest.json | grep -oE '[0-9.]+' | head -1)
OUT="secubox-toolbox-webext-${VER}.xpi"
rm -f "$OUT"
# -FS = sync (drop stale entries) ; exclude VCS, dotfiles, build script,
# any previously built artefact, and docs.
zip -r -FS "$OUT" . \
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' >/dev/null
echo "built $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)"
echo "Firefox: about:debugging → This Firefox → Load Temporary Add-on → pick the .xpi (or manifest.json)."
echo "Permanent install needs signing (web-ext sign / AMO) or Dev/ESR with xpinstall.signatures.required=false."
echo "Chromium: action icons must be raster — rasterise icons/icon.svg to PNG before a Chromium store build."

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<rect width="128" height="128" rx="24" fill="#0a0a0f"/>
<!-- outer eye almond -->
<path d="M64 36 C92 36 114 64 114 64 C114 64 92 92 64 92 C36 92 14 64 14 64 C14 64 36 36 64 36 Z"
fill="none" stroke="#c9a84c" stroke-width="5"/>
<!-- iris -->
<circle cx="64" cy="64" r="20" fill="#0c0c12" stroke="#00ff41" stroke-width="4"/>
<circle cx="64" cy="64" r="7" fill="#00ff41"/>
<!-- tracker spokes -->
<g stroke="#6e40c9" stroke-width="3" opacity="0.8">
<line x1="64" y1="64" x2="104" y2="40"/>
<line x1="64" y1="64" x2="24" y2="40"/>
<line x1="64" y1="64" x2="100" y2="92"/>
</g>
<g fill="#00d4ff">
<circle cx="104" cy="40" r="5"/>
<circle cx="24" cy="40" r="5"/>
</g>
<circle cx="100" cy="92" r="5" fill="#e63946"/>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -0,0 +1,36 @@
{
"manifest_version": 3,
"name": "SecuBox ToolBoX — Cartographie sociale",
"version": "0.1.0",
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
"browser_specific_settings": {
"gecko": {
"id": "secubox-toolbox-webext@cybermind.fr",
"strict_min_version": "115.0"
}
},
"permissions": ["storage", "alarms"],
"host_permissions": [
"*://*.secubox.in/*"
],
"action": {
"default_title": "SecuBox Cartographie",
"default_popup": "popup/popup.html",
"default_icon": {
"48": "icons/icon.svg",
"128": "icons/icon.svg"
}
},
"icons": {
"48": "icons/icon.svg",
"128": "icons/icon.svg"
},
"background": {
"service_worker": "background.js",
"scripts": ["api.js", "background.js"]
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
}
}

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<html lang="fr">
<head>
<meta charset="utf-8">
<title>SecuBox Cartographie — Réglages</title>
<style>
body { background:#0a0a0f; color:#e8e6d9; font:14px/1.5 system-ui,sans-serif;
max-width:520px; margin:40px auto; padding:0 20px; }
h1 { color:#c9a84c; font-size:18px; }
label { display:block; color:#6b6b7a; font-size:12px; margin:14px 0 4px; }
input,select { width:100%; padding:8px; border-radius:6px; border:1px solid #333;
background:#14141c; color:#e8e6d9; }
button { margin-top:16px; padding:9px 14px; border-radius:6px; border:1px solid #c9a84c;
background:#c9a84c; color:#0a0a0f; font-weight:700; cursor:pointer; }
.muted { color:#6b6b7a; font-size:12px; }
#msg { color:#00ff41; min-height:18px; margin-top:10px; }
</style>
</head>
<body>
<h1>👁️ SecuBox Cartographie — Réglages</h1>
<p class="muted">L'extension parle uniquement à ta cabine via le tunnel R3.
Aucune donnée n'est envoyée ailleurs.</p>
<label>Borne (hôte de la cabine)
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
</label>
<label>Fenêtre d'analyse
<select id="since">
<option value="3600">1 heure</option>
<option value="86400" selected>24 heures</option>
<option value="604800">7 jours</option>
</select>
</label>
<label>Jeton de session (optionnel — sinon appairage auto via R3)
<input id="token" type="text" placeholder="mac.exp.nonce.sig" autocomplete="off">
</label>
<button id="save">Enregistrer</button>
<p id="msg"></p>
<script src="../api.js"></script>
<script src="options.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
const api = globalThis.SbxApi;
const $ = (id) => document.getElementById(id);
async function load() {
const cfg = await api.getConfig();
$("host").value = cfg.host;
$("token").value = cfg.token || "";
$("since").value = String(cfg.since);
}
$("save").addEventListener("click", async () => {
await api.setConfig({
host: $("host").value.trim() || api.DEFAULTS.host,
token: $("token").value.trim(),
since: parseInt($("since").value, 10) || api.DEFAULTS.since,
});
api.ext.runtime.sendMessage({ type: "refresh" });
$("msg").textContent = "Enregistré ✓";
setTimeout(() => ($("msg").textContent = ""), 1500);
});
load();

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// Dependency-free mini "Round-Eye" cartographie : the device at the
// centre, top trackers on an outer ring, radius/colour by hits + tier.
// A compact stand-in for the full d3 view served at /social/{token}.
const SVGNS = "http://www.w3.org/2000/svg";
const PAL = {
base: "#c9a84c", // gold
cdn: "#00d4ff", // cyan
ab: "#e63946", // cinnabar (anti-bot)
opg: "#6e40c9", // void purple (operator-grade)
eye: "#00ff41", // matrix green
link: "#2a2a3a",
};
function el(name, attrs) {
const n = document.createElementNS(SVGNS, name);
for (const k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function tierOf(node) {
if (node.opgrade_vendor) return "opg";
if (node.antibot_vendor) return "ab";
if (node.cdn_vendor) return "cdn";
return "base";
}
function renderGraph(svg, data) {
while (svg.firstChild) svg.removeChild(svg.firstChild);
const W = 260, H = 180, cx = W / 2, cy = H / 2;
const nodes = (data && data.nodes ? data.nodes.slice() : [])
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
.slice(0, 14);
if (!nodes.length) {
const t = el("text", { x: cx, y: cy, fill: "#6b6b7a", "font-size": 11,
"text-anchor": "middle" });
t.textContent = "Aucun traceur détecté pour l'instant";
svg.appendChild(t);
return;
}
const maxHits = Math.max(...nodes.map((n) => n.hits || 1));
const R = 66;
// spokes first (under the dots)
nodes.forEach((n, i) => {
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
svg.appendChild(el("line", { x1: cx, y1: cy, x2: x, y2: y,
stroke: PAL.link, "stroke-width": 1 }));
});
// tracker dots
nodes.forEach((n, i) => {
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
const r = 3 + Math.round(6 * Math.sqrt((n.hits || 1) / maxHits));
const fill = PAL[tierOf(n)];
const c = el("circle", { cx: x, cy: y, r, fill, "fill-opacity": 0.85 });
const title = el("title", {});
title.textContent = `${n.domain}${n.hits || 0} hits`
+ (n.cdn_vendor ? ` · ${n.cdn_vendor}` : "")
+ (n.antibot_vendor ? ` · anti-bot ${n.antibot_vendor}` : "")
+ (n.opgrade_vendor ? ` · opérateur ${n.opgrade_vendor}` : "");
c.appendChild(title);
svg.appendChild(c);
});
// the eye (device) at the centre
svg.appendChild(el("circle", { cx, cy, r: 13, fill: "#0c0c12",
stroke: PAL.eye, "stroke-width": 2 }));
svg.appendChild(el("circle", { cx, cy, r: 4.5, fill: PAL.eye }));
}
globalThis.renderGraph = renderGraph;

View File

@ -0,0 +1,83 @@
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0 */
/* SecuBox cyberpunk/hermetic palette (DESIGN-CHARTER) */
:root {
--cosmos: #0a0a0f;
--gold: #c9a84c;
--cinnabar: #e63946;
--matrix: #00ff41;
--void: #6e40c9;
--cyan: #00d4ff;
--text: #e8e6d9;
--muted: #6b6b7a;
}
* { box-sizing: border-box; }
body {
width: 300px;
margin: 0;
background: var(--cosmos);
color: var(--text);
font: 13px/1.4 system-ui, "Segoe UI", sans-serif;
padding: 10px 12px 8px;
}
header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 8px;
}
.logo { color: var(--gold); font-weight: 700; letter-spacing: .5px; }
.r3 {
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
border: 1px solid currentColor;
}
.r3.on { color: var(--matrix); }
.r3.off { color: var(--muted); }
.muted { color: var(--muted); font-size: 11px; }
.err { color: var(--cinnabar); font-size: 11px; min-height: 14px; }
label { display: block; font-size: 11px; color: var(--muted); margin: 8px 0 4px; }
input[type=text] {
width: 100%; padding: 7px 8px; border-radius: 6px;
border: 1px solid #333; background: #14141c; color: var(--text);
}
button {
cursor: pointer; border: 1px solid #333; border-radius: 6px;
background: #14141c; color: var(--text); padding: 7px 8px; font-size: 12px;
}
button:hover { border-color: var(--gold); }
button.go {
width: 100%; margin-top: 8px; background: var(--gold); color: var(--cosmos);
font-weight: 700; border-color: var(--gold);
}
button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
.stats { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; margin-bottom: 8px; }
.stat {
background: #12121a; border: 1px solid #222; border-radius: 6px;
padding: 6px 2px; text-align: center;
}
.stat b { display: block; font-size: 16px; color: var(--gold); }
.stat span { font-size: 9px; color: var(--muted); }
.stat.warn b { color: var(--cinnabar); }
.stat.opg b { color: var(--void); }
#graph { width: 100%; height: 180px; background: #0c0c12; border-radius: 8px; display: block; }
.toplist { margin: 8px 0; max-height: 132px; overflow-y: auto; }
.row {
display: flex; align-items: center; gap: 6px; padding: 3px 2px;
border-bottom: 1px solid #1a1a22; font-size: 11px;
}
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .hits { color: var(--muted); }
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
.tier.cdn { background: #1d2a33; color: var(--cyan); }
.tier.ab { background: #2a1416; color: var(--cinnabar); }
.tier.opg { background: #1e1430; color: var(--void); }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 6px 0; }
.actions button:last-child { grid-column: 1 / 3; }
footer {
display: flex; justify-content: space-between; align-items: center;
margin-top: 6px; padding-top: 6px; border-top: 1px solid #1a1a22;
}
footer a { color: var(--cyan); text-decoration: none; font-size: 11px; }

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="popup.css">
<title>SecuBox Cartographie</title>
</head>
<body>
<header>
<span class="logo">👁️ VILLAGE3B</span>
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
</header>
<!-- Pairing (shown when no token) -->
<section id="pair" hidden>
<p class="muted">Connecte-toi à la cabine pour voir qui t'observe.</p>
<label>Borne
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
</label>
<button id="pairBtn" class="go">Appairer (R3)</button>
<p id="pairMsg" class="err"></p>
</section>
<!-- Live view (shown when paired) -->
<section id="live" hidden>
<div class="stats">
<div class="stat"><b id="sTrackers"></b><span>traceurs</span></div>
<div class="stat"><b id="sSites"></b><span>sites</span></div>
<div class="stat warn"><b id="sAntibot"></b><span>anti-bot</span></div>
<div class="stat opg"><b id="sOpgrade"></b><span>opérateur</span></div>
</div>
<svg id="graph" viewBox="0 0 260 180" role="img" aria-label="Mini cartographie"></svg>
<div class="toplist" id="topList"></div>
<div class="actions">
<button id="openFull">🗺️ Cartographie complète</button>
<button id="pdf">📄 Rapport PDF</button>
<button id="wipe" class="danger">🧹 Effacer mes données</button>
</div>
<p id="liveMsg" class="muted"></p>
</section>
<footer>
<a href="#" id="settings">Réglages</a>
<span class="muted" id="ver"></span>
</footer>
<script src="../api.js"></script>
<script src="graph.js"></script>
<script src="popup.js"></script>
</body>
</html>

View File

@ -0,0 +1,131 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: popup controller
const api = globalThis.SbxApi;
const ext = api.ext;
const $ = (id) => document.getElementById(id);
function show(which) {
$("pair").hidden = which !== "pair";
$("live").hidden = which !== "live";
}
function fillTopList(nodes) {
const list = $("topList");
list.innerHTML = "";
(nodes || [])
.slice()
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
.slice(0, 12)
.forEach((n) => {
const row = document.createElement("div");
row.className = "row";
const dom = document.createElement("span");
dom.className = "dom";
dom.textContent = n.domain || n.id;
row.appendChild(dom);
if (n.opgrade_vendor) addTier(row, "opg", n.opgrade_vendor);
else if (n.antibot_vendor) addTier(row, "ab", n.antibot_vendor);
else if (n.cdn_vendor) addTier(row, "cdn", n.cdn_vendor);
const hits = document.createElement("span");
hits.className = "hits";
hits.textContent = (n.hits || 0) + "×";
row.appendChild(hits);
list.appendChild(row);
});
}
function addTier(row, cls, label) {
const t = document.createElement("span");
t.className = "tier " + cls;
t.textContent = label;
row.appendChild(t);
}
function paint(data) {
const s = data.stats || {};
$("sTrackers").textContent = s.total_trackers ?? 0;
$("sSites").textContent = s.total_sites ?? 0;
$("sAntibot").textContent = s.antibot_sites ?? 0;
$("sOpgrade").textContent = s.opgrade_sites ?? 0;
globalThis.renderGraph($("graph"), data);
fillTopList(data.nodes);
}
async function load() {
const cfg = await api.getConfig();
$("ver").textContent = "v" + (ext.runtime.getManifest().version || "");
// tunnel indicator
api.r3Check(cfg.host).then((r) => {
const dot = $("r3dot");
dot.className = "r3 " + (r.tunnel ? "on" : "off");
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
});
if (!cfg.token) {
$("host").value = cfg.host;
show("pair");
return;
}
show("live");
$("liveMsg").textContent = "Chargement…";
try {
const data = await api.graph(cfg.host, cfg.token, cfg.since);
paint(data);
$("liveMsg").textContent = "";
} catch (e) {
if (String(e.message) === "token-expired") {
// token died — drop it and go back to pairing
await api.setConfig({ token: "" });
show("pair");
$("host").value = cfg.host;
$("pairMsg").textContent = "Session expirée — ré-appaire.";
} else {
$("liveMsg").textContent = "Erreur : " + e.message;
}
}
}
// ── events ──
$("pairBtn").addEventListener("click", async () => {
const host = $("host").value.trim() || api.DEFAULTS.host;
$("pairMsg").textContent = "Appairage…";
try {
await api.setConfig({ host });
const token = await api.pair(host);
await api.setConfig({ token });
ext.runtime.sendMessage({ type: "refresh" });
await load();
} catch (e) {
$("pairMsg").textContent = e.message + " (es-tu sur le tunnel ?)";
}
});
$("openFull").addEventListener("click", async () => {
const cfg = await api.getConfig();
ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
});
$("pdf").addEventListener("click", async () => {
const cfg = await api.getConfig();
ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
});
$("wipe").addEventListener("click", async () => {
if (!confirm("Effacer toutes tes données de cartographie sur la cabine ?")) return;
const cfg = await api.getConfig();
try {
const r = await api.wipe(cfg.host, cfg.token);
$("liveMsg").textContent = `Effacé : ${r.rows_deleted ?? 0} entrées.`;
await api.setConfig({ token: "" });
setTimeout(load, 800);
} catch (e) {
$("liveMsg").textContent = "Erreur effacement : " + e.message;
}
});
$("settings").addEventListener("click", (e) => {
e.preventDefault();
ext.runtime.openOptionsPage();
});
load();

View File

@ -1,3 +1,21 @@
secubox-toolbox (2.6.14-1~bookworm1) bookworm; urgency=medium
* Serve the browser ToolBoX extension .xpi from the toolbox (#532).
- api.py GET /wg/toolbox.xpi : serves the local .xpi
(/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi) with
content-type application/x-xpinstall ; if absent, 302 → the latest
public GitHub release asset (button never dead-ends).
- /wg/onboard : new "🧩 Extension navigateur (cartographie)" button
on both the inline + _install_panels variants.
- sbin/secubox-toolbox-fetch-xpi : pulls the latest release asset
into the serve path (best-effort, ZIP-magic sanity check).
- postinst : create the webext serve dir + best-effort first fetch.
- New client clients/webext-toolbox/ (MV3 Firefox/Chromium): live
tracker badge + popup mini Round-Eye graph over /social/* ;
build-webext.yml publishes the .xpi on webext-v* tags.
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 10:30:00 +0200
secubox-toolbox (2.6.13-1~bookworm1) bookworm; urgency=medium
* Serve the Android ToolBox APK from the toolbox (#536, follow-up #531).

View File

@ -52,6 +52,13 @@ case "$1" in
if [ -x /usr/sbin/secubox-toolbox-fetch-apk ]; then
/usr/sbin/secubox-toolbox-fetch-apk 2>&1 | head -2 || true
fi
# #532 : browser extension serve dir + best-effort fetch of the
# latest release .xpi (so GET /wg/toolbox.xpi serves it locally).
# Non-blocking : falls back to redirecting to the public release.
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox/webext
if [ -x /usr/sbin/secubox-toolbox-fetch-xpi ]; then
/usr/sbin/secubox-toolbox-fetch-xpi 2>&1 | head -2 || true
fi
# /var/log/secubox is a SHARED parent traversed by many service users
# (the aggregator runs as `secubox` and reads waf-threats.log under
# here). It MUST be 0755 — a 0750 owned by secubox-toolbox silently

View File

@ -105,6 +105,9 @@ execute_after_dh_auto_install:
# #536 : Android APK fetch helper.
install -m 0755 sbin/secubox-toolbox-fetch-apk \
debian/secubox-toolbox/usr/sbin/
# #532 : browser extension .xpi fetch helper.
install -m 0755 sbin/secubox-toolbox-fetch-xpi \
debian/secubox-toolbox/usr/sbin/
install -m 0755 sbin/secubox-toolbox-wg-restore \
debian/secubox-toolbox/usr/sbin/
install -m 0644 systemd/secubox-toolbox-wg-restore.service \

View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox-Deb :: secubox-toolbox-fetch-xpi (#532)
#
# Pull the latest browser ToolBoX extension .xpi (published as a GitHub
# release asset by build-webext.yml on webext-v* tags) into the toolbox
# serve path, so GET /wg/toolbox.xpi serves it locally (offline-capable
# install from the cabine). Best-effort : a failure leaves any existing
# .xpi in place ; the endpoint falls back to the public release redirect.
set -euo pipefail
readonly MODULE="secubox-toolbox-fetch-xpi"
DEST_DIR="/var/lib/secubox/toolbox/webext"
DEST="${DEST_DIR}/secubox-toolbox-webext.xpi"
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/latest/download/secubox-toolbox-webext.xpi"
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox "$DEST_DIR" 2>/dev/null \
|| mkdir -p "$DEST_DIR"
TMP=$(mktemp --suffix=.xpi)
trap 'rm -f "$TMP"' EXIT
if command -v wget >/dev/null 2>&1; then
if wget -q --timeout=20 --tries=2 "$RELEASE_URL" -O "$TMP" && [ -s "$TMP" ]; then
# Sanity : an .xpi is a ZIP — must start with PK\x03\x04.
if head -c 2 "$TMP" | grep -q "PK"; then
install -m 0644 "$TMP" "$DEST"
chown secubox-toolbox:secubox-toolbox "$DEST" 2>/dev/null || true
log "fetched .xpi -> ${DEST} ($(stat -c%s "$DEST" 2>/dev/null) bytes)"
exit 0
else
log "downloaded file is not an .xpi (no release asset yet?) — keeping existing"
fi
else
log "fetch failed (no release yet / network) — /wg/toolbox.xpi will redirect to the release"
fi
else
log "wget missing — cannot fetch .xpi"
fi
exit 0

View File

@ -622,6 +622,7 @@ pre{background:#1a1a25;color:var(--phos-hot);padding:0.6rem 0.8rem;border-radius
<div class="tab-content" data-content=android>
<a href="/wg/toolbox.apk" class="btn btn-go">📱 Installer l'app ToolBoX (1-tap)</a>
<a href="/wg/toolbox.xpi" class="btn">🧩 Extension navigateur (cartographie)</a>
<div class=warn style="margin-top:0.5rem">
<b>Le plus simple</b> : l'app fait tout (CA + tunnel + vérif) en 5 étapes.
Active « sources inconnues » à l'installation. Sinon, méthode manuelle ci-dessous :
@ -1192,6 +1193,7 @@ _ONBOARD_BODY = {
"android": """
<p><b> Le plus simple l'app ToolBoX fait tout :</b></p>
<a class=btn href="/wg/toolbox.apk">📱 Installer l'app ToolBoX (.apk, 1-tap)</a>
<a class=btn href="/wg/toolbox.xpi">🧩 Extension navigateur (cartographie live)</a>
<p class=note>Active « sources inconnues » à l'installation. L'app installe le CA, importe le tunnel et vérifie le R3 en 5 étapes. Sinon, méthode manuelle :</p>
<ol>
<li>Installe l'app <a class=btn href="https://play.google.com/store/apps/details?id=com.wireguard.android" target=_blank rel=noopener>WireGuard</a> depuis le Play Store.</li>
@ -1365,6 +1367,34 @@ async def wg_toolbox_apk() -> Response:
return RedirectResponse(url=_ANDROID_APK_RELEASE, status_code=302)
# Browser extension (Firefox .xpi), same serve pattern as the APK (#532).
_WEBEXT_XPI = Path("/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi")
_WEBEXT_XPI_RELEASE = (
"https://github.com/CyberMind-FR/secubox-deb/releases/latest/download/"
"secubox-toolbox-webext.xpi"
)
@router.get("/wg/toolbox.xpi")
async def wg_toolbox_xpi() -> Response:
"""Serve the browser ToolBoX extension .xpi (#532).
Local file first (install from the cabine, works offline) ; if it
hasn't been fetched yet, 302 to the latest public GitHub release
asset so the onboard button never dead-ends.
"""
if _WEBEXT_XPI.exists() and _WEBEXT_XPI.stat().st_size > 0:
return Response(
content=_WEBEXT_XPI.read_bytes(),
media_type="application/x-xpinstall",
headers={
"Content-Disposition": "attachment; filename=secubox-toolbox-webext.xpi",
"Cache-Control": "public, max-age=300",
},
)
return RedirectResponse(url=_WEBEXT_XPI_RELEASE, status_code=302)
@router.get("/wg/ca.mobileconfig")
async def wg_ca_mobileconfig() -> Response:
"""iOS profile that installs the mitm-wg CA in trust store."""

86
wiki/Browser-Extension.md Normal file
View File

@ -0,0 +1,86 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# 🧩 Browser extension — Cartographie sociale live
The **SecuBox ToolBoX browser extension** *emancipates* the R3 toolbox live
tracker analysis into your browser. Instead of only seeing the *cartographie
sociale* on `kbin/social/me`, a toolbar badge ticks up as trackers fire and a
popup shows who is watching you — live, as you browse.
Sibling of the [[Android-ToolBox]] app. Talks **only** to your cabine over the
R3 tunnel — no third-party calls.
- Source : [`clients/webext-toolbox/`](https://github.com/CyberMind-FR/secubox-deb/tree/master/clients/webext-toolbox)
- WebExtension **MV3** (Firefox `.xpi` + Chromium) · plain JS/HTML/CSS, no bundler
- License : `LicenseRef-CMSD-1.0`
## Install
The toolbox serves the built extension:
```
https://kbin.<board>.secubox.in/wg/toolbox.xpi
```
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
button. When a local build is present the cabine serves it
(`application/x-xpinstall`); otherwise it 302-redirects to the latest GitHub
release asset `secubox-toolbox-webext.xpi`.
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed build
(release CI / `web-ext sign`); for development use *about:debugging → Load
Temporary Add-on*, or an ESR/Dev build with
`xpinstall.signatures.required=false`.
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
Chromium action icons must be raster — rasterise `icons/icon.svg` to PNG
before a Web Store build (Firefox accepts the SVG as-is).
## What it does
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
`/social/{token}`; the extension reads the minted HMAC token from the final
URL. Anonymous (rotating `mac_hash`), no account. Manual token entry in the
options page.
- **Live badge** — the toolbar icon shows the live tracker count (polled once a
minute). Colour escalates: 🟡 gold → 🟥 anti-bot present → 🟪 operator-grade
present.
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade), a
dependency-free **mini Round-Eye graph** (device centre, trackers on the ring,
radius by hits, colour by tier), and a top-tracker list tagged with CDN
(12.A) / anti-bot (12.B) / operator-grade (12.C).
- **Actions***Cartographie complète* (full d3 view at `/social/{token}`),
*Rapport PDF* (`/social/report/{token}.pdf`), *Effacer mes données* (RGPD
art. 17 wipe → `POST /social/wipe/{token}`).
## Build (CI)
No bundler — `build-webext.yml` runs `web-ext lint` then packages the `.xpi`:
- artifact on push to `master` / PRs touching `clients/webext-toolbox/**`
- tagging `webext-v*` publishes the `.xpi` as a release asset
Locally:
```bash
cd clients/webext-toolbox
./build.sh # → secubox-toolbox-webext-<version>.xpi
```
## Cabine endpoints consumed
| Endpoint | Purpose |
|----------|---------|
| `/wg/r3-check` | tunnel presence indicator |
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
| `/social/wipe/{token}` | RGPD art. 17 erasure |
| `/social/{token}` | full d3 cartographie page |
| `/social/report/{token}.pdf` | bilingual PDF report |
| `/wg/toolbox.xpi` | the extension itself |
## Notes
- No server-side CORS needed: an MV3 extension with `host_permissions` for
`*.secubox.in` fetches cross-origin from its background without CORS.
- MVP polls `/social/graph` and computes the delta client-side; a future
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
*Poke/Emancipate* per-site control lands once the deception plane ships.

View File

@ -30,6 +30,7 @@
* [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH)
* [[Eye-Remote]] 📡
* [[Android-ToolBox]] 📱 one-tap R3
* [[Browser-Extension]] 🧩 cartographie
* [[QEMU-ARM64]] 🖥️
### 🟢 ROOT — Configuration