feat(backend+frontend): Sentinel lockdown mode + stale UX overhaul

Backend (main.go):
- negCacheEntry gains IsSentinel bool; lockdownTTL = 90min constant
- Lockdown gate at top of handleProxy: serves stale with X-MSDL-Lockdown:1
  header or 429 {"error":"...","lockdown":true} — zero MS API calls made
- Sentinel stale path: sets 90min lockdown neg cache, adds lockdown header,
  suppresses background refresh (retrying extends the Sentinel block timer)
- Non-Sentinel stale path: unchanged (60s neg cache, bg refresh fires)
- Expose X-MSDL-Lockdown in Access-Control-Expose-Headers

Frontend (ProductDetailPage.tsx):
- isLockdown state: set from X-MSDL-Lockdown header or data.lockdown field
- Stale state hides expired download links entirely; shows CLI recommendation
  inside the card instead of sending users to look for options elsewhere
- Lockdown: red banner explaining 90min cooldown + highlighted CLI card,
  no Refresh button (it cannot help while Sentinel block is active)
- Normal stale: amber banner + highlighted CLI card + secondary Refresh button
- Outer CliHandoff suppressed when already shown inside card (stale states)

Frontend (CliHandoff.tsx):
- CLI command uses msdl.exe (primary audience is Windows users)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shekhar Vaidya 2026-06-25 12:07:00 +05:30
parent 0525015ca9
commit 3141635300
3 changed files with 232 additions and 97 deletions

View file

@ -115,18 +115,20 @@ var (
linkCacheMu sync.RWMutex
)
// --- Negative cache (60s — prevents thundering herd during rate-limit events) ---
// --- Negative cache (60s normal / 90min Sentinel lockdown) ---
type negCacheEntry struct {
Message string
HTTPCode int
ExpiresAt time.Time
Message string
HTTPCode int
ExpiresAt time.Time
IsSentinel bool // Sentinel WAF block: use lockdownTTL and suppress background refresh
}
var (
negCache = make(map[string]negCacheEntry)
negCacheMu sync.RWMutex
negCacheTTL = 60 * time.Second
lockdownTTL = 90 * time.Minute
)
// --- Singleflight (one in-flight Microsoft fetch per unique key) ---
@ -950,7 +952,7 @@ func enableCORS(next http.Handler) http.Handler {
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Expose-Headers", "X-MSDL-Link-Status, X-MSDL-Link-Expires")
w.Header().Set("Access-Control-Expose-Headers", "X-MSDL-Link-Status, X-MSDL-Link-Expires, X-MSDL-Lockdown")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
@ -1075,6 +1077,38 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
negKey := "link:" + cacheKey
forceRefresh := r.URL.Query().Get("force") == "true"
// 0. Sentinel lockdown gate — checked before everything, including force refresh.
// When a Sentinel block is active, zero Microsoft API calls are made regardless
// of force=true, to avoid extending the block timer.
negCacheMu.RLock()
lockdownEntry, hasLockdown := negCache[negKey]
negCacheMu.RUnlock()
if hasLockdown && time.Now().Before(lockdownEntry.ExpiresAt) && lockdownEntry.IsSentinel {
atomic.AddInt64(&mLinkNegHits, 1)
w.Header().Set("X-MSDL-Lockdown", "1")
linkCacheMu.RLock()
staleEntry, hasStale := linkCache[cacheKey]
linkCacheMu.RUnlock()
if hasStale {
atomic.AddInt64(&mLinkStale, 1)
log.Printf("/proxy: product_id=%s sku_id=%s -> lockdown active, serving stale (expires %s)\n",
productID, skuID, lockdownEntry.ExpiresAt.Format(time.RFC3339))
w.Header().Set("X-MSDL-Link-Status", "stale")
if exp := extractRawExpiry(staleEntry.RawJSON); exp != "" {
w.Header().Set("X-MSDL-Link-Expires", exp)
}
w.Write(staleEntry.RawJSON)
return
}
log.Printf("/proxy: product_id=%s sku_id=%s -> lockdown active, no stale available\n", productID, skuID)
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": lockdownEntry.Message,
"lockdown": true,
})
return
}
// 1. Check link cache (dynamic TTL, stale-on-failure) — skipped when force=true
linkCacheMu.RLock()
cached, hasCached := linkCache[cacheKey]
@ -1137,10 +1171,33 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
// 6. Stale-on-failure: serve expired cache entry rather than erroring out
// Also fire a background refresh so the next request gets fresh data.
isSentinel := strings.Contains(err.Error(), "Sentinel")
// Stale-on-failure: serve expired cache entry rather than erroring out.
if hasCached {
atomic.AddInt64(&mLinkStale, 1)
if isSentinel {
// Sentinel lockdown: set 90-min neg cache, add lockdown header, skip
// background refresh — retrying extends the Sentinel block timer.
negCacheMu.Lock()
negCache[negKey] = negCacheEntry{
Message: err.Error(),
HTTPCode: http.StatusTooManyRequests,
ExpiresAt: time.Now().Add(lockdownTTL),
IsSentinel: true,
}
negCacheMu.Unlock()
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (90min), serving stale\n", productID, skuID)
w.Header().Set("X-MSDL-Lockdown", "1")
w.Header().Set("X-MSDL-Link-Status", "stale")
if exp := extractRawExpiry(cached.RawJSON); exp != "" {
w.Header().Set("X-MSDL-Link-Expires", exp)
}
w.Write(cached.RawJSON)
return
}
log.Printf("/proxy: product_id=%s sku_id=%s -> fetch failed (%v), serving stale\n", productID, skuID, err)
go func() {
// Use singleflight so concurrent stale serves don't all hit Microsoft
@ -1168,17 +1225,32 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
return
}
// No stale data available.
code := http.StatusBadGateway
// 4. Negative cache for rate-limit errors (only when no stale available)
if _, isRL := err.(*rateLimitError); isRL {
code = http.StatusTooManyRequests
ttl := negCacheTTL
if isSentinel {
ttl = lockdownTTL
}
negCacheMu.Lock()
negCache[negKey] = negCacheEntry{
Message: err.Error(),
HTTPCode: code,
ExpiresAt: time.Now().Add(negCacheTTL),
Message: err.Error(),
HTTPCode: code,
ExpiresAt: time.Now().Add(ttl),
IsSentinel: isSentinel,
}
negCacheMu.Unlock()
if isSentinel {
log.Printf("/proxy: product_id=%s sku_id=%s -> Sentinel lockdown (90min), no stale\n", productID, skuID)
w.Header().Set("X-MSDL-Lockdown", "1")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
"lockdown": true,
})
return
}
}
respondJSONError(w, code, err.Error())
return

View file

@ -72,8 +72,8 @@ export default function CliHandoff({ productId, langName, langDisplay, defaultOp
const [copied, setCopied] = useState(false)
const command = langName
? `msdl --id ${productId} --lang "${langName}"`
: `msdl --id ${productId}`
? `msdl.exe --id ${productId} --lang "${langName}"`
: `msdl.exe --id ${productId}`
function handleCopy() {
navigator.clipboard.writeText(command)

View file

@ -64,6 +64,7 @@ export default function ProductDetailPage() {
const [expiryRemaining, setExpiryRemaining] = useState<number | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
const [linkStatus, setLinkStatus] = useState<'fresh' | 'cached' | 'stale' | null>(null)
const [isLockdown, setIsLockdown] = useState(false)
const expiryTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [meta, setMeta] = useState<{ badge: string; archs: string[]; active: boolean }>({ badge: '', archs: [], active: false })
@ -199,17 +200,23 @@ export default function ProductDetailPage() {
setLinkExpiry(null)
setExpiryRemaining(null)
setLinkStatus(null)
setIsLockdown(false)
if (expiryTimerRef.current) clearInterval(expiryTimerRef.current)
try {
const r = await fetch(`${API_BASE}/proxy?product_id=${productId}&sku_id=${selectedSku.Id}`)
const status = r.headers.get('X-MSDL-Link-Status') as 'fresh' | 'cached' | 'stale' | null
const expiresHeader = r.headers.get('X-MSDL-Link-Expires')
const lockdownHeader = r.headers.get('X-MSDL-Lockdown')
const data = await r.json()
if (data.error) throw new Error(data.error)
if (data.error) {
if (data.lockdown) setIsLockdown(true)
throw new Error(data.error)
}
if (!data.ProductDownloadOptions?.length) throw new Error('No download links returned.')
setDownloadLinks(data.ProductDownloadOptions)
setLinkStatus(status)
if (lockdownHeader === '1') setIsLockdown(true)
let expiry: Date | null = null
if (expiresHeader) {
const d = new Date(expiresHeader)
@ -271,11 +278,17 @@ export default function ProductDetailPage() {
const r = await fetch(`${API_BASE}/proxy?product_id=${productId}&sku_id=${selectedSku.Id}&force=true`)
const status = r.headers.get('X-MSDL-Link-Status') as 'fresh' | 'cached' | 'stale' | null
const expiresHeader = r.headers.get('X-MSDL-Link-Expires')
const lockdownHeader = r.headers.get('X-MSDL-Lockdown')
const data = await r.json()
if (data.error) throw new Error(data.error)
if (data.error) {
if (data.lockdown) setIsLockdown(true)
throw new Error(data.error)
}
if (!data.ProductDownloadOptions?.length) throw new Error('No download links returned.')
setDownloadLinks(data.ProductDownloadOptions)
setLinkStatus(status)
if (lockdownHeader === '1') setIsLockdown(true)
else setIsLockdown(false)
let expiry: Date | null = null
if (expiresHeader) {
const d = new Date(expiresHeader)
@ -556,91 +569,141 @@ export default function ProductDetailPage() {
transition={{ type: 'spring', stiffness: 300, damping: 28 }}
>
<div className="pt-4 border-t border-white/6 space-y-3">
{/* Stale warning */}
{linkStatus === 'stale' && (
<div className="flex items-start gap-3 p-3.5 rounded-xl bg-amber-500/8 border border-amber-500/20 text-amber-400">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
<p className="text-[12px] leading-relaxed">
This link is cached and may have expired — Microsoft is currently limiting automated requests. Use the options below for a guaranteed fresh download.
</p>
</div>
)}
{/* Expiry row */}
<div className="flex items-center justify-between gap-2">
<div className={`flex items-center gap-2 p-3 rounded-lg border text-[11px] flex-1 ${
showRefresh
? 'bg-red-500/6 border-red-500/15 text-red-400/80'
: 'bg-amber-500/6 border-amber-500/12 text-amber-400/80'
}`}>
<AlertTriangle size={12} className="flex-shrink-0" />
{expiryRemaining !== null
? expiryRemaining <= 0
? 'Links expired — refresh to get new ones'
: `Expires in ${formatRemaining(expiryRemaining)} · Direct from Microsoft CDN`
: 'Links expire in 24 hours · Direct from Microsoft CDN'
}
</div>
{showRefresh && (
<button
onClick={handleRefreshLinks}
disabled={isRefreshing}
className="flex items-center gap-1.5 px-3 py-2.5 rounded-lg bg-blue-500/12 border border-blue-500/20 text-blue-400 text-[11px] font-medium hover:bg-blue-500/20 disabled:opacity-40 transition-all flex-shrink-0"
>
<motion.span
animate={{ rotate: isRefreshing ? 360 : 0 }}
transition={isRefreshing ? { duration: 1, repeat: Infinity, ease: 'linear' } : {}}
className="inline-block"
>
<RefreshCw size={11} />
</motion.span>
Refresh
</button>
)}
</div>
{/* Links */}
{downloadLinks.map(link => {
const filename = link.Uri.split('/').pop()?.split('?')[0] ?? 'download.iso'
const arch = link.Architecture || archFromUri(link.Uri)
const isCopied = copiedUri === link.Uri
return (
<div
key={link.Uri}
className="flex items-center justify-between gap-3 p-3.5 rounded-xl bg-white/3 border border-white/5 hover:border-white/9 transition-colors"
>
<div className="min-w-0">
<p className="text-[12px] font-mono text-white truncate">{filename}</p>
<p className="text-[11px] text-zinc-600 mt-0.5">{arch}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{linkStatus === 'stale' ? (
/* Stale state: hide links, show banner + CLI recommendation */
<>
{isLockdown ? (
/* Sentinel lockdown — no Refresh, no links, CLI only */
<>
<div className="flex items-start gap-3 p-3.5 rounded-xl bg-red-500/8 border border-red-500/20 text-red-400">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
<p className="text-[12px] leading-relaxed">
Our server has been temporarily blocked by Microsoft and is in a 90-minute cooldown. Use the CLI to get your link instantly — it bypasses our server entirely.
</p>
</div>
{selectedSku && (
<CliHandoff
productId={productId!}
langName={selectedSku.Language}
langDisplay={selectedSku.LocalizedLanguage}
highlight
defaultOpen
/>
)}
</>
) : (
/* Normal stale — show warning, CLI, and Refresh */
<>
<div className="flex items-start gap-3 p-3.5 rounded-xl bg-amber-500/8 border border-amber-500/20 text-amber-400">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
<p className="text-[12px] leading-relaxed">
These links are cached and have expired — Microsoft is currently limiting automated requests.
</p>
</div>
{selectedSku && (
<CliHandoff
productId={productId!}
langName={selectedSku.Language}
langDisplay={selectedSku.LocalizedLanguage}
highlight
defaultOpen
/>
)}
<button
onClick={() => handleCopy(link.Uri)}
className="p-2 rounded-lg text-zinc-600 hover:text-white hover:bg-white/6 transition-all"
title="Copy link"
onClick={handleRefreshLinks}
disabled={isRefreshing}
className="w-full flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-xl bg-white/4 border border-white/8 text-zinc-400 text-[12px] font-medium hover:bg-white/7 disabled:opacity-40 transition-all"
>
{isCopied
? <Check size={13} className="text-green-400" />
: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="w-3.5 h-3.5" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
}
<motion.span
animate={{ rotate: isRefreshing ? 360 : 0 }}
transition={isRefreshing ? { duration: 1, repeat: Infinity, ease: 'linear' } : {}}
className="inline-block"
>
<RefreshCw size={12} />
</motion.span>
Try refreshing links
</button>
<a
href={link.Uri}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-500/12 border border-blue-500/20 text-blue-400 text-[11px] font-medium hover:bg-blue-500/20 transition-all"
>
<Download size={11} />
{arch}
<ExternalLink size={10} className="opacity-50" />
</a>
</>
)}
</>
) : (
/* Fresh / cached state: show expiry row + links */
<>
<div className="flex items-center justify-between gap-2">
<div className={`flex items-center gap-2 p-3 rounded-lg border text-[11px] flex-1 ${
showRefresh
? 'bg-red-500/6 border-red-500/15 text-red-400/80'
: 'bg-amber-500/6 border-amber-500/12 text-amber-400/80'
}`}>
<AlertTriangle size={12} className="flex-shrink-0" />
{expiryRemaining !== null
? expiryRemaining <= 0
? 'Links expired — refresh to get new ones'
: `Expires in ${formatRemaining(expiryRemaining)} · Direct from Microsoft CDN`
: 'Links expire in 24 hours · Direct from Microsoft CDN'
}
</div>
{showRefresh && (
<button
onClick={handleRefreshLinks}
disabled={isRefreshing}
className="flex items-center gap-1.5 px-3 py-2.5 rounded-lg bg-blue-500/12 border border-blue-500/20 text-blue-400 text-[11px] font-medium hover:bg-blue-500/20 disabled:opacity-40 transition-all flex-shrink-0"
>
<motion.span
animate={{ rotate: isRefreshing ? 360 : 0 }}
transition={isRefreshing ? { duration: 1, repeat: Infinity, ease: 'linear' } : {}}
className="inline-block"
>
<RefreshCw size={11} />
</motion.span>
Refresh
</button>
)}
</div>
)
})}
{downloadLinks.map(link => {
const filename = link.Uri.split('/').pop()?.split('?')[0] ?? 'download.iso'
const arch = link.Architecture || archFromUri(link.Uri)
const isCopied = copiedUri === link.Uri
return (
<div
key={link.Uri}
className="flex items-center justify-between gap-3 p-3.5 rounded-xl bg-white/3 border border-white/5 hover:border-white/9 transition-colors"
>
<div className="min-w-0">
<p className="text-[12px] font-mono text-white truncate">{filename}</p>
<p className="text-[11px] text-zinc-600 mt-0.5">{arch}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={() => handleCopy(link.Uri)}
className="p-2 rounded-lg text-zinc-600 hover:text-white hover:bg-white/6 transition-all"
title="Copy link"
>
{isCopied
? <Check size={13} className="text-green-400" />
: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="w-3.5 h-3.5" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
}
</button>
<a
href={link.Uri}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-500/12 border border-blue-500/20 text-blue-400 text-[11px] font-medium hover:bg-blue-500/20 transition-all"
>
<Download size={11} />
{arch}
<ExternalLink size={10} className="opacity-50" />
</a>
</div>
</div>
)
})}
</>
)}
</div>
</motion.div>
)}
@ -656,8 +719,8 @@ export default function ProductDetailPage() {
productId={productId!}
languageName={selectedSku?.LocalizedLanguage ?? ''}
/>
{/* Collapsed CLI only when highlighted card isn't already shown inside the card */}
{!(error !== null && languages.length > 0) && (
{/* Skip outer CLI card when already shown inside the main card (any stale or error) */}
{linkStatus !== 'stale' && !(error !== null && languages.length > 0) && (
selectedSku ? (
<CliHandoff
productId={productId!}