Show the ISO filename above each link with a separator rule so the
output is easier to read. Updated contribute message to reference
MSDL Web App with URL instead of bare domain.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseLinkExpiry has a 1h safety floor that causes it to treat already-
expired URLs as valid. Replace with extractRawExpiry + direct se
timestamp comparison so any URL expiring < 1h out is rejected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Redis L2 persistent cache: initRedis() from REDIS_URL env, seed
in-memory linkCache on startup, write-through on every fresh fetch
and background refresh (silent fallback to memory-only if unavailable)
- POST /contribute endpoint: validates product catalog, expiry > 1h,
Microsoft CDN host allow-list, per-IP token bucket (~5/min),
CONTRIBUTE_SECRET header; writes to in-memory + Redis; logs all
accepts and rejections
- CLI --no-contribute flag (also MSDL_NO_CONTRIBUTE=1 env var):
background goroutine posts raw MS JSON to /contribute after every
consumer fetch; best-effort 5s timeout; opt-out message to stderr
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>