diff --git a/.changeset/extension-pii-config-wiring.md b/.changeset/extension-pii-config-wiring.md new file mode 100644 index 00000000..15b48816 --- /dev/null +++ b/.changeset/extension-pii-config-wiring.md @@ -0,0 +1,5 @@ +--- +"kiji-privacy-proxy": minor +--- + +Chrome extension: configure PII entity types and custom regex patterns from the options page, wired to the `/api/pii/entities` and `/api/pii/regexes` endpoints. diff --git a/chrome-extension/options.css b/chrome-extension/options.css index 881b9dcb..7952ffde 100644 --- a/chrome-extension/options.css +++ b/chrome-extension/options.css @@ -79,7 +79,9 @@ body { min-height: 100vh; padding: 48px 24px; display: flex; - justify-content: center; + flex-direction: column; + align-items: center; + gap: 24px; } .card { @@ -307,3 +309,136 @@ input[type="url"]:focus, .save-error { color: var(--err); } + +/* ---------- Label grid (PII types) ---------- */ + +.label-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 8px; + margin-bottom: 20px; +} + +.label-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-family: var(--mono); + color: var(--text); + cursor: pointer; + padding: 6px 8px; + border-radius: var(--radius-sm); + background: var(--bg-subtle); + user-select: none; +} + +.label-item input[type="checkbox"] { + accent-color: var(--brand); + width: 14px; + height: 14px; + flex-shrink: 0; + cursor: pointer; +} + +.label-loading { + font-size: 12px; + color: var(--text-muted); +} + +/* ---------- Custom patterns ---------- */ + +.pattern-form { + margin-bottom: 16px; +} + +.pattern-form-row { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.pattern-form-row .input-mono { + flex: 1; + min-width: 120px; +} + +.pattern-preview-row { + display: flex; + gap: 8px; + align-items: center; +} + +.pattern-preview-row .input-mono { + flex: 1; +} + +.pattern-preview-result { + font-size: 12px; + font-family: var(--mono); + color: var(--text-muted); + white-space: nowrap; +} + +.pattern-error { + color: var(--err); + min-height: 1.2em; +} + +.pattern-list { + display: flex; + flex-direction: column; + gap: 6px; + border-top: 0.5px solid var(--border); + padding-top: 16px; +} + +.pattern-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + background: var(--bg-subtle); + border-radius: var(--radius-sm); + font-size: 12px; + flex-wrap: wrap; +} + +.pattern-name { + font-family: var(--mono); + font-weight: 600; + color: var(--brand); + flex-shrink: 0; +} + +.pattern-regex-val { + font-family: var(--mono); + color: var(--code-text); + flex: 1; + word-break: break-all; +} + +.btn-danger { + padding: 5px 10px; + background: transparent; + color: var(--err); + border: 0.5px solid var(--err); + border-radius: var(--radius-sm); + font-size: 12px; + cursor: pointer; + flex-shrink: 0; + transition: background 0.15s ease; +} + +.btn-danger:hover { + background: rgba(226, 75, 74, 0.1); +} + +.pattern-row .input-mono { + padding: 5px 8px; + font-size: 12px; + flex: 1; + min-width: 80px; +} diff --git a/chrome-extension/options.html b/chrome-extension/options.html index 48ddcda3..67472d3f 100644 --- a/chrome-extension/options.html +++ b/chrome-extension/options.html @@ -73,6 +73,73 @@ + +
No custom patterns yet.
'; + return; + } + + patternList.innerHTML = ""; + regexes.forEach((p, idx) => { + const row = document.createElement("div"); + row.className = "pattern-row"; + row.innerHTML = ` + ${escHtml(p.name)} +${escHtml(p.pattern)}
+
+ `;
+ patternList.appendChild(row);
+ });
+
+ patternList.querySelectorAll("[data-action=delete]").forEach((btn) => {
+ btn.addEventListener("click", async () => {
+ const idx = Number(btn.dataset.idx);
+ const removed = regexes[idx];
+ regexes = regexes.filter((_, i) => i !== idx);
+ try {
+ await savePatterns();
+ } catch (e) {
+ regexes.splice(idx, 0, removed);
+ renderPatterns();
+ alert(`Failed to delete pattern: ${e.message}`);
+ }
+ });
+ });
+ }
+
+ patternForm.addEventListener("submit", async (e) => {
+ e.preventDefault();
+ const name = patternName.value.trim().toUpperCase();
+ const pattern = patternRegex.value.trim();
+
+ const error = validateRegex(pattern);
+ if (error) {
+ patternRegexError.textContent = error;
+ return;
+ }
+
+ regexes.push({ name, pattern });
+ try {
+ await savePatterns();
+ patternForm.reset();
+ patternPreviewResult.textContent = "";
+ } catch (err) {
+ regexes.pop();
+ patternRegexError.textContent = `Failed to save: ${err.message}`;
+ }
+ });
+
+ function escHtml(str) {
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ }
+
+ loadPatterns();
});
diff --git a/docs/06-chrome-extension.md b/docs/06-chrome-extension.md
index ae0eeb19..996d9abb 100644
--- a/docs/06-chrome-extension.md
+++ b/docs/06-chrome-extension.md
@@ -56,7 +56,7 @@ chrome-extension/
├── content.js # Content script: intercepts input, calls API, shows PII modal
├── styles.css # Modal and toast styles (injected into target pages)
├── popup.html/js/css # Extension popup: connection status, stats
-├── options.html/js/css# Settings page: backend URL, intercept domains
+├── options.html/js/css# Settings page: backend URL, intercept domains, PII entity types, custom patterns
└── icons/ # Extension icons (16, 48, 128)
```
@@ -73,6 +73,8 @@ All settings are accessible via the extension's options page (right-click extens
- **Backend URL** — The Kiji Privacy Proxy server address (default: `http://localhost:8081`)
- **Intercept domains** — URL match patterns where the extension is active (one per line)
+- **PII entity types** — Checkbox grid of the entity types the active model can detect (fetched from `GET /api/pii/entities`). Unchecking a type adds it to the backend's `disabled` (passthrough) set so it is left unmasked; the selection is saved with `POST /api/pii/entities`. The set is stored server-side, so it also applies to the proxy pipeline and the Linux server.
+- **Custom patterns** — Add named regular expressions (`name` + `pattern`) that are masked on top of the model's detections. A live preview tests the regex against sample text before saving. Patterns are read from `GET /api/pii/regexes` and the whole set is replaced with `POST /api/pii/regexes` on each add/remove. Each custom name also becomes a selectable entity type in the grid above.
Default domains:
```