When multiple download links are returned (e.g. x32+x64 for consumer
ISOs), an architecture picker is shown before printing the URL.
After the URL is printed, an interactive menu lets the user open the
link in the default browser (O), copy it to the clipboard (C), or exit
(Enter). Menu is suppressed when stdout is not a terminal (piping).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the dim "coming soon" footnote with a featured green card at the
top of the Install section. Manual binary downloads moved to a secondary
"Other platforms" section below. Ships with the post-winget-approval release.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add platform allowlist validation (windows, darwin, linux) and length caps for version (≤20) and product_id (≤10) in handleTelemetry to prevent unbounded Redis hash growth from attacker-controlled field values.
- Add HTTP method guard to handleCLIVersion to reject non-GET requests with 405 Method Not Allowed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- seedMetricsFromRedis: loads persisted counters into atomics on startup
- flushMetricsToRedis: writes all 12 counters to 3 Redis hashes on demand
- startMetricsFlusher: goroutine that flushes every 5 minutes
- Graceful shutdown: SIGINT/SIGTERM flushes metrics then drains HTTP in 10s
- http.ListenAndServe replaced with http.Server for Shutdown support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Microsoft Sentinel rejects a request (HTTP 502 with 'Sentinel'
in body), the session was being reused on every retry — hammering the
same poisoned session and making the block worse.
Now: detect Sentinel response, evict the session from cache, and
return a rateLimitError so stale-on-failure kicks in and users get
the last cached link instead of an error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consumer ISO CDN URLs use P1 (Unix timestamp) instead of se (RFC3339).
extractRawExpiry now tries se first then falls back to P1, so CLI
contributions of consumer ISOs are no longer rejected.
Also refactors parseLinkExpiry to call extractRawExpiry (DRY) and
fixes stale log message referencing 'se param'.
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>