Compare commits

...

17 commits

Author SHA1 Message Date
Shekhar Vaidya
fba4502055 docs: log Aug 17 Sentinel check-in, file per-language checksums idea
TLS fingerprint fix (v0.3.7) checked in 17 days post-release: aggregate
CLI Sentinel-rejection rate still ~21%, unchanged -- inconclusive given
mixed-version population, revisit once 0.3.7+ dominates usage.

New backlog item: per-language SHA256 checksums on product pages.
Confirmed live that Microsoft's own download pages publish a static
per-locale hash table for the current build, not reachable through any
API call MSDL/CLI already makes. Since it only changes when a product
ID is replaced, no scraper needed -- just copy it by hand at the same
time a new product gets added to the catalog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 23:52:21 +05:30
Shekhar Vaidya
d562bf8d0e fix(frontend): Windows 10 lifecycle text/badge were stale (dated Oct 2025, now Aug 2026)
Homepage's featured card said "Security support until October 2025" --
that date has already passed, so it read as a future deadline that
wasn't. Updated to reflect actual status (mainstream support ended,
consumer ESU runs through Oct 2026) -- kept tight to match the other
three cards' description length so the grid stays even (the first
draft ran long enough to wrap to 3 lines and grow that one card's
height past its siblings).

Also found products.json still tagged both Windows 10 22H2 variants
(2618, 2378) as "EOL SOON" -- inconsistent with the homepage's own
"EOL"/"END OF LIFE" badge for the same product, and equally stale
for the same reason. products.json's badge field renders verbatim
on the product detail page, so this was live and user-facing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 23:28:21 +05:30
Shekhar Vaidya
f6659fcc42 docs(ci): add AUR install method to release notes template
Now that msdl-bin is actually published on AUR, future releases'
auto-generated "What's New" -> Install section should mention it
too, matching what's already in README.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 13:14:34 +05:30
Shekhar Vaidya
34e2b35486 docs: msdl-bin is published on AUR now, not just prepared
README still said the AUR package was "prepared but not yet
published" -- it went live today (msdl-bin 0.3.7-1, now that AUR
registration reopened). Added it as a proper install method alongside
winget/Homebrew, matching their existing formatting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 13:12:38 +05:30
Shekhar Vaidya
f27e2931b4 chore(aur): bump msdl-bin to v0.3.7, note AUR registration reopened
.SRCINFO regenerated with real makepkg this time (Docker was running),
confirming the same values as the manual field-mapping used previously.

AUR disabled new account registrations 2026-06-15 after a malware
campaign; reopened as of today. Updated the README note accordingly --
this package is ready to actually publish once an account + SSH key
are set up, which is a manual step only the maintainer can do.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 12:49:31 +05:30
Shekhar Vaidya
33f3a46777 chore(release): bump to v0.3.7 -- backend version string + winget manifests; fix(ci): winget submission needs a Windows runner
Version bump: backend/main.go latestCLIVersion and the local
winget/manifests/*.yaml mirror to 0.3.7, with the real
InstallerUrl/InstallerSha256 for the cli/v0.3.7 release asset.

CI fix: the first real winget-submission attempt (v0.3.7, now that
WINGET_TOKEN is finally set) failed immediately -- wingetcreate isn't
published as a NuGet/dotnet-tool package at all, it's a native Windows
binary requiring .NET 6 + VC++ Redistributable. `dotnet tool install
--global wingetcreate` on ubuntu-latest could never have worked; this
path was just never exercised before since the empty-token check
always short-circuited it first. Split winget submission into its own
job on windows-latest, downloading wingetcreate.exe directly from its
GitHub release instead of dotnet tool install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 12:29:08 +05:30
Shekhar
cf318eb15c
Merge pull request #40 from starkSV/feat/cli-tls-fingerprint-hardening
feat(cli): TLS/HTTP2 fingerprint hardening via tls-client
2026-07-31 12:18:56 +05:30
Shekhar Vaidya
78be837a4a docs: explain the Sentinel WAF saga, retire completed plans, commit the needs-warming spec
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>
2026-07-31 12:08:38 +05:30
Shekhar Vaidya
9e22cd8bf8 feat(cli): TLS/HTTP2 fingerprint hardening via tls-client
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>
2026-07-31 12:02:44 +05:30
Shekhar
6ace2ceb19
Merge pull request #39 from starkSV/fix/sentinel-lockdown-and-proxy-validation
fix(backend): back off Sentinel lockdown retries, validate product_id in /proxy
2026-07-31 11:49:57 +05:30
Shekhar Vaidya
6ab849be5e fix(backend): back off Sentinel lockdown retries, validate product_id in /proxy
Log analysis over 17 days showed 181 Sentinel-block retry attempts on
/proxy, all 181 failed (0% success). Retrying every 90 minutes yields
zero value right now and just adds more blocked-request noise against
our own IP for nothing in return. Bumped lockdownTTL to 5h so it backs
off harder while still recovering same-day if Microsoft's block ever
eases.

Also found /proxy never validated product_id against the known
catalog, so a request for a nonexistent ID (e.g. product_id=2861,
never a real product) still burned a full outbound Microsoft session
attempt and a Sentinel-block hit for something that could never
succeed anyway. Now rejected with 404 before any Microsoft call,
reusing the existing validContributeProducts allow-list.

Verified locally: unknown product_id -> 404 (no MS fetch attempted in
logs), missing params -> 400 unchanged, known product_id -> proceeds
to a real MS session attempt as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 11:35:44 +05:30
Shekhar Vaidya
5a806eea87 fix(frontend): remove redundant, ignored _redirects rule
Cloudflare's deploy log has been flagging the SPA fallback rule
(/* /index.html 200) as a false-positive infinite loop and ignoring
it on every deploy. Investigated live: direct navigation to /about, a
dynamic route, and a genuinely invalid path all return 200 with the
correct content already, since Cloudflare Pages' own default fallback
serves index.html for any unmatched path. The rule was dead weight --
removing it to stop the recurring warning noise. Closes the tracked
issue in CLAUDE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 10:19:50 +05:30
Shekhar Vaidya
73eee730c9 fix(frontend): CLI curl-command copy bug, stale/missing content, and Fido attribution cleanup
- CliHandoff: the visible curl install command was a hardcoded truncated
  placeholder ("...install.sh"), so the copy button worked but manually
  selecting/copying the text gave a broken command. Now shows the real URL
  and lets CSS `truncate` handle the visual clipping.
- Dock: adds a full-width backdrop-blur shelf under the floating desktop
  dock so it doesn't visually blend into scrolled content (page bg is
  near-black, so plain color gradients were invisible -- blur is what
  actually reads). Desktop only; the mobile dock is already a flush bar.
- HomePage: hero now mentions "Open source" (linked, dotted underline) --
  previously absent anywhere on the landing page.
- StatsBar: "releases available" count now includes eval/enterprise
  editions, not just the consumer catalog (17 -> combined total).
- About/Privacy/Disclaimer: these hadn't been updated since the CLI
  shipped. Privacy Policy now discloses the CLI's anonymous telemetry and
  crowdsourced link-contribution behavior (previously undisclosed). About
  page's stale hardcoded product list replaced with a link to the catalog,
  plus a new section explaining the CLI. Disclaimer now covers the CLI
  binary under the same terms.
- Fido/Rufus attribution was scattered with duplicate linked mentions on
  the same pages; consolidated to one canonical linked credit per surface
  (About's Credits section, README's intro) instead of repeating it.
- FAQAccordion: fixed a factual error ("ported to Go and Node.js" -- no
  Node.js backend exists), and linked the actual repo in the open-source
  FAQ answer (previously just asserted it with no link).
- sitemap.xml: removed a dead entry for product 48, which was already
  removed from the catalog (Microsoft returns 502 for it).
- README: fixed three inaccuracies in the /contribute API doc (wrong auth
  header, wrong body field name, wrong status codes) against the actual
  backend/main.go implementation, and added the missing telemetry/sentinel
  fields to the /metrics example response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 10:10:38 +05:30
Shekhar Vaidya
b516804541 chore: add GitHub Sponsors funding link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-15 13:13:48 +05:30
Shekhar Vaidya
47698d8129 chore(aur): bump msdl-bin to v0.3.6
Local mirror only -- AUR account registration is still disabled
(since 2026-06-15), so this isn't pushed to aur.archlinux.org yet.
Kept in sync so it's ready to publish the moment registration
reopens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 11:49:07 +05:30
Shekhar
61fdcffaaa
Merge pull request #38 from starkSV/chore/cli-v0.3.6-bump
chore(release): bump to v0.3.6 -- backend version string + winget manifests
2026-07-14 11:35:08 +05:30
Shekhar Vaidya
a831cddb8e chore(release): bump to v0.3.6 -- backend version string + winget manifests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 11:33:48 +05:30
30 changed files with 558 additions and 2840 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: starkSV

View file

@ -74,7 +74,14 @@ jobs:
```
(formula is `msdl-cli`, not `msdl` -- homebrew/core has an unrelated package named `msdl`; the installed command is still just `msdl`)
**macOS/Linux (no Homebrew):**
**Arch Linux (AUR):**
```bash
yay -S msdl-bin
# or: paru -S msdl-bin
```
[aur.archlinux.org/packages/msdl-bin](https://aur.archlinux.org/packages/msdl-bin)
**macOS/Linux (no Homebrew/AUR):**
```bash
curl -fsSL https://api.msdl.tech-latest.com/install.sh | bash
```
@ -108,8 +115,15 @@ jobs:
msdl --list # list all products
```
submit-winget:
# wingetcreate is a native Windows tool (needs .NET 6 + VC++ Redistributable) --
# it is NOT published as a NuGet/dotnet-tool package, so it can only run on a
# windows-latest runner, not ubuntu-latest like the release job above.
needs: release
runs-on: windows-latest
steps:
- name: Submit winget update
if: success()
shell: bash
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
@ -117,10 +131,10 @@ jobs:
echo "WINGET_TOKEN not set, skipping winget update"
exit 0
fi
dotnet tool install --global wingetcreate
curl -L -o wingetcreate.exe https://github.com/microsoft/winget-create/releases/latest/download/wingetcreate.exe
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#cli/v}"
wingetcreate update starkSV.msdl \
./wingetcreate.exe update starkSV.msdl \
--version "$VERSION" \
--urls "https://github.com/starkSV/windows-iso-downloader/releases/download/${{ github.ref_name }}/msdl-windows-amd64.exe" \
--submit \

View file

@ -37,8 +37,64 @@ negative cache (60s), dynamic TTL, stale-on-failure, jitter. Verified in product
- [x] **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.
- [x] **Back off Sentinel retries** — bumped `lockdownTTL` 90min → 5h (`backend/main.go`).
181 retries over 17 days, 0 successes; retrying that often was pure noise.
(fix/sentinel-lockdown-and-proxy-validation, merged)
- [x] **Validate `product_id` in `/proxy`** — was passing unknown IDs straight through to a
real Microsoft session attempt (found via a stray `product_id=2861`, never a real
product, in the logs). Now rejected with 404 before any outbound call. (same PR, merged)
- [x] **CLI TLS/HTTP2 fingerprint hardening** — swapped the CLI's transport from stdlib
`net/http` to `github.com/bogdanfinn/tls-client` (wraps `utls` with a maintained Chrome
profile). Shipped in `cli/v0.3.7` (merged, released 2026-07-31). **Checked in on
2026-08-17, 17 days post-release: inconclusive-to-negative.** Aggregate CLI
Sentinel-rejection rate is still ~21%, statistically unchanged from every pre-fix
checkpoint. Real caveat: mixed-version population (`0.3.6`: 1156 actions vs `0.3.7`: 493
in that window) means a `0.3.7`-only improvement could still be masked — `/metrics`
doesn't break the error down by CLI version, so this can't be fully isolated yet. Revisit
once `0.3.7`+ dominates usage share; if the aggregate still hasn't moved by then, treat
the fingerprint theory as disproven.
- [ ] **`/needs-warming` community 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. See
`docs/superpowers/specs/2026-07-13-needs-warming-design.md`.
- [ ] **Per-language SHA256 checksums on product pages** — Microsoft's own download pages
(e.g. `/software-download/windows11`) publish a static hash table (one SHA256 per
locale) for the *current* ISO build, confirmed live via direct network inspection
2026-08-18. It's not in any API response MSDL/CLI call (`GetProductDownloadLinksBySku`
never includes a hash, confirmed against Microsoft's own live page too) — it's static
HTML on the Windows-version download page, tied to the product/build, not the session.
Since it only changes when a product ID is replaced (same trigger as adding a new
catalog entry), no scraper/refresh job needed: copy the hash table by hand at the same
time a new product ID is added, per the existing "Adding a new consumer Windows release"
steps in CONTRIBUTING.md. Open questions before building: where to store it
(`products.json` field vs. separate file), which languages to cover (all 38 vs. just
the popular ones), and where to surface it (collapsible "Verify your download" section
on the product page, mirroring Microsoft's own UX; maybe CLI output too).
### Known bugs / open issues
- [ ] **`_redirects` Cloudflare Pages** — SPA fallback rule (`/* /index.html 200`) is flagged
as an infinite loop and ignored by Cloudflare Pages, potentially causing 404s on
direct navigation to non-home routes. Needs investigation and fix.
- [x] **`_redirects` Cloudflare 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 serves `index.html` for
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). Removed
`frontend/public/_redirects` entirely to stop the recurring deploy warning.

View file

@ -2,6 +2,51 @@
## Shipped
### Sentinel WAF investigation & resilience hardening — backend merged, CLI fix pending
Three weekly `/metrics` + raw docker-log checkpoints (2026-07-14 → 07-21 → 07-31) turned the
"Microsoft blocks our server IP" assumption from a one-off observation into a confirmed,
stable fact — and revealed the crowdsourcing architecture below is now doing all the real work.
**What the data actually showed:**
- `link.ms_fetches: 0` at every checkpoint — the backend's own direct link-fetch to Microsoft
has not succeeded once, ever, across 17+ days.
- Raw docker logs showed not just zero successes but zero *attempted-and-failed* fetches
either — the existing Sentinel lockdown gate short-circuits the request before it even
reaches Microsoft, serving cache/stale silently instead. 181 blocked session attempts over
the same window, all 181 unsuccessful.
- The site stayed online anyway: 286 CLI contributions accepted, 0 rejected, in that same
window — confirming the crowdsourced cache from the CLI (see architecture entry below) is
the thing actually keeping links fresh now, not the backend's own fetch.
- `/skuinfo` (125 successful Microsoft fetches) and `/evallinks` (126 cache hits, 0 failures)
are both unaffected — Sentinel targets the download-link endpoint specifically.
- The CLI's own residential-IP requests held a stable ~20% Sentinel-rejection rate across all
three checkpoints too — not improving, not worsening, despite running from IPs that
shouldn't trip ASN-based blocking. Traced the exact error text
(`"Sentinel marked this request as rejected."`) to Microsoft's own API response
(`Errors[0].Value` with `Type: 9`) — confirming "Sentinel" is Microsoft's real name for this
system, not our guess, and that it returns a clean structured JSON deny rather than an
HTML/JS challenge page (suggesting a signature/reputation gate rather than full interactive
bot management).
**Backend fixes (`fix/sentinel-lockdown-and-proxy-validation`, merged):**
- `lockdownTTL` bumped 90min → 5h. 181 retries, 0 successes — retrying that often was pure
noise against Microsoft (and our own IP's reputation) for zero return.
- `/proxy` now validates `product_id` against the existing catalog allow-list before
attempting a Microsoft session. Found via a stray `product_id=2861` (never a real product)
in the logs — the backend was burning a full outbound attempt and a Sentinel-block hit on
IDs that could never succeed anyway.
**CLI fix (`feat/cli-tls-fingerprint-hardening`, not yet merged):** swapped the CLI's HTTP
transport from stdlib `net/http` to `github.com/bogdanfinn/tls-client` (wraps `utls` with a
maintained Chrome TLS+HTTP2 fingerprint), on the theory that Go's default TLS handshake is
itself a detectable non-browser signal, independent of IP. Verified functionally correct
end-to-end (real link fetched and contributed), but that does *not* prove the theory — the
old client already succeeded ~80% of the time. Real validation is watching the
Sentinel-rejection-rate telemetry over a comparable multi-day window post-merge.
---
### CLI + resilience architecture (formerly `IMPLEMENTATION_PLAN.md`, Phases 15) — merged
Microsoft's Azure Sentinel WAF started blocking the backend's data-center IP (Hetzner) on
@ -120,4 +165,3 @@ CF Worker ensures Hetzner IP is never exposed to Microsoft — rate-limit block
| Item | Notes |
|---|---|
| Per-IP / per-product rate limiter | Revisit after 1 month of production traffic data |
| `_redirects` Cloudflare Pages bug | SPA fallback rule flagged as infinite loop, may cause 404s on direct nav |

View file

@ -70,7 +70,14 @@ brew install msdl-cli
```
(the formula is named `msdl-cli`, not `msdl``homebrew/core` already has an unrelated package called `msdl`; the installed command is still just `msdl`)
**macOS / Linux (no Homebrew):**
**Arch Linux (AUR):**
```bash
yay -S msdl-bin
# or: paru -S msdl-bin
```
Package: [aur.archlinux.org/packages/msdl-bin](https://aur.archlinux.org/packages/msdl-bin). Source tracked here at [`aur/msdl-bin`](./aur/msdl-bin).
**macOS / Linux (no Homebrew/AUR):**
```bash
curl -fsSL https://api.msdl.tech-latest.com/install.sh | bash
```
@ -86,8 +93,6 @@ Auto-detects OS/arch (including Termux on Android) and installs the latest relea
| Linux (x86_64) | `msdl-linux-amd64` | `msdl` |
| Linux (ARM64, incl. Termux on Android) | `msdl-linux-arm64` | `msdl` |
An AUR package (`msdl-bin`) is prepared but not yet published — Arch Linux disabled new AUR registrations after a [malware incident](https://itsfoss.com/news/arch-linux-aur-malware-flood/); see [`aur/msdl-bin`](./aur/msdl-bin) for status.
### Crowdsourced cache
By default, each successful fetch is contributed back to the web app's cache — so the next visitor gets a cached link instead of hitting Microsoft cold. Contribution is a background POST to `/contribute`. No personal data is sent — only the product ID, SKU ID, and the raw Microsoft JSON response. To opt out:
@ -134,8 +139,6 @@ Browser → Backend → (CF Worker) → Microsoft API → Signed CDN URL
5. Cache result → return to browser
```
The flow mirrors [Fido.ps1](https://github.com/pbatard/Fido) by Pete Batard — the same script bundled with Rufus.
Outbound requests to Microsoft are optionally routed through a Cloudflare Worker (`cloudflare-worker/worker.js`). This distributes requests across Cloudflare's global edge IPs instead of a single server IP, preventing Microsoft's rate-limit block (error 715-123130) under high traffic. The Worker is opt-in via environment variables — omit them to go direct to Microsoft.
### Caching layer
@ -249,17 +252,17 @@ Valid slugs: `server-2025`, `server-2022`, `server-2019`, `server-2016`, `win11-
### `POST /contribute`
Accepts a crowdsourced link from the CLI tool to warm the cache. Requires `Authorization: Bearer <CONTRIBUTE_SECRET>` header.
Accepts a crowdsourced link from the CLI tool to warm the cache. Requires an `X-Contribute-Secret: <CONTRIBUTE_SECRET>` header.
```json
{
"product_id": "3262",
"sku_id": "0x0409",
"response": { /* raw Microsoft JSON */ }
"raw_json": { /* raw Microsoft JSON */ }
}
```
Returns `200 OK` on acceptance, `400` on validation failure (expired link, unknown product), `429` on rate limit (~5 req/min per IP).
Returns `204 No Content` on acceptance; `422` on validation failure (unknown product, invalid SKU, expiry under 1 hour, disallowed download host); `400` on a malformed request body; `401` if the secret header doesn't match; `429` on rate limit (~5 req/min per IP).
### `GET /metrics?secret=<secret>`
@ -271,10 +274,22 @@ Returns real-time cache statistics for the running instance. Auth via `?secret=`
"link": { "requests": 38, "cache_hits": 35, "ms_fetches": 3, "neg_hits": 0, "stale": 0, "hit_rate": "92.1%", "cache_size": 6 },
"eval": { "requests": 12, "cache_hits": 12, "stale": 0, "hit_rate": "100.0%", "cache_size": 5 },
"neg_cache_size": 0,
"total_ms_fetches": 6
"total_ms_fetches": 6,
"sentinel_errors": 0,
"sentinel_distinct_sources_est": 0,
"telemetry": {
"actions": { "fetch": 364, "eval": 40, "interactive": 14 },
"platforms": { "windows": 176, "darwin": 70, "linux": 132 },
"products": { "3262": 109, "2618": 95 },
"results": { "success": 197, "failed": 221 },
"versions": { "0.3.6": 41 },
"errors": { "fetching download links: Sentinel marked this request as rejected.": 90 }
}
}
```
`sentinel_errors` / `sentinel_distinct_sources_est` track how often Microsoft's Sentinel WAF has blocked the backend's own outbound Microsoft calls. `telemetry` aggregates anonymous CLI usage events (see [Usage telemetry](#usage-telemetry) below) — omitted entirely if no CLI events have been recorded yet.
---
## Supported Products
@ -292,7 +307,6 @@ Returns real-time cache statistics for the running instance. Auth via `?secret=`
| Windows 10 22H2 | 2618 | x64 / x86 |
| Windows 10 22H2 Home China | 2378 | x64 |
| Windows 8.1 | 52 | x64 / x86 |
| Windows 8.1 Single Language | 48 | x64 / x86 |
### Evaluation editions (Server & Enterprise)

View file

@ -1,6 +1,6 @@
pkgbase = msdl-bin
pkgdesc = Download Windows ISO files directly from Microsoft's servers
pkgver = 0.3.5
pkgver = 0.3.7
pkgrel = 1
url = https://msdl.tech-latest.com
arch = x86_64
@ -8,9 +8,9 @@ pkgbase = msdl-bin
license = MIT
provides = msdl
conflicts = msdl
source_x86_64 = msdl-bin-0.3.5-x86_64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.5/msdl-linux-amd64
sha256sums_x86_64 = dfb0648acd6caa79d597839bb9803c979446a2d77f90447273ee5003d8857c9d
source_aarch64 = msdl-bin-0.3.5-aarch64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.5/msdl-linux-arm64
sha256sums_aarch64 = 933beb694b3e59fc84cdccfb5cb3dfe48db3274046df88390cf8ee32070f4088
source_x86_64 = msdl-bin-0.3.7-x86_64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.7/msdl-linux-amd64
sha256sums_x86_64 = 4714f47044814733ca2e20ae8f64327ad0b71b912d6fe6f677f18c90b3a5f1c0
source_aarch64 = msdl-bin-0.3.7-aarch64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.7/msdl-linux-arm64
sha256sums_aarch64 = 9059678043de3995f67ddc3083badf674b572d9bba8e8118e511f57d1f80d3eb
pkgname = msdl-bin

View file

@ -1,6 +1,6 @@
# Maintainer: starkSV <shekharvaidya2@gmail.com>
pkgname=msdl-bin
pkgver=0.3.5
pkgver=0.3.7
pkgrel=1
pkgdesc="Download Windows ISO files directly from Microsoft's servers"
arch=('x86_64' 'aarch64')
@ -10,10 +10,10 @@ provides=('msdl')
conflicts=('msdl')
source_x86_64=("$pkgname-$pkgver-x86_64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv${pkgver}/msdl-linux-amd64")
sha256sums_x86_64=('dfb0648acd6caa79d597839bb9803c979446a2d77f90447273ee5003d8857c9d')
sha256sums_x86_64=('4714f47044814733ca2e20ae8f64327ad0b71b912d6fe6f677f18c90b3a5f1c0')
source_aarch64=("$pkgname-$pkgver-aarch64::https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv${pkgver}/msdl-linux-arm64")
sha256sums_aarch64=('933beb694b3e59fc84cdccfb5cb3dfe48db3274046df88390cf8ee32070f4088')
sha256sums_aarch64=('9059678043de3995f67ddc3083badf674b572d9bba8e8118e511f57d1f80d3eb')
package() {
install -Dm755 "$srcdir/$pkgname-$pkgver-$CARCH" "$pkgdir/usr/bin/msdl"

View file

@ -1,12 +1,11 @@
# msdl-bin (AUR)
> **⚠️ Publishing blocked as of 2026-07-13:** Arch Linux disabled new AUR account
> registrations on 2026-06-15 after a malware campaign compromised 1,500+ AUR
> packages across several waves. There's no announced reopening date. This
> package is ready to publish (`PKGBUILD` + `.SRCINFO` below) the moment
> registration reopens — check the [aur-general mailing list](https://lists.archlinux.org/mailman3/lists/aur-general.lists.archlinux.org/)
> or [aur.archlinux.org](https://aur.archlinux.org/register/) periodically.
> Until then, macOS/Linux users should use the [Homebrew tap](https://github.com/starkSV/homebrew-msdl) instead.
> **✅ Registration reopened as of 2026-07-31:** Arch Linux disabled new AUR
> account registrations on 2026-06-15 after a malware campaign compromised
> 1,500+ AUR packages across several waves; registration is open again.
> This package (`PKGBUILD` + `.SRCINFO` below) is ready to publish — see
> "Publishing" below once an account + SSH key are set up at
> [aur.archlinux.org](https://aur.archlinux.org/register/).
`PKGBUILD` and `.SRCINFO` for the [msdl-bin](https://aur.archlinux.org/packages/msdl-bin) AUR package, tracked here so version bumps have the same history/review as the winget manifests.

View file

@ -43,7 +43,7 @@ const (
CUSTOMER_ID = "560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175"
PORT = ":3002"
latestCLIVersion = "0.3.5"
latestCLIVersion = "0.3.7"
)
// --- Session cache (short-lived, used to chain /skuinfo → /proxy) ---
@ -132,7 +132,7 @@ func truncateBody(s string) string {
return s
}
// --- Negative cache (60s normal / 90min Sentinel lockdown) ---
// --- Negative cache (60s normal / 5h Sentinel lockdown) ---
type negCacheEntry struct {
Message string
@ -145,7 +145,10 @@ var (
negCache = make(map[string]negCacheEntry)
negCacheMu sync.RWMutex
negCacheTTL = 60 * time.Second
lockdownTTL = 90 * time.Minute
// Bumped from 90min on 2026-07-31: 181 retry attempts over 17 days, 0 successes.
// Retries yield zero value right now, so back off harder instead of hammering
// Microsoft (and our own IP's reputation) every 90 minutes for nothing.
lockdownTTL = 5 * time.Hour
)
// --- Singleflight (one in-flight Microsoft fetch per unique key) ---
@ -1184,6 +1187,10 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
respondJSONError(w, http.StatusBadRequest, "sku_id contains invalid characters")
return
}
if !validContributeProducts[productID] {
respondJSONError(w, http.StatusNotFound, "unknown product_id")
return
}
atomic.AddInt64(&mLinkRequests, 1)
cacheKey := productID + ":" + skuID
@ -1301,7 +1308,7 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&mLinkStale, 1)
if isSentinel {
// Sentinel lockdown: set 90-min neg cache, add lockdown header, skip
// Sentinel lockdown: set lockdownTTL neg cache, add lockdown header, skip
// background refresh — retrying extends the Sentinel block timer.
negCacheMu.Lock()
negCache[negKey] = negCacheEntry{
@ -1311,7 +1318,7 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
IsSentinel: true,
}
negCacheMu.Unlock()
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (90min), serving stale\n", productID, skuID)
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (%s), serving stale\n", productID, skuID, lockdownTTL)
w.Header().Set("X-MSDL-Lockdown", "1")
w.Header().Set("X-MSDL-Link-Status", "stale")
if exp := extractRawExpiry(cached.RawJSON); exp != "" {
@ -1365,7 +1372,7 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
}
negCacheMu.Unlock()
if isSentinel {
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (90min), no stale\n", productID, skuID)
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (%s), no stale\n", productID, skuID, lockdownTTL)
w.Header().Set("X-MSDL-Lockdown", "1")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{

View file

@ -1,3 +1,24 @@
module github.com/starkSV/msdl-cli
go 1.21
go 1.24.1
require (
github.com/bogdanfinn/fhttp v0.6.8
github.com/bogdanfinn/tls-client v1.15.1
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
)

48
cli/go.sum Normal file
View file

@ -0,0 +1,48 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4=
github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s=
github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg=
github.com/bogdanfinn/tls-client v1.15.1 h1:KiFAlED55DJ8Fcocn+/1nX6PrDFcttIHAf/GDkV6KN8=
github.com/bogdanfinn/tls-client v1.15.1/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM=
github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI=
github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -7,17 +7,25 @@ import (
"fmt"
"html"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
)
// msTLSProfile is the browser TLS/HTTP2 fingerprint the CLI presents to Microsoft.
// Kept in step with msUA's claimed Chrome version -- a mismatched UA vs. TLS
// fingerprint is itself a detectable signal, so if one changes, change both.
var msTLSProfile = profiles.Chrome_133
const (
msUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
msUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
msProfile = "606624d44113"
msLocale = "en-US"
msOrgID = "y6jn8c31"
@ -47,37 +55,6 @@ type EvalLink struct {
URL string
}
type simpleCookieJar struct {
mu sync.Mutex
cookies []*http.Cookie
}
func (j *simpleCookieJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
j.mu.Lock()
defer j.mu.Unlock()
for _, c := range cookies {
found := false
for i, existing := range j.cookies {
if existing.Name == c.Name {
j.cookies[i] = c
found = true
break
}
}
if !found {
j.cookies = append(j.cookies, c)
}
}
}
func (j *simpleCookieJar) Cookies(_ *url.URL) []*http.Cookie {
j.mu.Lock()
defer j.mu.Unlock()
out := make([]*http.Cookie, len(j.cookies))
copy(out, j.cookies)
return out
}
func newSessionID() string {
b := make([]byte, 16)
rand.Read(b)
@ -116,10 +93,16 @@ func mapDownloadType(n int) string {
}
}
func newSession() (*http.Client, string) {
func newSession() (tls_client.HttpClient, string) {
sessionID := newSessionID()
jar := &simpleCookieJar{}
client := &http.Client{Timeout: 15 * time.Second, Jar: jar}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithTimeoutSeconds(15),
tls_client.WithClientProfile(msTLSProfile),
tls_client.WithCookieJar(tls_client.NewCookieJar()),
)
if err != nil {
return nil, sessionID
}
q1 := url.Values{}
q1.Set("org_id", msOrgID)
@ -158,7 +141,7 @@ func newSession() (*http.Client, string) {
return client, sessionID
}
func msGet(client *http.Client, reqURL, productID string) ([]byte, error) {
func msGet(client tls_client.HttpClient, reqURL, productID string) ([]byte, error) {
req, _ := http.NewRequest("GET", reqURL, nil)
req.Header.Set("User-Agent", msUA)
req.Header.Set("Referer", referer(productID))
@ -249,7 +232,7 @@ func parseDownloadLinks(raw []byte) ([]DownloadLink, error) {
return links, nil
}
func fetchLanguages(client *http.Client, sessionID, productID string) ([]Language, error) {
func fetchLanguages(client tls_client.HttpClient, sessionID, productID string) ([]Language, error) {
q := url.Values{}
q.Set("profile", msProfile)
q.Set("productEditionId", productID)
@ -267,7 +250,7 @@ func fetchLanguages(client *http.Client, sessionID, productID string) ([]Languag
}
// fetchDownloadLinks returns parsed links and the raw Microsoft JSON (for cache contribution).
func fetchDownloadLinks(client *http.Client, sessionID, productID, skuID string) ([]DownloadLink, []byte, error) {
func fetchDownloadLinks(client tls_client.HttpClient, sessionID, productID, skuID string) ([]DownloadLink, []byte, error) {
wq := url.Values{}
wq.Set("profile", msProfile)
wq.Set("productEditionId", productID)
@ -335,7 +318,13 @@ func detectLang(rawURL string) string {
}
func fetchEvalLinks(evalURL string) ([]EvalLink, error) {
client := &http.Client{Timeout: 20 * time.Second}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithTimeoutSeconds(20),
tls_client.WithClientProfile(msTLSProfile),
)
if err != nil {
return nil, fmt.Errorf("creating http client: %w", err)
}
req, _ := http.NewRequest("GET", evalURL, nil)
req.Header.Set("User-Agent", msUA)
req.Header.Set("Accept", "text/html,application/xhtml+xml")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,200 +0,0 @@
# CLI Telemetry, Update Check & Help — Design Spec
**Date:** 2026-06-21
**Status:** Approved
**Scope:** CLI (cli/) + Backend (backend/main.go)
---
## Overview
Three related additions to the msdl CLI and backend:
1. **Telemetry** — anonymous usage counters sent on every CLI run, stored durably in Redis
2. **Update check** — CLI checks our backend for the latest version on every run, notifies if behind
3. **Help**`msdl --help` / `-h` prints usage with all flags and examples
No opt-in required. `MSDL_NO_TELEMETRY=1` skips both telemetry and update check silently.
Disclosed in README and `/cli` page with one line each.
---
## Backend Changes
### 1. New constant: `latestCLIVersion`
```go
const latestCLIVersion = "0.3.0"
```
Bumped manually in `backend/main.go` whenever a new CLI release is cut.
### 2. `GET /cli/version` — public, no auth
Returns the latest CLI version. The CLI calls this on every run.
```json
{ "latest": "0.3.0" }
```
No rate limiting needed — response is trivial and cacheable by the client.
### 3. `POST /telemetry` — public, no auth
Accepts one event per CLI run. Body:
```json
{
"action": "fetch" | "eval" | "list" | "interactive",
"product_id": "2618",
"eval_slug": "server-2025",
"platform": "windows" | "darwin" | "linux",
"version": "0.3.0",
"success": true
}
```
- `product_id` present only for `fetch` action
- `eval_slug` present only for `eval` action
- All other fields always present
- Returns `200 OK` with `{}` — CLI ignores the response
- Per-IP rate limit: ~10 req/min (same token bucket pattern as `/contribute`)
- Increments Redis counters via `HINCRBY` directly (no in-memory layer)
- If Redis unavailable: silently drop (telemetry is best-effort)
### 4. Redis telemetry keys
```
msdl:telemetry:actions hash { fetch, eval, list, interactive }
msdl:telemetry:platforms hash { windows, darwin, linux }
msdl:telemetry:versions hash { "0.2.0", "0.3.0", ... }
msdl:telemetry:products hash { "2618", "3262", "3113", ... }
msdl:telemetry:results hash { success, failed }
```
### 5. Existing metrics — Redis persistence
Current in-memory `atomic.Int64` counters are preserved for fast per-request increments. Added durability:
- **On startup**: seed in-memory counters from Redis (`HGETALL msdl:metrics:*`)
- **Every 5 minutes**: flush in-memory counters to Redis (`HSET msdl:metrics:*`)
- **On graceful shutdown**: final flush before exit
Redis keys for existing metrics:
```
msdl:metrics:sku hash { requests, hits, fetches, neg_hits }
msdl:metrics:link hash { requests, hits, fetches, neg_hits, stale }
msdl:metrics:eval hash { requests, hits, stale }
```
### 6. `/metrics` response — updated
Adds telemetry section to existing cache stats:
```json
{
"sku": { ... },
"link": { ... },
"eval": { ... },
"telemetry": {
"actions": { "fetch": 142, "eval": 31, "list": 8, "interactive": 67 },
"platforms": { "windows": 180, "darwin": 42, "linux": 18 },
"versions": { "0.3.0": 145, "0.2.0": 95 },
"products": { "3262": 134, "2618": 89, "3113": 67 },
"results": { "success": 390, "failed": 48 }
}
}
```
---
## CLI Changes
### 1. Version constant
Injected at build time via ldflags:
```
-ldflags "-X main.Version=0.3.0"
```
Added to the existing GitHub Actions release workflow (`cli-release.yml`). Fallback: `const Version = "dev"` in source.
### 2. Update check + telemetry — concurrent goroutines
On every run, immediately after flag parsing, two goroutines are launched:
```
main()
├── go updateCheck() — GET /cli/version, 500ms timeout, print notice if behind
└── go sendTelemetry() — POST /telemetry, fire-and-forget, result ignored
```
Both are non-blocking. The update check result is printed before any other output if it arrives within 500ms. If the timeout fires, it's silently skipped for that run.
**Update notice format** (printed before picker or output):
```
A new version of msdl is available: v0.3.0
Download: https://github.com/starkSV/windows-iso-downloader/releases/latest
```
Both goroutines are skipped entirely when `MSDL_NO_TELEMETRY=1` is set.
### 3. Telemetry payload construction
| Scenario | action | product_id | eval_slug | success |
|---|---|---|---|---|
| `msdl` (interactive) | `interactive` | set after pick | — | true/false |
| `msdl --id X --lang Y` | `fetch` | X | — | true/false |
| `msdl --eval slug` | `eval` | — | slug | true/false |
| `msdl --list` | `list` | — | — | true |
`platform` is set from `runtime.GOOS` at runtime (not build time — same result, simpler).
`version` is set from the `Version` constant.
Telemetry is sent **after** the main action completes so `success` reflects the actual outcome.
### 4. `--help` / `-h`
Prints a formatted usage block and exits 0. Shown automatically by Go's `flag` package or via a custom print if the existing CLI uses manual arg parsing.
```
msdl — Windows ISO downloader
Usage:
msdl Interactive mode — pick product and language
msdl --id <id> --lang <lang> Fetch link directly (skip picker)
msdl --eval <slug> Evaluation / Server ISO
msdl --list List all available products
Flags:
--id <id> Product ID (e.g. 3262 for Windows 11 25H2)
--lang <language> Language name (e.g. "English")
--eval <slug> Eval ISO slug (server-2025, server-2022, win11-ent, ...)
--list List all products and exit
--no-contribute Skip contributing link back to msdl web cache
-h, --help Show this help
Environment:
MSDL_NO_TELEMETRY=1 Disable anonymous usage reporting and update checks
MSDL_NO_CONTRIBUTE=1 Disable cache contribution
More info: https://msdl.tech-latest.com/cli
```
---
## Documentation Updates
- **README** — one line under CLI section: "msdl sends anonymous usage counts (action, platform, version) to help us understand which products are popular. Set `MSDL_NO_TELEMETRY=1` to opt out."
- **`/cli` page** — same one-liner in the contribute section, next to the `--no-contribute` note.
---
## What's Not In Scope
- No per-user tracking, no IP storage, no session IDs
- No telemetry dashboard UI (raw numbers via `/metrics` is sufficient)
- No forced update or auto-update — notification only
- No telemetry for the web frontend

View file

@ -0,0 +1,184 @@
# Spec: "Needs Warming" Community Help Page
**Status:** Proposed
**Date:** 2026-07-13
**Author:** Shekhar + Grok (original idea), refined against live codebase by Claude
**Related:** Crowdsourced cache warming (`/contribute`), Sentinel lockdown resilience, CLI contribution flow
---
## 1. Goal
Give visible, honest signal for products currently failing web users because Microsoft's Sentinel WAF (or a rate limit) has blocked the backend and no cached link is available to fall back on. Point people at the CLI as the fix, and let a successful fetch/contribution clear the item automatically.
This is not a new mechanism bolted onto the project — it's a public window into resilience state (`negCache` / `linkCache`) that already exists and already drives the per-product "blocked, use CLI" banner. This just makes that signal visible in aggregate, with a stronger call to action.
**Change from the original draft:** a dedicated page, not a homepage section. Original draft (written by Grok from the GitHub repo alone, without runtime visibility into cache state) proposed a parallel tracking system; this version derives the list from state that already exists, which is both less code and immune to drift between two copies of "is this broken."
---
## 2. What Actually Needs Tracking (verified against `backend/main.go`)
`handleProxy` (main.go:1189-1404) has exactly three outcomes when a product/SKU is under an active Sentinel or rate-limit block:
| Outcome | Code path | User experience | Needs warming? |
|---|---|---|---|
| `serving cached` | negCache active, `linkCache` entry still within its own TTL (e.g. just contributed) | Gets a working link | **No** |
| `serving stale` | negCache active, `linkCache` entry expired but still returned | Gets a (possibly near-expired) link | **No** |
| `no stale available` | negCache active, nothing in `linkCache` at all | Gets a 429, no link | **Yes** — this is the actual failure |
So "needs warming" = **an active `negCache` entry with no corresponding valid-or-stale `linkCache` entry** — computed on read, not tracked as separate state. No new manager struct, no `Record()`/`Remove()` calls to keep in sync, no new Redis schema.
**Known gap, scoped out of MVP:** a fetch that fails with a non-rate-limit error (network blip, malformed response) and has no stale cache to fall back on returns an error directly (main.go:1377) without ever touching `negCache`. In practice this hasn't been the dominant failure mode this month — Sentinel/rate-limit blocks have been — so the MVP doesn't cover it. If it turns out to matter, closing the gap is a two-line addition (write a `negCacheEntry` with `IsSentinel: false` at that call site) rather than a redesign.
---
## 3. Data Needed That Doesn't Exist Yet
Two small additions, both minimal:
1. **A request counter per locked product/SKU.** `negCache`/`linkCache` don't track "how many times did this get hit while broken" — add a small `map[string]int` (or extend `negCacheEntry` with a counter field) incremented at the existing `"no stale available"` log call sites.
2. **Consumer product display names on the backend.** `validContributeProducts` (main.go:476) is `map[string]bool` — just an ID allow-list, no names. Eval products already have names (`{Name, EvalURL}` map at main.go:207-211). Converting `validContributeProducts` to `map[string]string` (ID → Name) serves both its existing validation role and this new lookup need, with no behavior change to `/contribute`.
Language name hydration for consumer products reuses the existing `skuCache` (already populated per-product from the normal SKU-fetch flow) — look up the SKU by ID within that product's cached SKU list to get `Language`/`LocalizedLanguage`. No new structure needed; if the product isn't in `skuCache` yet, just omit the language field rather than blocking the response.
---
## 4. New Endpoint
### `GET /needs-warming`
Computed on each request by iterating `negCache`, filtering to entries where `IsSentinel` (or rate-limited) is still active **and** no valid `linkCache`/eval-cache entry exists for that key. No background job, no persistence — it's a live view.
**Query params:** `limit` (default 10, max 50)
**Response:**
```json
{
"items": [
{
"product_id": "3262",
"sku_id": "0x0409",
"product_name": "Windows 11 25H2",
"language": "English (United States)",
"is_eval": false,
"reason": "waf_blocked",
"last_seen": "2026-07-13T17:42:00Z",
"request_count": 47,
"cli_command": "msdl --id 3262 --lang \"English (United States)\""
}
],
"total": 3
}
```
- Public, unauthenticated, rate-limited (30 req/min per IP — matches the existing pattern used by `/contribute` at 5/min and `/telemetry` at 10/min)
- Sorted by `request_count` descending (surfaces the highest-impact items first, more useful than recency alone)
- Eval products included (`is_eval: true`, `cli_command` uses `msdl --eval <slug>` instead of `--id`)
---
## 5. Backend Implementation Outline
No new package-level state beyond the two additions in §3. The handler:
```go
func handleNeedsWarming(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !needsWarmingRL.allow(clientIP(r)) {
respondJSONError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
limit := parseLimitParam(r, 10, 50)
negCacheMu.RLock()
defer negCacheMu.RUnlock()
linkCacheMu.RLock()
defer linkCacheMu.RUnlock()
var items []needsWarmingItem
for key, neg := range negCache {
if time.Now().After(neg.ExpiresAt) {
continue // lockdown/rate-limit window itself already expired
}
if _, hasValidLink := resolveValidLink(key, linkCache); hasValidLink {
continue // already resolved via cached/stale link -- not a failure state
}
items = append(items, buildNeedsWarmingItem(key, neg))
}
sort.Slice(items, func(i, j int) bool { return items[i].RequestCount > items[j].RequestCount })
if len(items) > limit {
items = items[:limit]
}
json.NewEncoder(w).Encode(map[string]interface{}{"items": items, "total": len(items)})
}
```
Integration points:
- Increment the new request counter at the existing `"lockdown active, no stale available"` and first-time-Sentinel/rate-limit-with-no-stale log sites (main.go:1225, ~1367)
- No changes needed to `/contribute` or the fresh-fetch success path — an item stops appearing automatically once `linkCache` has a valid entry, which those paths already populate
---
## 6. Frontend: Dedicated Page
**Route:** `/needs-warming` (or `/community` — bikeshed-able, not load-bearing)
Linked from the footer and from the per-product "blocked" banner (`CliHandoff.tsx`'s `highlight` variant) — "See what else needs help →" — so people already primed to help via the CLI can see the fuller picture instead of just their one product.
**Empty state:** hide the page's list section entirely (or show a short "Everything's healthy right now" line) — no "0 items" placeholder box. Most visits will likely find this empty, which is a good sign, not a gap to fill with UI.
Each card:
- Product name + language (or eval product name)
- Request count as a small badge (social proof — "47 people hit this")
- One-click "Copy CLI command" button (same interaction pattern as the existing `CliHandoff` component — reuse its copy-button styling)
- No per-item "reason" jargon exposed in the UI (WAF/rate-limit distinction is backend detail); just "temporarily unavailable"
Tone: matches the project's existing calm, transparent voice (README already explains the caching layer and Sentinel lockdown openly) — helpful, not alarmist.
---
## 7. CLI Impact
None required for MVP. Optional future addition: `msdl --needs-warming` prints the same list from the terminal and offers to fetch+contribute the top item directly — natural fit for the homepage-screen work already shipped (`cli/homepage.go`), but a separate phase.
---
## 8. Privacy & Safety
- Only ever stores/exposes `product_id`, `sku_id`/slug, reason, counters, timestamps — no IPs, no user identifiers (matches the existing telemetry/contribute philosophy already documented in README)
- Rate-limited public endpoint, same pattern as existing public endpoints
- Nothing new to abuse: the list is derived from state Microsoft's own WAF already put the backend into, not something a client can inject
---
## 9. Implementation Phases
| Phase | Description | Effort |
|---|---|---|
| MVP | Request counter + product-name map conversion + `/needs-warming` handler + dedicated page | Small — most of the state already exists |
| 2 | Link from per-product blocked banner; polish empty/loading states | Small |
| 3 | Close the non-rate-limit fetch-error gap (§2) if it turns out to matter in practice | Small, only if needed |
| 4 | `msdl --needs-warming` CLI command | Small |
(Dropped the original draft's "Redis-backed persistence" phase — this is inherently live/transient state mirroring `negCache`'s own in-memory, reset-on-restart behavior; persisting it would add a Redis key namespace for no real benefit.)
---
## 10. Open Questions Resolved
1. **Eval products included?** Yes.
2. **Max age before auto-removal?** No separate TTL — inherits the underlying `negCache` entry's own expiry (60s rate-limit / 90min Sentinel), since the list is computed live rather than tracked separately.
3. **Empty state?** Hide the list, don't show a "0 items" box.
4. **Tone?** Helpful/neutral, matching existing project voice.
---
## 11. Success Metrics (Future)
- Contributions triggered from this page specifically (could tag `/contribute` calls with a `source=needs-warming` query param to measure this)
- Reduction in time-to-resolution for locked-down products
- CLI download/usage bump correlated with items appearing on the page

View file

@ -1 +0,0 @@
/* /index.html 200

View file

@ -12,7 +12,7 @@
},
"2378": {
"name": "Windows 10 22H2 Home China (19045.2006)",
"badge": "EOL SOON",
"badge": "EOL",
"archs": [
"x64"
],
@ -22,7 +22,7 @@
},
"2618": {
"name": "Windows 10 22H2 v1 (19045.2965)",
"badge": "EOL SOON",
"badge": "EOL",
"archs": [
"x64",
"x86"

View file

@ -46,7 +46,6 @@
<url><loc>https://msdl.tech-latest.com/product/win11-ent</loc><lastmod>2026-05-18</lastmod><priority>0.8</priority><changefreq>monthly</changefreq></url>
<!-- Product Pages -->
<url><loc>https://msdl.tech-latest.com/product/48</loc><lastmod>2026-05-12</lastmod><priority>0.8</priority><changefreq>monthly</changefreq></url>
<url><loc>https://msdl.tech-latest.com/product/52</loc><lastmod>2026-05-12</lastmod><priority>0.8</priority><changefreq>monthly</changefreq></url>
<url><loc>https://msdl.tech-latest.com/product/2378</loc><lastmod>2026-05-12</lastmod><priority>0.8</priority><changefreq>monthly</changefreq></url>
<url><loc>https://msdl.tech-latest.com/product/2618</loc><lastmod>2026-05-12</lastmod><priority>0.8</priority><changefreq>monthly</changefreq></url>

View file

@ -64,7 +64,7 @@ function InstallSteps() {
</div>
</div>
<div className="flex items-center justify-between gap-2 rounded-lg bg-white/4 border border-white/7 px-3 py-2">
<code className="text-[11px] font-mono text-zinc-400 truncate">curl -fsSL .../install.sh | bash</code>
<code className="text-[11px] font-mono text-zinc-400 truncate">curl -fsSL https://api.msdl.tech-latest.com/install.sh | bash</code>
<button onClick={handleCurlCopy} className="flex-shrink-0 text-zinc-500 hover:text-white transition-colors">
{curlCopied ? <Check size={12} className="text-green-400" /> : <Copy size={12} />}
</button>

View file

@ -45,6 +45,11 @@ export default function Dock() {
return (
<>
{/* Full-width shelf so the floating desktop dock separates from scrolled content page bg is
near-black (zinc-950), so plain color gradients are invisible; backdrop-blur is what reads.
Desktop only: the mobile dock is already a flush full-width bar with no gap to bridge. */}
<div className="hidden sm:block fixed inset-x-0 bottom-0 h-24 backdrop-blur-md bg-gradient-to-t from-zinc-950/90 via-zinc-950/60 to-transparent pointer-events-none z-40" />
{/* ── DESKTOP DOCK (sm+) — centered floating pill ── */}
<div className="hidden sm:flex fixed bottom-5 left-0 right-0 justify-center z-50 pointer-events-none">
<motion.div

View file

@ -1,12 +1,23 @@
import { useState } from 'react'
import { useState, type ReactNode } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { ChevronRight } from 'lucide-react'
interface FaqItem {
q: string
a: string
a: ReactNode
}
const repoLink = (
<a
href="https://github.com/starkSV/windows-iso-downloader"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 transition-colors"
>
GitHub
</a>
)
const faqs: FaqItem[] = [
{
q: 'Is this legal?',
@ -34,7 +45,13 @@ const faqs: FaqItem[] = [
},
{
q: 'Is MSDL open source?',
a: 'Yes. Both the frontend and the backend proxy are fully open source. The backend implements the same session-based flow as the Fido PowerShell script, ported to Go and Node.js. You can self-host the entire stack.',
a: (
<>
Yes. Both the frontend and the backend proxy are fully open source, on {repoLink}. The
backend and the standalone CLI both implement the same session-based flow as the Fido
PowerShell script, ported to Go. You can self-host the entire stack.
</>
),
},
]

View file

@ -1,5 +1,6 @@
import { motion } from 'motion/react'
import { useState, useEffect } from 'react'
import { evalProducts } from '../data/evalProducts'
interface Stat {
value: string
@ -7,14 +8,14 @@ interface Stat {
}
export default function StatsBar() {
const [totalReleases, setTotalReleases] = useState('17')
const [totalReleases, setTotalReleases] = useState((17 + evalProducts.length).toString())
useEffect(() => {
fetch('/data/products.json')
.then(r => r.json())
.then((data: Record<string, { active?: boolean }>) => {
const activeCount = Object.values(data).filter(p => p.active !== false).length
setTotalReleases(activeCount.toString())
setTotalReleases((activeCount + evalProducts.length).toString())
})
.catch(() => {})
}, [])

View file

@ -49,16 +49,8 @@ export default function AboutPage() {
<Section title="How it works">
<p>
Our backend replicates the session-based authentication flow that Microsoft uses to
serve download links to end users. The same approach is used by{' '}
<a
href="https://github.com/pbatard/Fido"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 transition-colors"
>
Fido
</a>{' '}
(the PowerShell script bundled with Rufus). The flow is:
serve download links to end users the same technique used by Rufus (credited below).
The flow is:
</p>
<ol className="list-decimal list-inside space-y-1 text-zinc-500 text-sm">
<li>Register a session with Microsoft's tracking endpoint</li>
@ -87,9 +79,22 @@ export default function AboutPage() {
</code>
</p>
<p>
Product IDs are maintained manually based on Microsoft's release cadence.
Windows 11 25H2, 24H2, Windows 10 22H2, and Windows 8.1 are currently listed.
New releases are added as Microsoft publishes them.
Product IDs are maintained manually based on Microsoft's release cadence, covering
consumer Windows (11, 10, 8.1) as well as Windows Server (20162025) and Windows 11
Enterprise evaluation editions. New releases are added as Microsoft publishes them
see the <a href="/products" className="text-blue-400 hover:text-blue-300 transition-colors">full catalog</a>.
</p>
</Section>
<Section title="CLI tool">
<p>
MSDL also ships as a standalone command-line tool,{' '}
<code className="text-[11px] font-mono bg-white/5 border border-white/7 px-1.5 py-0.5 rounded text-zinc-400">msdl</code>.
It runs the same Microsoft session flow directly from your own machine instead of through
our backend useful when Microsoft is rate-limiting our server, since a request from
your own connection isn't affected by that. See the{' '}
<a href="/cli" className="text-blue-400 hover:text-blue-300 transition-colors">CLI page</a> for
install instructions.
</p>
</Section>

View file

@ -31,7 +31,7 @@ export default function DisclaimerPage() {
>
<div>
<h1 className="text-2xl font-bold text-white mb-1">Disclaimer</h1>
<p className="text-xs font-mono text-zinc-600">Last updated: May 18, 2026</p>
<p className="text-xs font-mono text-zinc-600">Last updated: July 16, 2026</p>
</div>
<div className="p-4 rounded-xl border border-amber-500/15 bg-amber-500/6 text-amber-400/80 text-[12px] leading-relaxed">
@ -43,7 +43,9 @@ export default function DisclaimerPage() {
<p>
MSDL is an independent, open-source project. It is not produced, approved, or supported
by Microsoft Corporation. The name "Windows" and the Windows logo are registered
trademarks of Microsoft Corporation.
trademarks of Microsoft Corporation. This applies equally to the website and the{' '}
<code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">msdl</code>{' '}
command-line tool.
</p>
</Section>
@ -76,8 +78,9 @@ export default function DisclaimerPage() {
<Section title="Open source">
<p>
The source code is publicly available. You are free to inspect, fork, and self-host
this project under the terms of its open-source license. The project credits the
open-source Fido script by Pete Batard for the underlying session flow.
this project under the terms of its open-source license. See the{' '}
<a href="/about" className="text-blue-400 hover:text-blue-300 transition-colors">About page</a>{' '}
for credits.
</p>
</Section>
</motion.div>

View file

@ -35,7 +35,7 @@ const featured = [
name: 'Windows 10',
version: '22H2',
build: '19045.2965',
description: 'The final Windows 10 feature update. Security support until October 2025.',
description: 'The final Windows 10 feature update. Support ended Oct 2025; ESU through Oct 2026.',
badge: 'eol' as const,
archs: ['x64', 'x86'],
},
@ -121,7 +121,7 @@ export default function HomePage() {
<p className="text-zinc-400 text-base max-w-sm mx-auto mb-6 leading-relaxed">
Direct links from Microsoft's CDN. No ads. No registration.
</p>
<div className="flex items-center justify-center gap-4 text-[12px] text-zinc-500">
<div className="flex items-center justify-center flex-wrap gap-x-4 gap-y-1.5 text-[12px] text-zinc-500">
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
Always official
@ -136,6 +136,16 @@ export default function HomePage() {
<span className="w-1.5 h-1.5 rounded-full bg-zinc-600 inline-block" />
Free forever
</span>
<span className="text-zinc-700">·</span>
<a
href="https://github.com/starkSV/windows-iso-downloader"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 hover:text-zinc-300 transition-colors"
>
<span className="w-1.5 h-1.5 rounded-full bg-purple-500 inline-block" />
<span className="underline decoration-dotted decoration-zinc-600 underline-offset-4">Open source</span>
</a>
</div>
</motion.div>

View file

@ -31,14 +31,37 @@ export default function PrivacyPolicyPage() {
>
<div>
<h1 className="text-2xl font-bold text-white mb-1">Privacy Policy</h1>
<p className="text-xs font-mono text-zinc-600">Last updated: May 18, 2026</p>
<p className="text-xs font-mono text-zinc-600">Last updated: July 16, 2026</p>
</div>
<Section title="No data collected">
<Section title="No data collected on the website">
<p>
MSDL does not collect, store, log, or share any personally identifiable information.
There are no user accounts, no cookies, no analytics trackers, and no third-party
advertising scripts of any kind.
The MSDL website does not collect, store, log, or share any personally identifiable
information. There are no user accounts, no cookies, no analytics trackers, and no
third-party advertising scripts of any kind.
</p>
</Section>
<Section title="CLI tool: anonymous telemetry and link sharing">
<p>
The <code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">msdl</code>{' '}
CLI is a separate, optional download and sends two kinds of anonymous data by default,
both of which you can disable:
</p>
<p>
<strong className="text-zinc-400">Usage telemetry</strong> action type (fetch, eval, list,
interactive), platform (Windows/macOS/Linux), CLI version, and whether the run succeeded
or failed. No personal data, IP address, or product-key information is included. Disable
with <code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">--no-telemetry</code>{' '}
or the <code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">MSDL_NO_TELEMETRY=1</code> environment variable.
</p>
<p>
<strong className="text-zinc-400">Link contribution</strong> after a successful fetch, the
CLI can share the product ID, SKU ID, and Microsoft's raw signed-link response back to our
backend, so the next website visitor gets a cached hit instead of a fresh Microsoft request.
Only that Microsoft response is shared nothing about you or your machine. Disable with{' '}
<code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">--no-contribute</code>{' '}
or <code className="text-zinc-400 text-[11px] font-mono bg-white/5 px-1 py-0.5 rounded">MSDL_NO_CONTRIBUTE=1</code>.
</p>
</Section>

View file

@ -1,5 +1,5 @@
PackageIdentifier: starkSV.msdl
PackageVersion: 0.3.5
PackageVersion: 0.3.7
Platform:
- Windows.Desktop
MinimumOSVersion: 10.0.0.0
@ -8,7 +8,7 @@ Commands:
- msdl
Installers:
- Architecture: x64
InstallerUrl: https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.5/msdl-windows-amd64.exe
InstallerSha256: D9C8B3F86BC48C2819220365277691EB96E924A4DE796CC2816808D20E81FEB6
InstallerUrl: https://github.com/starkSV/windows-iso-downloader/releases/download/cli%2Fv0.3.7/msdl-windows-amd64.exe
InstallerSha256: 44E9225BFE12DCE801301C2A68CBC114F57BA3313F775740BFC9A83CD87A431F
ManifestType: installer
ManifestVersion: 1.6.0

View file

@ -1,5 +1,5 @@
PackageIdentifier: starkSV.msdl
PackageVersion: 0.3.5
PackageVersion: 0.3.7
PackageLocale: en-US
Publisher: starkSV
PublisherUrl: https://github.com/starkSV

View file

@ -1,5 +1,5 @@
PackageIdentifier: starkSV.msdl
PackageVersion: 0.3.5
PackageVersion: 0.3.7
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.6.0