PROGRESS.md and CLAUDE.md now document the full investigation: three weekly checkpoints confirming the backend's direct link-fetch is 100% dead, the crowdsourced CLI-contribution cache carrying the real load (286 accepted, 0 rejected), the two merged backend fixes (lockdown TTL, /proxy product_id validation), and the CLI TLS fingerprint hardening in this branch (functionally verified, not yet validated against the Sentinel-rejection-rate trend). Removed two plan+spec pairs under docs/superpowers/ (the original CLI build plan and the telemetry/update-check plan+design) -- both fully shipped, redundant with PROGRESS.md's own record of what happened. Committed docs/superpowers/specs/2026-07-13-needs-warming-design.md for the first time -- it's been sitting untracked in the working tree for weeks. Unlike the two removed docs, this one is a live, unbuilt proposal (a public page surfacing which products are currently Sentinel-locked with no cached fallback), not completed work, so it's kept and now tracked in the backlog instead of discarded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.7 KiB
MSDL — Claude Context
Project
Open-source Windows ISO downloader. React + Go. Live at msdl.tech-latest.com.
Frontend on Cloudflare Pages, backend on Hetzner via Coolify.
Outbound Microsoft API calls route through a Cloudflare Worker (cloudflare-worker/worker.js)
controlled by CF_WORKER_URL + CF_WORKER_SECRET env vars on the backend.
Conventions
- Small config/one-liner fixes → commit directly to main
- Any meaningful feature or multi-file change → feature branch → PR → merge
- GitHub issues are for bugs and community contributions only — planned features tracked here and in PROGRESS.md
- Commit messages follow conventional commits:
feat:,fix:,docs:,chore: - Always test locally before committing or pushing — research feasibility before implementing (e.g. file size was planned but Microsoft CDN doesn't return it; caught after commit)
Backlog (implement when ready, in priority order)
#12 — Two-layer in-memory caching ✅ DONE (feat/caching-layer, merged)
All 7 items shipped: singleflight, SKU cache (7d TTL), link cache (dynamic TTL from se param),
negative cache (60s), dynamic TTL, stale-on-failure, jitter. Verified in production.
Beyond #12 — Observability & resilience
- Cache hit/miss logging — logged on every request (fetched vs cached, cached until timestamp).
/metricsendpoint (auth-protected) — exposes cache hit rate, miss count, stale serves. Auth viaMETRICS_SECRETenv var. (feat/metrics, merged)- README architecture section — caching layer,
/metrics, env vars documented. - Per-IP / per-product rate limiter — deferred; revisit after 1 month of production traffic data.
Frontend UX improvements ✅ DONE (feat/ux-improvements, merged — v1.2.0)
- Expiry countdown — parses
separam from signed URL where available; falls back to "24 hours" for consumer links (Microsoft doesn't includesein consumer CDN URLs). - Refresh links —
?force=trueon/proxybypasses cache, fetches fresh from Microsoft. Button shown when under 6h remaining. - CLI command tabs — wget / curl / aria2 tabs, persists selection in localStorage. Replaces old aria2-only tip. Applied to both consumer and eval pages.
- Recently viewed — localStorage only. Shows on homepage. Consumer + eval pages both tracked. Expired state shown when link expiry known.
File size— not feasible. Microsoft CDN does not return file size in the API response.
Sentinel WAF resilience (ongoing)
Confirmed via three weekly /metrics + docker-log checkpoints (2026-07-14 → 07-21 → 07-31):
the backend's own direct link-fetch (/proxy → Microsoft) is 100% blocked, permanently —
link.ms_fetches: 0 every time, and zero even-attempted-and-failed fetches in the raw logs
(the lockdown gate short-circuits before reaching that code path). The site stays alive
entirely on cached/stale entries plus CLI-contributed links (286 accepted, 0 rejected,
confirmed 2026-07-31). /skuinfo and /evallinks are unaffected — only the download-link
endpoint is targeted.
The CLI's own residential-IP requests also see a stable ~20% Sentinel rejection rate across
all three checkpoints — not improving, not worsening. Microsoft's own API error response
literally names the system ("Sentinel marked this request as rejected.", Type: 9 in
Errors[]), confirming it's a real, named product, not our guess. The clean structured-JSON
deny (not an HTML/JS challenge page) suggests a signature/reputation gate rather than full
interactive bot-management — TLS ClientHello fingerprinting is a plausible contributing
signal, since Go's stdlib crypto/tls doesn't look like any real browser, independent of IP.
- Back off Sentinel retries — bumped
lockdownTTL90min → 5h (backend/main.go). 181 retries over 17 days, 0 successes; retrying that often was pure noise. (fix/sentinel-lockdown-and-proxy-validation, merged) - Validate
product_idin/proxy— was passing unknown IDs straight through to a real Microsoft session attempt (found via a strayproduct_id=2861, never a real product, in the logs). Now rejected with 404 before any outbound call. (same PR, merged) - CLI TLS/HTTP2 fingerprint hardening — swapped the CLI's transport from stdlib
net/httptogithub.com/bogdanfinn/tls-client(wrapsutlswith a maintained Chrome profile). Verified functionally correct (real link fetched + contributed end-to-end), but does NOT yet prove the fingerprint theory — the old client already succeeded ~80% of the time. Need to watch the Sentinel-rejection-rate telemetry over a comparable multi-day window post-merge. (feat/cli-tls-fingerprint-hardening, not yet merged) /needs-warmingcommunity page — proposed, not built. Surfaces products currently failing web users (active Sentinel/rate-limit lockdown, no cached/stale link available) with a one-click CLI command to fix it. Seedocs/superpowers/specs/2026-07-13-needs-warming-design.md.
Known bugs / open issues
_redirectsCloudflare Pages — investigated 2026-07-21. The rule was indeed ignored (Cloudflare's deploy log flags it as a false-positive infinite loop), but this turned out to be a non-issue: Cloudflare Pages' own default fallback already servesindex.htmlfor any unmatched path, so the explicit rule was redundant. Verified live: direct navigation to/about, a dynamic route (/product/3262), and a genuinely invalid path all return HTTP 200 with the correct content (including React Router's own 404 page for the invalid one). Removedfrontend/public/_redirectsentirely to stop the recurring deploy warning.