Files
2026-05-27 23:29:53 +01:00

12 KiB

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

  • 1.1 Read ../suaveplan/enterprise/hibp.md end-to-end
  • 1.2 Read HIBP API v3 — every endpoint, header, rate-limit, status-code anchor
  • 1.3 Read Pwned Passwords range API and Add-Padding notes
  • 1.4 Read Cloudflare Validating Leaked Passwords with k-Anonymity post for the privacy threat model
  • 1.5 Read openspec/AGENTS.md Sections 13, 14, 14a
  • 1.6 Draft proposal.md from openspec/templates/proposal.md
  • 1.7 Fill in ## Source mapping table — every requirement cites either an enterprise/hibp.md line range or a documented API v3 anchor
  • 1.8 Draft specs/hibp/spec.md from openspec/templates/spec-delta.md
  • 1.9 Every requirement in the spec delta carries a **Verified by:** line naming concrete it(...) titles
  • 1.10 bun scripts/openspec-conformance.ts @suaveplan/hibp clean

2. Package scaffold

  • 2.1 packages/security/hibp/src/{range,breach,cache,client,errors,observability,privacy,schemas}/ directories created
  • 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)
  • 2.3 tsconfig.lib.json extends @suaveplan/typescript-config/library.json
  • 2.4 vite.config.ts and vitest.config.ts (latter via createTestConfig() from @suaveplan/testing/config)
  • 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)

  • 3.1 src/range/range.ts — checkPassword(password, opts?) SHA-1s the input, slices first 5 hex chars, dispatches range fetch, returns { pwned, count }
  • 3.2 src/range/range.ts — sets Add-Padding: true on every outgoing request (mandatory; not configurable off)
  • 3.3 src/range/range-mode.ts — RangeMode = "sha1" | "ntlm"; NTLM appends ?mode=ntlm and routes to the NTLM hash adapter
  • 3.4 src/range/range-stream-parse.ts — line streamer over Response.body via TextDecoderStream and newline framing; never collects to a single string
  • 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
  • 3.6 src/range/constant-time-compare.ts — constantTimeEqualsHex(a, b) XOR-folds the full 35-char range with no early exit
  • 3.7 src/range/differential-privacy.ts — padPrefixes(real, n, rng) returning real plus N uniformly random sibling prefixes
  • 3.8 AbortSignal threaded through checkPassword and the inner streaming parser

4. Implementation — authenticated breach / paste API

  • 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
  • 4.2 src/breach/breach-client.ts — getBreach(name) against /breach/{name}
  • 4.3 src/breach/breach-client.ts — getAllBreaches(opts?) against /breaches with optional domain query param
  • 4.4 src/breach/breach-client.ts — getPastesForAccount(email) against /pasteaccount/{email}
  • 4.5 src/breach/rate-limit-gate.ts — RateLimitGate enforcing 1500 ms minimum gap on breach + paste endpoints; injectable Clock
  • 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
  • 4.7 src/breach/breach-client.ts — never logs the email, never logs hibp-api-key, never echoes them into thrown errors
  • 4.8 AbortSignal threaded through every public method

5. Implementation — cache layer

  • 5.1 src/cache/cache.ts — HibpCacheLike<V> interface (get, set, delete, size)
  • 5.2 src/cache/lru.ts — LruHibpCache with default 10 000 entries, 24 h TTL, key normalization to uppercase hex
  • 5.3 src/cache/null.ts — NullHibpCache always-miss adapter
  • 5.4 HibpClientOptions.cache?: HibpCacheLike accepted; cache: null disables; absent uses LruHibpCache default

6. Implementation — schemas and errors

  • 6.1 src/schemas/breach.schema.ts — BreachSchema matching API v3 Breach shape (every field, including IsStealerLog)
  • 6.2 src/schemas/paste.schema.ts — PasteSchema matching API v3 Paste shape
  • 6.3 src/schemas/pwned-result.schema.ts — PwnedResultSchema (pwned: boolean, count: int >= 0)
  • 6.4 src/client/options.schema.ts — HibpClientOptionsSchema strict object; rejects apiKey === "", extraPrefixes < 0, unknown keys
  • 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)
  • 6.6 src/errors/hibp-error.ts — HibpError extends SuaveplanError<HibpErrorCode> with stable JSON form
  • 6.7 Every code in the registry thrown by ≥1 production path (no decorative entries)

7. Implementation — observability

  • 7.1 src/observability/metrics.ts — hibp.range.duration_ms and hibp.breach.duration_ms histograms
  • 7.2 src/observability/metrics.ts — hibp.range.outcome_total{outcome=pwned|clean|error} counter
  • 7.3 src/observability/metrics.ts — hibp.errors_total{code} counter
  • 7.4 src/observability/metrics.ts — hibp.rate_limit_throttled_total counter
  • 7.5 Histograms recorded on every checkPassword, getBreachesForAccount, getBreach, getAllBreaches, getPastesForAccount exit path

8. Implementation — privacy assertions

  • 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
  • 8.2 HibpClientOptions.assertOutgoingRequestSafe?: boolean opt-in production gate (default off, defence-in-depth)
  • 8.3 Privacy assertion thrown errors carry HIBP.MALFORMED_RESPONSE (privacy violation is a malformed-self-state condition)

9. Client orchestrator + barrels

  • 9.1 src/client/client.ts — HibpClient composes range + breach + cache + rate-limit + observability + privacy
  • 9.2 Constructor takes HibpClientOptions (api key, fetch, clock, rng, cache, rate-limit gate, observability, strict mode, differential-privacy mode, opt-in privacy assertions)
  • 9.3 src/index.ts — explicit named re-exports across all sub-namespaces (no export * per CLAUDE.md rule 10)
  • 9.4 package.json exports map covers every documented subpath

10. Documentation

  • 10.1 Sibling .md for every implementation .ts (≥200 words, basic + advanced examples, full API reference)
  • 10.2 README.md ## Modules section links every co-located .md
  • 10.3 README documents the privacy contract verbatim (full password and full hash never leave the function)
  • 10.4 README documents the differential-privacy mode trade-offs

11. Tests (runner-neutral imports per CLAUDE.md rule 11)

  • 11.1 range/range.test.ts — k-anonymity slice, Add-Padding header, NTLM mode, OTel emission, AbortSignal
  • 11.2 range/range-stream-parse.test.ts — multi-chunk 30 KiB streaming decode without single-string buffering
  • 11.3 range/range-strict-frame.test.ts — strict-mode rejects lowercase / malformed :count / non-uppercase hex
  • 11.4 range/range-mode.test.ts — NTLM URL + adapter routing
  • 11.5 range/constant-time-compare.test.ts — every byte touched on both equal and unequal inputs; differential-timing test
  • 11.6 range/differential-privacy.test.ts — padPrefixes returns N+1 entries with real present
  • 11.7 breach/breach-client.test.ts — every endpoint URL, query, header; AbortSignal; Zod validation
  • 11.8 breach/rate-limit-gate.test.ts — 1500 ms gate; injectable clock; hibp.rate_limit_throttled_total increments
  • 11.9 breach/retry-after.test.ts — delta-seconds and HTTP-date forms; 60 s cap; @suaveplan/retry integration
  • 11.10 cache/cache.test.ts — interface type-level conformance
  • 11.11 cache/lru.test.ts — capacity eviction; TTL eviction; uppercase normalization
  • 11.12 cache/null.test.ts — always-miss; set is a no-op
  • 11.13 client/client.test.ts — full composition; cache: null disables; default LruHibpCache
  • 11.14 client/options.schema.test.ts — every Zod rejection path
  • 11.15 schemas/breach.schema.test.ts — recorded /breach/Adobe fixture round-trip
  • 11.16 schemas/paste.schema.test.ts — recorded /pasteaccount/... fixture round-trip
  • 11.17 schemas/pwned-result.schema.test.ts — accepts valid; rejects negative count
  • 11.18 errors/codes.test.ts — registry shape, no duplicate keys
  • 11.19 errors/hibp-error.test.ts — JSON serialization, code + cause carriage
  • 11.20 observability/metrics.test.ts — every histogram and counter against an in-memory MeterProvider
  • 11.21 privacy/assert-no-leak.test.ts — full SHA-1 → throws; prefix-only → passes; full password → throws; email → throws
  • 11.22 privacy/range-no-leak.spec-conformance.test.ts — 1 000-iteration checkPassword("hunter2") interception; zero leaks across all outgoing URLs and bodies
  • 11.23 privacy/breach-no-log.spec-conformance.test.ts — LoggerLike capture; email never appears in logs
  • 11.24 index.test.ts — every documented subpath export resolves
  • 11.25 Every it(...) carries { timeout: 5000 } (or 30000 for network-bound integration cases — none expected since fetch is injected)
  • 11.26 Runner-neutral imports — CLAUDE.md §11 grep clean

12. Benchmarks (mitata)

  • 12.1 src/__benchmarks__/range-cold.bench.ts — cold range fetch + parse
  • 12.2 src/__benchmarks__/range-warm-cache.bench.ts — LRU hit path
  • 12.3 src/__benchmarks__/range-parse-30kib.bench.ts — streaming parser throughput on a 30 KiB padded fixture
  • 12.4 Baseline numbers captured in README

13. Pre-archive verification (the 5-gate triple)

  • 13.1 bun run check — Biome 0 errors / 0 warnings / 0 info
  • 13.2 bunx turbo lint typecheck test --filter=@suaveplan/hibp — green
  • 13.3 bunx turbo test:coverage --filter=@suaveplan/hibp — 100/100/100/100
  • 13.4 bun scripts/openspec-conformance.ts @suaveplan/hibp — exit 0, no error findings
  • 13.5 bunx turbo build --filter=@suaveplan/hibp — green

14. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 14.1 Generics threaded; expectTypeOf assertions on HibpCacheLike<V> and HibpClientOptions
  • 14.2 Zod schemas wired at every declared boundary (range result, breach response, paste response, options)
  • 14.3 Every error code in errors/codes.ts thrown by at least one production path
  • 14.4 Strict-mode framing tested both on (default) and off
  • 14.5 OTel instruments emitted from production paths and asserted with a real in-memory MeterProvider
  • 14.6 Lifecycle dedup — N/A (HibpClient is stateless beyond its cache; cache set is idempotent and tested)
  • 14.7 No retroactive tick — every box above reflects work done in this change

15. Archive

  • 15.1 Fold spec delta into openspec/specs/hibp/spec.md
  • 15.2 Move openspec/changes/enterprise-hibp/ → openspec/archive/<yyyy-mm-dd>-enterprise-hibp/
  • 15.3 Refresh MANIFEST.md (bun scripts/folder-reorg/generate-manifest.ts)
  • 15.4 Update STATUS.md row from Pending → Completed
  • 15.5 Update CLAUDE.md package listing under #### Security / supply chain