12 KiB
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.mdend-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-Paddingnotes - 1.4 Read Cloudflare Validating Leaked Passwords with k-Anonymity post for the privacy threat model
- 1.5 Read
openspec/AGENTS.mdSections 13, 14, 14a - 1.6 Draft
proposal.mdfromopenspec/templates/proposal.md - 1.7 Fill in
## Source mappingtable — every requirement cites either anenterprise/hibp.mdline range or a documented API v3 anchor - 1.8 Draft
specs/hibp/spec.mdfromopenspec/templates/spec-delta.md - 1.9 Every requirement in the spec delta carries a
**Verified by:**line naming concreteit(...)titles - 1.10
bun scripts/openspec-conformance.ts @suaveplan/hibpclean
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.jsonextends@suaveplan/typescript-config/library.json - 2.4
vite.config.tsandvitest.config.ts(latter viacreateTestConfig()from@suaveplan/testing/config) - 2.5
README.mdwith package description, install snippet, quick-start, and## Modulestable 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— setsAdd-Padding: trueon every outgoing request (mandatory; not configurable off) - 3.3
src/range/range-mode.ts—RangeMode = "sha1" | "ntlm"; NTLM appends?mode=ntlmand routes to the NTLM hash adapter - 3.4
src/range/range-stream-parse.ts— line streamer overResponse.bodyviaTextDecoderStreamand 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 withHIBP.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
checkPasswordand the inner streaming parser
4. Implementation — authenticated breach / paste API
- 4.1
src/breach/breach-client.ts—getBreachesForAccount(email, opts?)buildshttps://haveibeenpwned.com/api/v3/breachedaccount/{email}?truncateResponse=false&includeUnverified=true; email URL-encoded;hibp-api-keyheader set - 4.2
src/breach/breach-client.ts—getBreach(name)against/breach/{name} - 4.3
src/breach/breach-client.ts—getAllBreaches(opts?)against/breacheswith optionaldomainquery param - 4.4
src/breach/breach-client.ts—getPastesForAccount(email)against/pasteaccount/{email} - 4.5
src/breach/rate-limit-gate.ts—RateLimitGateenforcing 1500 ms minimum gap on breach + paste endpoints; injectableClock - 4.6
src/breach/retry-after.ts—parseRetryAfterhonours both delta-seconds and HTTP-date forms; integrates with@suaveplan/retryRetryPolicy; cap at 60 s - 4.7
src/breach/breach-client.ts— never logs the email, never logshibp-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—LruHibpCachewith default 10 000 entries, 24 h TTL, key normalization to uppercase hex - 5.3
src/cache/null.ts—NullHibpCachealways-miss adapter - 5.4
HibpClientOptions.cache?: HibpCacheLikeaccepted;cache: nulldisables; absent usesLruHibpCachedefault
6. Implementation — schemas and errors
- 6.1
src/schemas/breach.schema.ts—BreachSchemamatching API v3Breachshape (every field, includingIsStealerLog) - 6.2
src/schemas/paste.schema.ts—PasteSchemamatching API v3Pasteshape - 6.3
src/schemas/pwned-result.schema.ts—PwnedResultSchema(pwned: boolean,count: int >= 0) - 6.4
src/client/options.schema.ts—HibpClientOptionsSchemastrict object; rejectsapiKey === "",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_msandhibp.breach.duration_mshistograms - 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_totalcounter - 7.5 Histograms recorded on every
checkPassword,getBreachesForAccount,getBreach,getAllBreaches,getPastesForAccountexit path
8. Implementation — privacy assertions
- 8.1
src/privacy/assert-no-leak.ts— scans an outgoingRequestURL and body byte-by-byte for the full SHA-1 hash, the full password, and the configured email - 8.2
HibpClientOptions.assertOutgoingRequestSafe?: booleanopt-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—HibpClientcomposes 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 (noexport *per CLAUDE.md rule 10) - 9.4
package.jsonexportsmap covers every documented subpath
10. Documentation
- 10.1 Sibling
.mdfor every implementation.ts(≥200 words, basic + advanced examples, full API reference) - 10.2
README.md## Modulessection 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-Paddingheader, 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—padPrefixesreturns 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_totalincrements - 11.9
breach/retry-after.test.ts— delta-seconds and HTTP-date forms; 60 s cap;@suaveplan/retryintegration - 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;setis a no-op - 11.13
client/client.test.ts— full composition;cache: nulldisables; defaultLruHibpCache - 11.14
client/options.schema.test.ts— every Zod rejection path - 11.15
schemas/breach.schema.test.ts— recorded/breach/Adobefixture 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+causecarriage - 11.20
observability/metrics.test.ts— every histogram and counter against an in-memoryMeterProvider - 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-iterationcheckPassword("hunter2")interception; zero leaks across all outgoing URLs and bodies - 11.23
privacy/breach-no-log.spec-conformance.test.ts—LoggerLikecapture; 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, noerrorfindings - 13.5
bunx turbo build --filter=@suaveplan/hibp— green
14. Pre-archive 7-row checklist (AGENTS.md Section 13)
- 14.1 Generics threaded;
expectTypeOfassertions onHibpCacheLike<V>andHibpClientOptions - 14.2 Zod schemas wired at every declared boundary (range result, breach response, paste response, options)
- 14.3 Every error code in
errors/codes.tsthrown 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 (
HibpClientis stateless beyond its cache; cachesetis 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.mdrow from Pending → Completed - 15.5 Update
CLAUDE.mdpackage listing under#### Security / supply chain