Three checkpoints over 17 days confirmed a stable ~20% Sentinel
rejection rate on the CLI's own requests, despite running from
residential IPs specifically to avoid ASN-based blocking. That
persistence points at TLS ClientHello fingerprinting as an additional
signal: Go's stdlib crypto/tls produces a handshake that looks nothing
like a real browser, independent of IP or headers.
Swaps the CLI's HTTP transport (session setup, SKU lookup, download
link fetch, eval link resolution) from net/http to
github.com/bogdanfinn/tls-client, which wraps utls with a maintained
Chrome browser profile (TLS + HTTP2 fingerprint together, not just
TLS -- a browser-consistent one without the other is its own tell).
Bumped msUA's claimed Chrome version to 133 to match the chosen
profile, since a mismatched UA vs. TLS fingerprint is itself
detectable.
The custom simpleCookieJar is gone -- tls-client provides its own
cookie jar. fetchEvalLinks got the same treatment for consistency,
even though the Eval Center path isn't currently blocked.
Verified: go build/vet/test all pass, and a live end-to-end run
(msdl --id 3262 --lang English) returned a real signed download link
and successfully contributed it back to the shared cache. That proves
the flow still works functionally through the new client -- it does
NOT prove the fingerprint theory, since the old client already
succeeded ~80% of the time. The real test is watching the Sentinel-
rejection-rate telemetry over a comparable multi-day window after
this ships.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves msdls_v3.py from repo root into scripts/check-new-releases/,
alongside a new Go tool, documented as Method 1 (Python brute-force
range scan, can write results to a catalog-shaped JSON) and Method 2
(Go auto-discovery via page-scrape + bounded adjacent-ID probe, no
range-guessing needed). Only Windows 11 is checked -- 8.1 is fully
frozen and Windows 10 is past end-of-life, so neither will produce a
new consumer ISO again.
Fixed msdls_v3.py's release-name extraction: it checked EditionName/
ReleaseName/FriendlyName, none of which exist in the real API
response -- the actual field is ProductDisplayName, confirmed live.
Also added the vlscppe session-permit call to setup_session(), which
was skipped entirely; higher-risk gap for this script's brute-force
range-scan usage pattern than for a single lookup.
Both tools' live runs surfaced a real catalog gap: product IDs
3322/3323/3325/3326 (Home/Pro China variants of the 25H2 "V2" refresh)
exist on Microsoft's side but weren't in our catalog. Added them to
cli/catalog.go, products.json, validContributeProducts, and
sitemap.xml, and renamed 3321/3324 from our own guessed "(Updated
Oct)" label to Microsoft's actual name for this refresh, "(V2)"
(their internal name is "Windows 11 25H2__V2"; cleaned up the
double-underscore for user-facing display).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bare `msdl` invocation showed all 22 products (17 consumer + 5
eval) as a flat numbered list. Replaces it with a curated landing
screen: 5 popular products (picked from real usage telemetry) plus a
2-item eval shortlist, with "list" as an escape hatch to today's full
picker and free text routed through the existing search flow.
Also fixes a search bug: querying "windows 10" incorrectly matched
Windows 11 24H2 because "10" is a substring of its build number
(26100.1742). Query words now must start at a word boundary rather
than matching anywhere, so digit fragments inside unrelated numbers
can no longer collide -- while "arm" still fuzzy-matches "ARM64".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consumer Windows products list their English option as "English" (or
"English International"), not "English (United States)" -- the exact
example this used, which is why it was such a common source of --lang
mismatch errors. Applies to --help, the --lang flag description, and
the release workflow's usage example.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Real-world telemetry showed --lang "English (United States)" failing
for several consumer products that only list "English International"
-- the exact example used in --help. The mismatch error now lists the
languages actually available, and the help text notes names vary by
product.
Also fixes truncateError to keep both head and tail of long error
strings (previously kept only the head, which for wrapped errors with
a long URL cut off the actual root cause at the end).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Failure telemetry only recorded success=false with no context. Adds
a truncated error string (150 chars, CLI-side) forwarded to a new
msdl:telemetry:errors Redis hash so failure causes are diagnosable
from /metrics instead of guessed at.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Telemetry was fired as a goroutine then the process exited immediately,
killing it before the HTTP request could complete. Now called
synchronously — sendTelemetry already has a 2s internal timeout so
worst-case latency is unchanged for the user.
Also moves the combined picker into run() so eval products selected
interactively are tagged action=eval with the correct eval_slug instead
of action=interactive with no slug.
Adds What's New changelog section to release notes, auto-generated from
commits since the previous tag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Interactive `msdl` (no flags) now lists consumer + eval products in one
picker with an Evaluation / Enterprise separator; selecting a server
product routes to the eval flow automatically
- copyToClipboard calls clip.exe by full SystemRoot path, bypassing
cmd.exe PATH lookup that failed in some terminal environments
- CliHandoff card: winget primary install + msdl (no .exe) in run command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cmd.exe treats & as a command separator, truncating Microsoft's signed
download URLs at the first query parameter boundary. Writing the URL to
a Windows Internet Shortcut (.url) file means cmd.exe only ever sees a
temp filename with no special characters; the browser reads the full URL
from the file contents.
Uses %COMSPEC% to locate cmd.exe reliably across environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>