6.9 KiB
OpenSpec Change: site-icons
Package: @suaveplan/site-icons
Status: PROPOSED
Type: new-package
Why
Vaulted needs to display website icons next to vault login items. No maintained standalone library combines multi-source favicon discovery, size-aware selection, domain-normalized caching, and MIME validation in a single tree-shakeable package. Hand-rolling this inside the Vaulted app would duplicate concerns and prevent reuse across browser extensions and desktop.
What changes
- ADDED:
SiteIconFetcher— orchestrates multi-source discovery, caching, and fallback - ADDED:
fetchSiteIcons(domain)— fetches icons from all known sources for a domain - ADDED:
selectBestIcon(candidates, options?)— size-aware ranking and selection - ADDED:
IconSource— discriminated union of discovery sources (favicon,appleTouchIcon,manifestIcon,openGraph) - ADDED:
SiteIconResult—{ source: IconSource; url: string; size: IconSize | null; mimeType: string; data: Uint8Array } - ADDED:
IconSize—{ width: number; height: number } - ADDED:
SiteIconErrorextendsSuaveplanError - ADDED:
FetchTimeoutErrorextendsSiteIconError(codeSITE_ICON.TIMEOUT) - ADDED:
InvalidIconErrorextendsSiteIconError(codeSITE_ICON.INVALID) - ADDED:
NotFoundErrorextendsSiteIconError(codeSITE_ICON.NOT_FOUND) - ADDED: Zod schemas for all boundary types
- ADDED: OTel counters
site_icons.fetch.total(bysource,outcome) and histogramsite_icons.fetch.duration_ms
Out of scope
- Image resizing / downscaling manipulation — deferred to
@suaveplan/compress-image - Build-time app icon generation —
@suaveplan/iconzalready covers this - DOM-parsing HTML for arbitrary
<link rel="icon">discovery beyond the well-known sources listed above (may be added later) - ICO file multi-image extraction (treat ICO as opaque blob)
Gap analysis: sub-standard → enterprise
Typical favicon implementations fetch a single hard-coded /favicon.ico with no caching strategy, no size-aware selection, and no graceful fallback when sources are missing or return invalid data. This leads to broken UI states, excessive network requests, and unpredictable memory usage from unvalidated image blobs. Enterprise-grade code must treat icon discovery as a resilient pipeline with normalized domain keys, TTL-bound caching, MIME validation, and a ranked fallback chain.
Security model
- Threat model: Malicious domains serving oversized or malformed image payloads intended to exhaust memory or exploit image-parser vulnerabilities; MITM interception of icon traffic to fingerprint user vault contents.
- Privacy guarantees: No vault metadata or credential identifiers are transmitted during icon discovery. Domain names are normalized but not obfuscated; requests are made without authentication cookies.
- Input validation: All fetched blobs are inspected against an allow-list of MIME types and size bounds before being retained in memory or cache.
- Side-channel resistance: Cache lookups use constant-time key comparison where feasible; request timing is not correlated with vault item count.
- Key material handling: Not applicable — this package does not handle cryptographic keys.
Performance SLOs
- Latency: P95 icon fetch (cache miss) < 1.5s; P95 cache hit < 5ms.
- Throughput: Support 1,000 concurrent domain lookups without head-of-line blocking.
- Memory bounds: In-flight blob memory capped at 10 MB per
SiteIconFetcherinstance; cached entries bounded by LRU count and TTL. - Bundle size budget: Core fetcher + selector < 8 kB gzipped; optional cache adapter tree-shakeable.
Observability plan
- OTel spans/metrics: Counter
site_icons.fetch.totalbysourceandoutcome; histogramsite_icons.fetch.duration_ms; cache hit/miss ratio gauge. - Structured logging: JSON logs at
warnfor fetch timeouts,errorfor MIME validation failures,debugfor cache hits. - Error telemetry: Distinct error codes (
TIMEOUT,INVALID,NOT_FOUND) tagged with domain (hashed for privacy) and source. - Health checks: Expose
SiteIconFetcher.isHealthy()returning false if > 50% of sources are consistently failing.
Dependency audit
- Runtime deps:
@suaveplan/errors,@suaveplan/otel,@suaveplan/domain-normalizer(domain key normalization),@suaveplan/cache-browser(TTL-bound icon caching),@suaveplan/mime(image MIME validation), and platformfetch/crypto. - Supply-chain risk: All external image parsing is deferred to platform APIs; no third-party image-decoding libraries.
- Tree-shakeability:
SiteIconFetcher,selectBestIcon, cache adapters, and fallback chains are individually tree-shakeable. - License: All code and bundled assets are MIT-licensed; no copyleft dependencies.
Operational readiness
- Graceful degradation: If all sources fail, the fallback chain yields a transparent 1×1 pixel or the consumer-provided default; UI never blocks.
- Resource limits: Configurable per-request timeout, global concurrency limit, and max blob size.
- Error recovery: Exponential backoff for repeated source failures; circuit-breaker pattern if a source is persistently unhealthy.
- Backpressure: Concurrent fetches limited by semaphore; excess requests queued with a max queue depth and drop policy.
- Version compatibility: Public API surface frozen after v1; additive-only changes; deprecated APIs supported for two major versions.
Verification approach
- 100% coverage target: All exported functions, branches, and error paths instrumented in test.
- Property-based tests: Domain normalization, size ranking, and cache key generation tested with fast-check invariants.
- Load/stress criteria: Sustained 1,000 concurrent fetches for 60s without memory growth > 10%.
- Security audit checklist: MIME bypass attempts, oversized payload handling, cache poisoning via domain normalization edge cases.
src/fetcher/fetcher.test.ts— covers multi-source fetch, timeout, abort, MIME validationsrc/selector/selector.test.ts— covers size-aware ranking, null-size handling, exact matchsrc/cache/cache.test.ts— covers cache hit, miss, TTL expiry, domain-normalized keyssrc/fallback/fallback.test.ts— covers fallback chain ordering, exhaustionsrc/errors/errors.test.ts— covers error construction, code narrowing, context shapessrc/errors/errors.spec-conformance.test.ts— pins strict-mode behaviors andinstanceofchainstests/dep-graph.test.ts— pins dependency invariants (no React, no Node-only deps)
Migration impact
None — additive only.
Impact
Downstream consumers
No dependents (verified by bunx turbo ls --filter=...@suaveplan/site-icons).
Breaking changes
None — additive only.
Workspace dep prerequisites
@suaveplan/errors@suaveplan/otel@suaveplan/domain-normalizer@suaveplan/cache-browser@suaveplan/mime