Files
Inboxintel/docs/specs/feature-privacy-monitor.md
T
cesnimda 6af03ea807 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 <noreply@anthropic.com>
2026-06-30 23:07:43 +02:00

6.4 KiB

Spec: Privacy Monitor (data-breach checking)

Checks the user's email address against known data breaches. Default provider: XposedOrNot — 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. 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):

public class PrivacyOptions
{
    public const string SectionName = "Privacy";
    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: Enabled: true, Provider: "XposedOrNot", empty HibpApiKey.

// 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

public record BreachDto(string Name, string Title, string? Domain, int? Year,
                        IReadOnlyList<string> DataClasses, string? RiskLevel,
                        string? Industry, string? LogoUrl, string? Description);

public interface IBreachProvider
{
    bool IsEnabled { get; }
    /// <summary>Breaches for an address; empty list if clean; throws only on hard errors.</summary>
    Task<IReadOnlyList<BreachDto>> CheckAsync(string emailAddress, CancellationToken ct = default);
}
  • XposedOrNotBreachProvider (default) — GET /v1/breach-analytics?email={url-encoded} on the "xposedornot" named client. Map ExposedBreaches.breaches_details[] (breach, xposed_dataDataClasses, xposed_date/year, industry, riskRiskLevel, logo, detailsDescription) into BreachDto. Treat Error:"Not found" / 404 as clean (empty). 429 → respect backoff, return cached/empty. No key, no user-agent requirement.
  • HibpBreachProviderGET https://haveibeenpwned.com/api/v3/breachedaccount/{account}?truncateResponse=false, header hibp-api-key, descriptive user-agent. 404 = clean, 401 = misconfig (log, disable), 429 = back off. Used only when Provider="Hibp" + key set.
  • NullBreachProviderIsEnabled => 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

public interface IPrivacyService
{
    bool IsEnabled { get; }
    /// <summary>Check the signed-in user's own address. Cached per PrivacyOptions.CacheHours.</summary>
    Task<PrivacyReportDto> CheckSelfAsync(Guid userId, CancellationToken ct = default);
}

public record PrivacyReportDto(string Address, bool Checked, int BreachCount,
                               IReadOnlyList<BreachDto> Breaches, DateTimeOffset? LastCheckedUtc);
  • Resolve the user's address from User/identity.
  • Cache the last result + timestamp (new User.LastBreachCheckUtc + a small BreachCheck/JSON column, or a dedicated table if we later check multiple addresses). For v1, store on User: LastBreachCheckUtc, BreachCountCached.
  • Respect CacheHours: return cached unless stale or force requested.

4. API (PrivacyController : ApiControllerBase)

Method Route Purpose
GET /privacy/status { enabled } so the UI can hide the feature when off
GET /privacy/self cached report for the signed-in user
POST /privacy/self/refresh force a fresh check (rate-limit aware)

All scoped to UserId. We only check the authenticated user's own address in v1 — never arbitrary addresses (avoids turning the app into a breach-lookup tool for others).

5. Frontend

  • Privacy page (/app/privacy), hidden from nav when GET /privacy/status is disabled.
  • Shows: address checked, breach count, and a card per breach (title, date, what leaked, verified badge), plus a "Re-check" button and "what this means / next steps" guidance.
  • Dashboard widget (optional): a small "Privacy" tile with breach count + link, also hidden when disabled.

6. Security & safety

  • Only the authenticated user's own address is ever checked (no lookup of others).
  • 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.