# Privacy Guard — Browser Extension

A companion to the Privacy Guard web app. The extension watches
paste events and `<input type="file">` selections on every page and
flags personal data **before** it leaves the page.

Two scan engines:

- **Local (default)** — the PII model (`openai/privacy-filter`, the
  same one the server uses) runs in the browser via
  [`@huggingface/transformers`](https://huggingface.co/docs/transformers.js).
  WebGPU if available, WASM/CPU otherwise. Text never leaves the
  machine.
- **Server (optional)** — fallback for devices without WebGPU/WASM
  capability and the only path for file uploads (PDF/DOCX/XLSX/PPTX),
  which are parsed server-side.

Currently supported:

- `paste` events on `<textarea>`, plain `<input>` types, and generic
  `contenteditable` elements — scanned **locally**.
- `<input type="file">` for the formats Privacy Guard parses
  (`.txt`, `.md`, `.csv`, `.tsv`, `.json`, `.jsonl`, `.log`, `.xml`,
  `.html`, `.yaml`, `.yml`, `.pdf`, `.docx`, `.xlsx`, `.pptx`) —
  scanned via the **server** endpoint (needs backend + token).
- Auto-replacement: text inserted in-place, files swapped in the
  input via the DataTransfer API.
- Site allowlist with a "suggest defaults" starter set.
- Optional **strict mode**: when a scan fails, the paste/upload is
  blocked instead of let through (model-loading is exempt). Off by
  default — the extension otherwise fails open.
- Localized UI (English + German) via `chrome.i18n`, following the
  browser language. The product name "Privacy Guard" stays untranslated.
- Per-site engine override: pin specific hosts to local or server
  (e.g. always server on an internal domain) via `pattern=engine`
  lines in the options.
- Model preload on browser start (local engine) so the first paste
  isn't blocked by the one-time model download.

Known limitations:

- Drag-and-drop file uploads onto `<div>` dropzones (Gmail, GitHub
  attach areas) are not intercepted — only `<input type="file">`.
- Rich-text editors with their own input handling (CodeMirror,
  Monaco, Notion / ProseMirror) fall back to a non-blocking toast
  when `execCommand("insertText")` cannot reach the editor.
- Files larger than 5 MB skip the pre-scan and prompt immediately.
- Files larger than 25 MB are passed through untouched (matches
  the backend's `MAX_UPLOAD_BYTES` default).
- Safari is out of scope — MV3 support is incomplete and WebGPU in
  extensions is not exposed.
- Model integrity: the ONNX model is fetched from Hugging Face over
  HTTPS without an extra SHA pin. Risk is limited (classification
  only, no code execution); strict pinning isn't practical with the
  transformers.js internal loader, so it is intentionally out of scope.

## Prerequisites

Depends on which engine you run.

### Local engine (default)

Nothing on the server side. The first paste with PII triggers a
one-time download of the model from Hugging Face
(~770 MB, cached in IndexedDB). Subsequent loads come from the cache
and start in seconds.

Recommended browser: Chrome / Edge / Brave 121+ with WebGPU enabled,
or Firefox 128+ (uses WASM/CPU automatically).

### Server engine (optional / file uploads)

You need a running Privacy Guard backend (see the repository root
README) with:

1. **A user account** — log in via the web UI.
2. **A token** — go to *Browser extension → New token*, give it a
   label (e.g. "Chrome on laptop"), copy the value once.
3. **CORS enabled for the extension origin.** Set
   `EXTENSION_ALLOWED_ORIGINS` in `.env` to the extension's origin
   (`chrome-extension://<id>` or `moz-extension://<uuid>`) and restart
   the API container. The extension's ID is shown on
   `chrome://extensions` after loading it.

File uploads (PDF/DOCX/XLSX/PPTX) always need the server engine —
there is no in-browser parser for these formats.

## Install — recommended path (download from your server)

The Privacy Guard server bundles ready-to-load ZIP archives for both
browsers. No git clone needed:

1. Open the Privacy Guard web UI, sign in, go to **Browser
   extension**.
2. Click **Chrome / Edge** or **Firefox** to download the
   corresponding ZIP from
   `/static/extension/privacy-guard-<browser>-latest.zip`.
3. Unzip the archive.
4. **Chrome / Edge:** open `chrome://extensions`, enable Developer
   mode, click *Load unpacked*, pick the unzipped folder.
5. **Firefox:** open `about:debugging` → *This Firefox* → *Load
   Temporary Add-on…*, pick the `manifest.json` inside the unzipped
   folder. (Firefox-specific manifest is already in place — no
   renaming required.)
6. Click the extension icon → **Open settings**. Default engine is
   *Local*. Either trigger a paste with PII to start the model
   download, or click **Modell jetzt laden** to warm it up
   explicitly. If you need server-side file scans, also fill in the
   server URL + token and verify with **Test connection**.

The ZIP archives are regenerated by `scripts/package-extension.sh`
on the server side; the `-latest.zip` filenames stay stable across
version bumps.

## Install — developer path (load unpacked from this folder)

If you've cloned the repo and want to iterate on the extension code
directly:

```bash
cd browser-extension
npm ci
npm run build:vendor   # copies transformers.js + WASM into vendor/
```

Then:

- **Chrome / Edge:** point *Load unpacked* at `browser-extension/`.
- **Firefox:** rename `manifest.json` → `manifest.chrome.json` and
  `manifest.firefox.json` → `manifest.json` first (or copy the folder
  to `browser-extension-firefox/` and swap there to keep both
  browsers loadable side-by-side).

The ZIP variant served from the web UI exists exactly to spare
end-users this rename step.

For permanent installation, sign the extension via the Chrome Web
Store / AMO; out of scope for the MVP.

## Manual test

A small fixture lives at `test-fixtures/textarea.html`:

```bash
cd browser-extension
python3 -m http.server 5500
# open http://localhost:5500/test-fixtures/textarea.html
```

Test matrix (local engine, after first model load):

| Paste                                   | Expected                                             |
|----------------------------------------|------------------------------------------------------|
| `hello world`                          | No dialog, paste appears immediately                 |
| `Contact: jane@example.com — call me`  | Dialog → click **Anonymize** → field shows `[PRIVATE_EMAIL]` |
| Same paste again within 5 minutes      | Dialog appears again (only the scan is cached)       |
| PII paste → **Paste anyway**           | Original text is inserted                            |
| PII paste → **Cancel** / Esc           | Field stays unchanged                                |
| First paste, model not yet loaded      | Toast "Privacy Guard lädt das Modell …" + paste through; next paste prompts normally |
| File upload while engine=local without server config | Toast "File-Scans benötigen Server-Konfiguration", file passes through |
| WebGPU disabled in `chrome://flags`    | Slower but still works (WASM fallback)              |
| Server engine, token missing           | Clear error message in popup / on test paste        |

For the server engine the test cases from the original Slice-5 fixture
also still apply — the backend offline → extension fails open and
pastes are passed through with a warning logged in the page console.

## Architecture

```
browser-extension/
├── manifest.json           Chrome MV3 manifest (incl. offscreen + CSP)
├── manifest.firefox.json   Firefox MV3 alternative
├── background.js           Service worker — engine switch + cache + throttle
├── content.js              Per-page paste/file hook + Shadow-DOM overlay
├── offscreen.html          Chrome offscreen-document bootstrap
├── offscreen.js            Hosts the transformers.js pipeline
├── overlay.css             Overlay stylesheet (web_accessible_resources)
├── shared/
│   ├── browser-shim.js     chrome ↔ browser namespace
│   ├── api.js              Backend client (server engine)
│   ├── span-postprocess.js Port of app/services/privacy_filter.py — same
│   │                       LABEL_MAP, merge logic, replacement strings
│   │                       so local & server outputs match bit-for-bit
│   ├── settings.js         storage.sync (settings) + storage.local (token, modelStatus)
│   └── i18n-dom.js         Applies _locales strings to [data-i18n] elements
├── _locales/               chrome.i18n catalogs (en + de, 92 keys each)
├── options/                Settings page (engine, strict mode, URL, token, labels, allowlist)
├── popup/                  Toolbar popup with engine status + model progress
├── vendor/transformers/    transformers.js v3 + ONNX-Runtime WASM
│                           (built by scripts/copy-vendor.mjs, gitignored)
└── test-fixtures/          Pages for manual testing
```

### Where the local scan runs

```
content.js (paste hook)
   └→ runtime.sendMessage("scan")
background.js (Service Worker)
   ├─ scanEngine === "server" ?  → shared/api.js → fetch /api/scan
   └─ scanEngine === "local"   ?  → ensureOffscreen()
                                    └→ offscreen.html / offscreen.js
                                       └→ @huggingface/transformers pipeline
                                          → WebGPU (q4f16) → WASM (q8) fallback
                                          → IndexedDB-cached openai/privacy-filter ONNX
```

The content script never calls `fetch()`. It posts messages via
`runtime.sendMessage()` to the background service worker, which is
the only context that holds the bearer token (server engine) and
the only context that owns the offscreen document (local engine).

**MV3 quirk:** Chrome service workers don't reliably expose
`navigator.gpu` and the full ONNX-Runtime WASM stack. The extension
delegates inference to a Chrome **offscreen document**
(`chrome.offscreen.createDocument`, permission `offscreen`). Firefox
MV3 has no offscreen API but its service workers are far more
permissive — there the pipeline runs directly in the SW via a
dynamic `import("./offscreen.js")`.

**CSP:** the extension-pages policy is
`default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https:; img-src 'self' data:; style-src 'self'; object-src 'self'`.
`'wasm-unsafe-eval'` is required for ONNX-Runtime to compile its WASM
module (served from `vendor/transformers/`, listed in
`web_accessible_resources`). `connect-src 'self' https:` covers both
the Hugging Face model download and the user-configured HTTPS backend
(whose URL is dynamic and can't be pinned statically). The policy
applies to extension pages only, not to content scripts on visited
sites.

## Troubleshooting

- **First paste takes 30–90 s** — model is being downloaded from
  Hugging Face. Watch DevTools → Application → IndexedDB to see the
  cached chunks grow. Subsequent loads are instant.
- **"Modell-Lade-Fehler" badge** — check DevTools console on the
  offscreen page (`chrome://extensions` → *Privacy Guard* → *Inspect
  views: offscreen.html*). Common cause: corporate firewall blocks
  `huggingface.co`. With `fallbackToServer` enabled and a server
  configured, the extension will retry server-side automatically.
- **Free up disk** — click *Modell-Cache löschen* in the options.
  Next paste re-downloads the model.
- **`MODEL_LOADING` toast on every paste** — the model takes longer
  than a single paste cycle to compile (slow disk, no WebGPU). Wait
  until the popup shows "Local — model ready", then retry.
- **Backend offline & local engine works** — file uploads will fail
  with a clear toast; text paste is unaffected.
- **Both engines fail** — the extension fails open: pastes go through
  with a console warning, no redaction happens. Privacy regresses to
  whatever the page itself does.

## Branding

The toolbar icon, popup and options page reuse the Privacy Guard
shield from [app/static/favicon.svg](../app/static/favicon.svg).
PNG sizes are produced once by
[scripts/build-extension-icons.sh](../scripts/build-extension-icons.sh)
(uses `rsvg-convert`, `magick`, or `qlmanage` — whichever is
available) and committed under `icons/`. ZIP bundles are produced by
[scripts/package-extension.sh](../scripts/package-extension.sh),
which also runs `npm ci` + `npm run build:vendor` to populate
`vendor/transformers/` before zipping.

Screenshots for the project report can be added under
`docs/extension-overlay.png` and `docs/extension-options.png` once
captured against a live page; this README will link to them when
they exist.
