secubox-deb/clients/webext-toolbox/api.js
CyberMind-FR 55955867af feat(clients): persistent WG identity (APK) + Tor status in webext/APK (ref #683)
APK (0.4.0):
- FIX lost-referrer: persist the WG profile in app-internal filesDir and REUSE
  it. /wg/profile/new mints a fresh keypair each call and onboarding runs every
  boot, so the device kept re-keying → new sha256(pubkey) identity → stats reset
  each reboot. Now one stable identity across reboot/reconnect/restart.
  (Reinstall still wipes filesDir; allowBackup stays off for CSPN.)
- Silent root onboarding (CA system-store + native WG) already runs on boot
  (#538/#558); it now provisions the STABLE profile.
- Surfaces 🧅 kbin Tor-egress status after onboarding.

webext (0.1.5):
- popup shows a 🧅 Tor indicator (exit anonymised) next to the R3 dot,
  via the new public /wg/tor-status endpoint.

toolbox (2.7.5):
- public, kbin-safe GET /wg/tor-status {tor_mode,running,bootstrap,exit_ip}
  (mirrors /wg/r3-check; /admin/tor/* stays admin-gated). Verified live on kbin.

13 toolbox tests green. .xpi 0.1.5 built; .apk builds via build-android-apk.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:03:15 +02:00

162 lines
4.8 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.

// 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 };
}
}
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
async function torStatus(host) {
try {
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
if (!resp.ok) return { tor_mode: false };
return await resp.json();
} catch (_) {
return { tor_mode: false };
}
}
// 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();
}
// #574 — protection stats + modular filter toggles (cabine admin API).
async function ghost(host) {
try {
const r = await fetch(`${baseUrl(host)}/admin/ghost`, { credentials: "omit" });
return r.ok ? await r.json() : null;
} catch (_) { return null; }
}
async function getAdminFilters(host) {
try {
const r = await fetch(`${baseUrl(host)}/admin/filters`, { credentials: "omit" });
return r.ok ? await r.json() : null;
} catch (_) { return null; }
}
async function setAdminFilters(host, patch) {
const r = await fetch(`${baseUrl(host)}/admin/filters`, {
method: "POST", credentials: "omit",
headers: { "content-type": "application/json" },
body: JSON.stringify(patch),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return await r.json();
}
// Favicon of a major site/tracker via the cabine's server-side proxy
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
function faviconUrl(host, domain) {
return `${baseUrl(host)}/social/favicon/${encodeURIComponent(domain || "")}`;
}
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,
torStatus,
graph,
wipe,
ghost,
getAdminFilters,
setAdminFilters,
faviconUrl,
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;