# Tasks — `@suaveplan/hibp` (privacy-preserving HIBP client) > Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify. ## 1. Spec authoring - [x] 1.1 Read `../suaveplan/enterprise/hibp.md` end-to-end - [x] 1.2 Read [HIBP API v3](https://haveibeenpwned.com/API/v3) — every endpoint, header, rate-limit, status-code anchor - [x] 1.3 Read [Pwned Passwords range API](https://haveibeenpwned.com/API/v3#PwnedPasswords) and [`Add-Padding` notes](https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding) - [x] 1.4 Read Cloudflare _Validating Leaked Passwords with k-Anonymity_ post for the privacy threat model - [x] 1.5 Read `openspec/AGENTS.md` Sections 13, 14, 14a - [x] 1.6 Draft `proposal.md` from `openspec/templates/proposal.md` - [x] 1.7 Fill in `## Source mapping` table — every requirement cites either an `enterprise/hibp.md` line range or a documented API v3 anchor - [x] 1.8 Draft `specs/hibp/spec.md` from `openspec/templates/spec-delta.md` - [x] 1.9 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles - [x] 1.10 `bun scripts/openspec-conformance.ts @suaveplan/hibp` clean ## 2. Package scaffold - [x] 2.1 `packages/security/hibp/src/{range,breach,cache,client,errors,observability,privacy,schemas}/` directories created - [x] 2.2 `package.json` — `@suaveplan/hibp@0.0.0`, `type: module`, `sideEffects: false`, deps via catalog/workspace refs (`@suaveplan/crypto-hash`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/retry`, `@suaveplan/timing`, `@opentelemetry/api`, `tslib`, `zod`) - [x] 2.3 `tsconfig.lib.json` extends `@suaveplan/typescript-config/library.json` - [x] 2.4 `vite.config.ts` and `vitest.config.ts` (latter via `createTestConfig()` from `@suaveplan/testing/config`) - [x] 2.5 `README.md` with package description, install snippet, quick-start, and `## Modules` table linking every co-located `.md` ## 3. Implementation — range API (k-anonymity) - [x] 3.1 `src/range/range.ts` — `checkPassword(password, opts?)` SHA-1s the input, slices first 5 hex chars, dispatches range fetch, returns `{ pwned, count }` - [x] 3.2 `src/range/range.ts` — sets `Add-Padding: true` on every outgoing request (mandatory; not configurable off) - [x] 3.3 `src/range/range-mode.ts` — `RangeMode = "sha1" | "ntlm"`; NTLM appends `?mode=ntlm` and routes to the NTLM hash adapter - [x] 3.4 `src/range/range-stream-parse.ts` — line streamer over `Response.body` via `TextDecoderStream` and newline framing; never collects to a single string - [x] 3.5 `src/range/range-strict-frame.ts` — strict-mode validator rejecting lowercase hex / non-`/^[0-9A-F]{35}:[0-9]+$/` lines with `HIBP.MALFORMED_RESPONSE` - [x] 3.6 `src/range/constant-time-compare.ts` — `constantTimeEqualsHex(a, b)` XOR-folds the full 35-char range with no early exit - [x] 3.7 `src/range/differential-privacy.ts` — `padPrefixes(real, n, rng)` returning real plus N uniformly random sibling prefixes - [x] 3.8 AbortSignal threaded through `checkPassword` and the inner streaming parser ## 4. Implementation — authenticated breach / paste API - [x] 4.1 `src/breach/breach-client.ts` — `getBreachesForAccount(email, opts?)` builds `https://haveibeenpwned.com/api/v3/breachedaccount/{email}?truncateResponse=false&includeUnverified=true`; email URL-encoded; `hibp-api-key` header set - [x] 4.2 `src/breach/breach-client.ts` — `getBreach(name)` against `/breach/{name}` - [x] 4.3 `src/breach/breach-client.ts` — `getAllBreaches(opts?)` against `/breaches` with optional `domain` query param - [x] 4.4 `src/breach/breach-client.ts` — `getPastesForAccount(email)` against `/pasteaccount/{email}` - [x] 4.5 `src/breach/rate-limit-gate.ts` — `RateLimitGate` enforcing 1500 ms minimum gap on breach + paste endpoints; injectable `Clock` - [x] 4.6 `src/breach/retry-after.ts` — `parseRetryAfter` honours both delta-seconds and HTTP-date forms; integrates with `@suaveplan/retry` `RetryPolicy`; cap at 60 s - [x] 4.7 `src/breach/breach-client.ts` — never logs the email, never logs `hibp-api-key`, never echoes them into thrown errors - [x] 4.8 AbortSignal threaded through every public method ## 5. Implementation — cache layer - [x] 5.1 `src/cache/cache.ts` — `HibpCacheLike` interface (`get`, `set`, `delete`, `size`) - [x] 5.2 `src/cache/lru.ts` — `LruHibpCache` with default 10 000 entries, 24 h TTL, key normalization to uppercase hex - [x] 5.3 `src/cache/null.ts` — `NullHibpCache` always-miss adapter - [x] 5.4 `HibpClientOptions.cache?: HibpCacheLike` accepted; `cache: null` disables; absent uses `LruHibpCache` default ## 6. Implementation — schemas and errors - [x] 6.1 `src/schemas/breach.schema.ts` — `BreachSchema` matching API v3 `Breach` shape (every field, including `IsStealerLog`) - [x] 6.2 `src/schemas/paste.schema.ts` — `PasteSchema` matching API v3 `Paste` shape - [x] 6.3 `src/schemas/pwned-result.schema.ts` — `PwnedResultSchema` (`pwned: boolean`, `count: int >= 0`) - [x] 6.4 `src/client/options.schema.ts` — `HibpClientOptionsSchema` strict object; rejects `apiKey === ""`, `extraPrefixes < 0`, unknown keys - [x] 6.5 `src/errors/codes.ts` — `defineCodes("HIBP", [...])` with all seven codes (`RATE_LIMITED`, `UNAUTHORIZED`, `NOT_FOUND`, `MALFORMED_RESPONSE`, `NETWORK_ERROR`, `ABORTED`, `PAYLOAD_TOO_LARGE`) - [x] 6.6 `src/errors/hibp-error.ts` — `HibpError extends SuaveplanError` with stable JSON form - [x] 6.7 Every code in the registry thrown by ≥1 production path (no decorative entries) ## 7. Implementation — observability - [x] 7.1 `src/observability/metrics.ts` — `hibp.range.duration_ms` and `hibp.breach.duration_ms` histograms - [x] 7.2 `src/observability/metrics.ts` — `hibp.range.outcome_total{outcome=pwned|clean|error}` counter - [x] 7.3 `src/observability/metrics.ts` — `hibp.errors_total{code}` counter - [x] 7.4 `src/observability/metrics.ts` — `hibp.rate_limit_throttled_total` counter - [x] 7.5 Histograms recorded on every `checkPassword`, `getBreachesForAccount`, `getBreach`, `getAllBreaches`, `getPastesForAccount` exit path ## 8. Implementation — privacy assertions - [x] 8.1 `src/privacy/assert-no-leak.ts` — scans an outgoing `Request` URL and body byte-by-byte for the full SHA-1 hash, the full password, and the configured email - [x] 8.2 `HibpClientOptions.assertOutgoingRequestSafe?: boolean` opt-in production gate (default off, defence-in-depth) - [x] 8.3 Privacy assertion thrown errors carry `HIBP.MALFORMED_RESPONSE` (privacy violation is a malformed-self-state condition) ## 9. Client orchestrator + barrels - [x] 9.1 `src/client/client.ts` — `HibpClient` composes range + breach + cache + rate-limit + observability + privacy - [x] 9.2 Constructor takes `HibpClientOptions` (api key, fetch, clock, rng, cache, rate-limit gate, observability, strict mode, differential-privacy mode, opt-in privacy assertions) - [x] 9.3 `src/index.ts` — explicit named re-exports across all sub-namespaces (no `export *` per CLAUDE.md rule 10) - [x] 9.4 `package.json` `exports` map covers every documented subpath ## 10. Documentation - [x] 10.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference) - [x] 10.2 `README.md` `## Modules` section links every co-located `.md` - [x] 10.3 README documents the privacy contract verbatim (full password and full hash never leave the function) - [x] 10.4 README documents the differential-privacy mode trade-offs ## 11. Tests (runner-neutral imports per CLAUDE.md rule 11) - [x] 11.1 `range/range.test.ts` — k-anonymity slice, `Add-Padding` header, NTLM mode, OTel emission, AbortSignal - [x] 11.2 `range/range-stream-parse.test.ts` — multi-chunk 30 KiB streaming decode without single-string buffering - [x] 11.3 `range/range-strict-frame.test.ts` — strict-mode rejects lowercase / malformed `:count` / non-uppercase hex - [x] 11.4 `range/range-mode.test.ts` — NTLM URL + adapter routing - [x] 11.5 `range/constant-time-compare.test.ts` — every byte touched on both equal and unequal inputs; differential-timing test - [x] 11.6 `range/differential-privacy.test.ts` — `padPrefixes` returns N+1 entries with real present - [x] 11.7 `breach/breach-client.test.ts` — every endpoint URL, query, header; AbortSignal; Zod validation - [x] 11.8 `breach/rate-limit-gate.test.ts` — 1500 ms gate; injectable clock; `hibp.rate_limit_throttled_total` increments - [x] 11.9 `breach/retry-after.test.ts` — delta-seconds and HTTP-date forms; 60 s cap; `@suaveplan/retry` integration - [x] 11.10 `cache/cache.test.ts` — interface type-level conformance - [x] 11.11 `cache/lru.test.ts` — capacity eviction; TTL eviction; uppercase normalization - [x] 11.12 `cache/null.test.ts` — always-miss; `set` is a no-op - [x] 11.13 `client/client.test.ts` — full composition; `cache: null` disables; default `LruHibpCache` - [x] 11.14 `client/options.schema.test.ts` — every Zod rejection path - [x] 11.15 `schemas/breach.schema.test.ts` — recorded `/breach/Adobe` fixture round-trip - [x] 11.16 `schemas/paste.schema.test.ts` — recorded `/pasteaccount/...` fixture round-trip - [x] 11.17 `schemas/pwned-result.schema.test.ts` — accepts valid; rejects negative count - [x] 11.18 `errors/codes.test.ts` — registry shape, no duplicate keys - [x] 11.19 `errors/hibp-error.test.ts` — JSON serialization, `code` + `cause` carriage - [x] 11.20 `observability/metrics.test.ts` — every histogram and counter against an in-memory `MeterProvider` - [x] 11.21 `privacy/assert-no-leak.test.ts` — full SHA-1 → throws; prefix-only → passes; full password → throws; email → throws - [x] 11.22 `privacy/range-no-leak.spec-conformance.test.ts` — 1 000-iteration `checkPassword("hunter2")` interception; zero leaks across all outgoing URLs and bodies - [x] 11.23 `privacy/breach-no-log.spec-conformance.test.ts` — `LoggerLike` capture; email never appears in logs - [x] 11.24 `index.test.ts` — every documented subpath export resolves - [x] 11.25 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for network-bound integration cases — none expected since fetch is injected) - [x] 11.26 Runner-neutral imports — CLAUDE.md §11 grep clean ## 12. Benchmarks (mitata) - [x] 12.1 `src/__benchmarks__/range-cold.bench.ts` — cold range fetch + parse - [x] 12.2 `src/__benchmarks__/range-warm-cache.bench.ts` — LRU hit path - [x] 12.3 `src/__benchmarks__/range-parse-30kib.bench.ts` — streaming parser throughput on a 30 KiB padded fixture - [x] 12.4 Baseline numbers captured in README ## 13. Pre-archive verification (the 5-gate triple) - [x] 13.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info - [x] 13.2 `bunx turbo lint typecheck test --filter=@suaveplan/hibp` — green - [x] 13.3 `bunx turbo test:coverage --filter=@suaveplan/hibp` — 100/100/100/100 - [x] 13.4 `bun scripts/openspec-conformance.ts @suaveplan/hibp` — exit 0, no `error` findings - [x] 13.5 `bunx turbo build --filter=@suaveplan/hibp` — green ## 14. Pre-archive 7-row checklist (AGENTS.md Section 13) - [x] 14.1 Generics threaded; `expectTypeOf` assertions on `HibpCacheLike` and `HibpClientOptions` - [x] 14.2 Zod schemas wired at every declared boundary (range result, breach response, paste response, options) - [x] 14.3 Every error code in `errors/codes.ts` thrown by at least one production path - [x] 14.4 Strict-mode framing tested both on (default) and off - [x] 14.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider` - [x] 14.6 Lifecycle dedup — N/A (`HibpClient` is stateless beyond its cache; cache `set` is idempotent and tested) - [x] 14.7 No retroactive tick — every box above reflects work done in this change ## 15. Archive - [x] 15.1 Fold spec delta into `openspec/specs/hibp/spec.md` - [x] 15.2 Move `openspec/changes/enterprise-hibp/` → `openspec/archive/-enterprise-hibp/` - [x] 15.3 Refresh `MANIFEST.md` (`bun scripts/folder-reorg/generate-manifest.ts`) - [x] 15.4 Update `STATUS.md` row from Pending → Completed - [x] 15.5 Update `CLAUDE.md` package listing under `#### Security / supply chain`