- Backend: emit X-MSDL-Link-Status (fresh/cached/stale) and
X-MSDL-Link-Expires headers on every /proxy response; expose
both via Access-Control-Expose-Headers
- Frontend: read headers in handleGetLinks/handleRefreshLinks;
show amber stale warning when backend is WAF-blocked
- Frontend: new OfficialFallback component (collapsible) with
step-by-step guide to Microsoft's official download page,
pre-filled with the user's selected language; shown on stale
- Plan: add Redis/Valkey persistence section to Phase 3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Temporary diagnostic — tells us whether Set-Cookie headers from
Microsoft are actually reaching the backend through the CF Worker.
If count is 0, the Worker is stripping them or MS sets cookies via JS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sentinel started rejecting all /proxy requests because cookies set during
the fingerprinting flow (vlscppe/ov-df) were never reaching the
GetProductDownloadLinksBySku call.
Two gaps closed:
- CF Worker now forwards Cookie header to Microsoft and Set-Cookie headers
back to the backend
- Backend uses a simpleCookieJar (domain-scoping disabled) shared across
the full session: setup → warmup → download link fetch. Standard
cookiejar rejects microsoft.com cookies when the request URL is the
Worker domain.
Tested locally without Worker (direct MS calls): download links returned,
no Sentinel error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: ?force=true on /proxy bypasses cache for fresh link fetch
- Frontend: live expiry countdown from `se` param; falls back to static 24h for consumer links
- Frontend: Refresh button appears when link expires in <6h, fires force fetch
- Frontend: CliCommand replaces Aria2Tip — wget/curl/aria2 tabs, persists in localStorage, on all pages
- Frontend: RecentlyViewed on homepage — localStorage, tracks consumer + eval pages, shows expired state
- chore: add .claude/ to .gitignore; remove FileSize (not returned by Microsoft CDN)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: ?force=true on /proxy bypasses cache for fresh link fetch
- Frontend: parse `se` param for live expiry countdown on download links
- Frontend: show Refresh button when link expires in <6h, force-fetches fresh URL
- Frontend: CliCommand component replaces Aria2Tip — wget/curl/aria2 tabs, persists selection in localStorage
- Frontend: RecentlyViewed component — localStorage, shows active countdown or expired state
- Frontend: FileSize shown alongside arch if returned by Microsoft CDN
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Background refresh on stale serve now goes through sfGroup.Do so
concurrent stale hits collapse into one Microsoft call instead of
hammering the API from multiple goroutines simultaneously.
- cleanupCaches: only logs when entries are actually evicted (was
logging every 30min regardless of whether anything changed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /metrics: atomic counters for requests, cache hits, MS fetches, neg
hits, stale serves, and cache sizes per endpoint. Auth via
METRICS_SECRET env var (query param or Authorization: Bearer).
- Cache eviction: cleanupCaches() goroutine runs every 30min, removes
expired SKU/neg entries and link entries past ExpiresAt + 4h stale
window. Prevents unbounded map growth under sustained traffic.
- rand: replace global rand with package-level rand.Rand to avoid
mutex contention at high QPS (jitter() uses local source).
- Singleflight keys: separate sfKey from negKey in both handlers for
clarity — "sku:<id>" and "link:<id>:<sku>" vs neg cache keys.
- Background refresh on stale: when /proxy serves a stale entry after
a failed refresh, fires a goroutine to retry and update the cache
so the next request gets fresh data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduces Microsoft API traffic from thousands/day to ~50-100/day,
eliminating the 715-123130 rate-limit block under real traffic.
1. Singleflight (golang.org/x/sync/singleflight) — wraps all Microsoft
fetches so 500 concurrent cache misses collapse into 1 API hit.
2. SKU info cache — 7-day TTL keyed by product_id. Language lists are
stable; no need to hit Microsoft on every product page load.
3. Download link cache — dynamic TTL keyed by product_id:sku_id. Expiry
is derived from the signed URL's `se` query param minus 30min buffer,
so we never serve an expired signed URL.
4. Negative response caching — 60s TTL for 429 / 715-123130 failures.
Prevents thundering herd from retries worsening an existing block.
Only stored when no stale data is available to fall back on.
5. Dynamic TTL — parses `se` (signed expiry) from Microsoft CDN URL:
TTL = (se - now) - 30min. Falls back to 22h if unparseable.
6. Stale-on-failure — if a refresh fails (rate-limited or transient),
the expired cache entry is served temporarily. Applies to both
/proxy (download links) and /evallinks (eval ISOs).
7. Jitter — ±5min random offset on all TTLs prevents synchronised
mass-expiry spikes when many entries are warmed at the same time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Server & Enterprise ISOs to "What it does" bullets
- Document /evallinks API endpoint with example response
- Add evaluation editions section to Supported Products table
- Fix Contributing guide to reflect current architecture (products.json,
no PRODUCT_META/RELATED_GROUPS) and add steps for adding eval products
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProductsPage: align <title> with og:title ("All Windows ISO Releases")
- Disclaimer + PrivacyPolicy: add noindex (legal/thin pages shouldn't rank)
- ProductDetailPage: improve auto-generated meta description wording;
add SoftwareApplication JSON-LD to match EvalDetailPage
- HomePage: update JSON-LD description to include Windows Server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Server & Enterprise ISOs" row to the Why MSDL comparison table
and a new FAQ entry explaining eval editions, the 180-day trial, and
where to find them. Closes #eval-polish.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Listing pages (home, products, eval): max-w-4xl
Content pages (product detail, eval detail, about, privacy, disclaimer): max-w-2xl
Previously four different widths were in use across the app.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add unique seoDesc field to each eval product (≤155 chars)
- EvalDetailPage: use seoDesc for description, add Evaluation to
title, add SoftwareApplication JSON-LD alongside breadcrumb
- EvalPage: add ItemList JSON-LD for the listing page
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds /eval listing page and /product/<slug> detail pages for Windows
Server 2025/2022/2019/2016 and Windows 11 Enterprise evaluation ISOs,
sourced from Microsoft evalcenter without any session flow.
Backend: new /evallinks endpoint fetches evalcenter page, extracts
fwlinks, follows all redirects in parallel, detects arch + locale.
24h cache per product with startup warming so first hit is instant.
Frontend: EvalPage listing at /eval, EvalDetailPage at /product/<slug>
matching consumer structure with downloads, aria2 template, system
requirements, and also-available section. Smart ProductRouter in
App.tsx dispatches slug vs numeric productId. Dock highlights Products
tab for /product/* and /eval routes. CTA banner on home page and
footer link added. sitemap.xml updated with 6 new URLs.
Closes#13
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- README: add cloudflare-worker/ to project structure, update How It
Works diagram to show CF Worker hop, add CF Worker setup instructions
under Running Locally, and fix Deployment section (Worker is an
outbound proxy, not the backend host)
- About: mention CF Worker distributes requests across Cloudflare edge
- Privacy: add Cloudflare as a third-party service, update date
- Disclaimer: update date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced strconv.Atoi on sku_id with a permissive allowlist regex
(alphanumeric, hyphens, underscores). Older products like Windows 8.1
(#48, #52) return non-integer SKU IDs from Microsoft API, causing the
Get Download Links button to fail with 'sku_id must be a numeric value'.
Closes#9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split ternary into two separate navigate() calls to satisfy TypeScript
overload resolution — navigate(delta: number) and navigate(to: To)
are distinct overloads that cannot share a ternary return type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cloudflare-worker/worker.js: edge proxy forwarding requests to allowed
Microsoft hosts from Cloudflare distributed IPs. Set CF_WORKER_URL env var
to activate; omit to keep direct-to-Microsoft behaviour (self-hosters unaffected).
- Wrap all outbound Microsoft URLs in proxyURL() in main.go so the Worker
sits transparently in front of every session and download-link request.
- Fix Back button navigating to external referrer when user lands directly
on a product URL (#5) — use window.history.state?.idx to detect whether
there is prior in-app history before calling navigate(-1).
Closes#5, Closes#6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>