From 6af03ea8078e5628afa098ecc06edb5999a0c31b Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 30 Jun 2026 23:07:43 +0200 Subject: [PATCH] docs: switch Privacy Monitor default to XposedOrNot (free, keyless) Email breach endpoints need no API key (2 req/s, cached), so Privacy Monitor ships enabled by default. HIBP kept as a swappable key-based alternative; provider chosen via Privacy:Provider config. Co-Authored-By: Claude Sonnet 4.6 --- docs/specs/README.md | 2 +- docs/specs/feature-privacy-monitor.md | 88 ++++++++++++++++++++------- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/docs/specs/README.md b/docs/specs/README.md index 676fcf7..58e9e33 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -11,7 +11,7 @@ files, and the patterns already in the codebase. | **Automation safety** | **Hybrid.** Safe, reversible actions (label, archive/skip-inbox, mark-read, star, move-to-label) apply automatically. Destructive actions (trash, delete, keep-newest culling, trash-by-age) are **proposed** and require one-click user approval before touching Gmail. | | **Scope** | **Tier 1 + 2 only.** Gmail-only. **No sending** (no Compose/Reply/Forward). No multi-provider. | | **Gmail vs in-app** | **Touch real Gmail.** Screener/Block/Pause/Read-Later/Deliver-To use managed `InboxIntel/…` labels + skip-inbox so the inbox is clean everywhere (phone, web). All reversible. | -| **Privacy Monitor** | Spec around Have I Been Pwned; ship behind a config flag, **off until a key is added**. No cost now. | +| **Privacy Monitor** | Use **XposedOrNot** (free, no API key for email endpoints) as the default provider, **on by default**. HIBP kept as a swappable key-based alternative. Only ever checks the signed-in user's own address. | | **UI aesthetic** | **Stripe / Notion** — light, airy, generous whitespace, soft shadows. | | **Color modes** | **Light + dark toggle**, light-first, driven by CSS-variable design tokens. | | **UI stack** | **Tailwind CSS + shadcn-style** headless primitives (Radix + cva + tailwind-merge), hand-built component set. | diff --git a/docs/specs/feature-privacy-monitor.md b/docs/specs/feature-privacy-monitor.md index ef970a4..25516ca 100644 --- a/docs/specs/feature-privacy-monitor.md +++ b/docs/specs/feature-privacy-monitor.md @@ -1,10 +1,28 @@ # Spec: Privacy Monitor (data-breach checking) -Checks the user's email address (and optionally addresses they've corresponded with) -against known data breaches. Spec'd around **Have I Been Pwned (HIBP)**, shipped behind -a config flag that stays **off until an API key is provided** — zero cost until then. +Checks the user's email address against known data breaches. **Default provider: +[XposedOrNot](https://xposedornot.com/api_doc)** — free, no API key required for email +endpoints. HIBP is kept as a swappable alternative behind a key. Because it's free and +keyless, Privacy Monitor ships **enabled by default** (it only ever checks the +signed-in user's own address). -## 1. Configuration +## 1. Provider details (XposedOrNot) + +No auth for email endpoints. Rate limit: **2 req/sec per IP** (we cache, so this is a +non-issue). Two endpoints: + +| Purpose | Request | Returns | +|---------|---------|---------| +| Quick check | `GET https://api.xposedornot.com/v1/check-email/{email}` | `{ "breaches": [["Name1","Name2",…]], "email", "status":"success" }`; or `{ "Error":"Not found", "email":null }` when clean | +| **Rich analytics** (what we use) | `GET https://api.xposedornot.com/v1/breach-analytics?email={email}` | `BreachesSummary`, **`ExposedBreaches`** (entity name, industry, risk level, exposed data types, year, record count), `BreachMetrics`, `ExposedPastes` | + +We call **`breach-analytics`** to populate the rich UI. A `404`/`Error:"Not found"` +means "no breaches" → return empty, not an error. We call the REST API directly via a +named `HttpClient` (consistent with the existing `ollama`/`openai`/`unsubscribe` +clients); the official `XposedOrNot-DotNet` SDK exists but we avoid the extra dependency ++ audit surface. + +## 2. Configuration `PrivacyOptions` (new, `Configuration/Options.cs`): @@ -12,21 +30,35 @@ a config flag that stays **off until an API key is provided** — zero cost unti public class PrivacyOptions { public const string SectionName = "Privacy"; - public bool Enabled { get; set; } = false; // master flag - public string Provider { get; set; } = "Hibp"; // "Hibp" | "None" - public string? HibpApiKey { get; set; } // required for Hibp - public int CacheHours { get; set; } = 24; // don't hammer the API + public bool Enabled { get; set; } = true; // free + keyless → on by default + public string Provider { get; set; } = "XposedOrNot"; // "XposedOrNot" | "Hibp" | "None" + public string? HibpApiKey { get; set; } // only needed if Provider="Hibp" + public int CacheHours { get; set; } = 24; // don't re-check more than daily } ``` -`appsettings.json` gains a `Privacy` section with `Enabled: false`, empty key. -`IsEnabled => Enabled && Provider == "Hibp" && !string.IsNullOrWhiteSpace(HibpApiKey)`. - -## 2. Provider abstraction +`appsettings.json` gains a `Privacy` section: `Enabled: true`, `Provider: +"XposedOrNot"`, empty `HibpApiKey`. ```csharp -public record BreachDto(string Name, string Title, string Domain, DateOnly BreachDate, - IReadOnlyList DataClasses, bool IsVerified, string? Description); +// XposedOrNot needs no key; Hibp does. +IsEnabled => Enabled && Provider switch { + "XposedOrNot" => true, + "Hibp" => !string.IsNullOrWhiteSpace(HibpApiKey), + _ => false +}; +``` + +> **Privacy note:** this sends the user's own email address to a third-party service. +> That's the feature's purpose and it's the signed-in user's own address, but it stays +> a single config flag away from off, and we never check anyone else's address. + +## 3. Provider abstraction + +```csharp +public record BreachDto(string Name, string Title, string? Domain, int? Year, + IReadOnlyList DataClasses, string? RiskLevel, + string? Industry, string? LogoUrl, string? Description); public interface IBreachProvider { @@ -36,12 +68,20 @@ public interface IBreachProvider } ``` +- **`XposedOrNotBreachProvider`** (default) — `GET /v1/breach-analytics?email={url-encoded}` + on the `"xposedornot"` named client. Map `ExposedBreaches.breaches_details[]` + (`breach`, `xposed_data` → `DataClasses`, `xposed_date`/`year`, `industry`, + `risk` → `RiskLevel`, `logo`, `details` → `Description`) into `BreachDto`. Treat + `Error:"Not found"` / `404` as clean (empty). `429` → respect backoff, return + cached/empty. No key, no `user-agent` requirement. - `HibpBreachProvider` — `GET https://haveibeenpwned.com/api/v3/breachedaccount/{account}?truncateResponse=false`, - header `hibp-api-key`, a descriptive `user-agent`. Handle: `404` = no breaches (return - empty), `401` = misconfig (log, treat as disabled), `429` = rate-limited (respect - `Retry-After`, return cached/empty). Uses a named `HttpClient` "hibp" with a sane timeout. -- `NullBreachProvider` — `IsEnabled => false`, returns empty. Registered when the flag is off - (mirrors the `NullAiProvider` pattern in `DependencyInjection`). + header `hibp-api-key`, descriptive `user-agent`. `404` = clean, `401` = misconfig (log, + disable), `429` = back off. Used only when `Provider="Hibp"` + key set. +- `NullBreachProvider` — `IsEnabled => false`, returns empty. Registered when + `Provider="None"` or the chosen provider isn't usable (mirrors `NullAiProvider`). + +DI selects the provider by `PrivacyOptions.Provider` (switch in `DependencyInjection`, +same shape as the AI provider selection). ## 3. Service @@ -85,7 +125,9 @@ never arbitrary addresses (avoids turning the app into a breach-lookup tool for ## 6. Security & safety - [ ] Only the authenticated user's own address is ever checked (no lookup of others). -- [ ] API key read from config/secrets, never logged, never sent to the client. -- [ ] Feature fully inert (endpoints return `{ enabled:false }`, nav hidden) until a key is set. -- [ ] Rate-limit/backoff respected; failures degrade to cached/empty, never crash. -- [ ] HIBP responses cached to minimize external calls and avoid leaking usage patterns. +- [ ] Any API key (HIBP path) read from config/secrets, never logged, never sent to the client. +- [ ] Feature can be fully disabled via `Privacy:Enabled=false` or `Provider="None"` + (endpoints return `{ enabled:false }`, nav hidden). +- [ ] Rate-limit/backoff respected (XposedOrNot 2 req/s); failures degrade to cached/empty, never crash. +- [ ] Provider responses cached (`CacheHours`) to minimize external calls and avoid leaking usage patterns. +- [ ] Email is URL-encoded into the request path/query; no other PII is sent.