From dd37b36f6e9fb0bd942b4744fdedfcb2912aee20 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:18:33 +0200 Subject: [PATCH] Mint the download token when the button is pressed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A friend got "The requested resource wasn't found." downloading the adult Windows installer. It is not a permissions problem and not missing data: the row and the file are both there, and the same download returns 206 with a valid token. That message is exactly what a protected file answers when the token is missing or expired, verified against production. releases.file is protected, so the URL needs a file token, and a file token lives 180 seconds -- read off the JWT's own exp claim, not guessed. The download list minted three of them while building the rows and baked them into the hrefs, so three minutes after the page loaded every button was permanently dead, and pressing it again could not help because the stale token was in the markup. Nothing suggested reloading. The owner never saw it because he presses the button as soon as the tab renders. The setup flow I added this week makes it near-certain for anyone else: it walks somebody through choosing services, creating accounts, paying and fetching a TMDB key before pointing them at Stáhnout, which is many minutes after the download tab was first rendered. So the token is minted in the click handler now, and the link carries download=1 so PocketBase sends an attachment rather than leaving the browser to decide what to do with a .exe or a .zip. The app was already correct: UpdateService mints its token immediately before _dio.download, so auto-update on the family's devices was never affected. This was the web page only. Co-Authored-By: Claude Opus 5 --- pb_public/index.html | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/pb_public/index.html b/pb_public/index.html index d4f4c3b..d6a4f3e 100644 --- a/pb_public/index.html +++ b/pb_public/index.html @@ -1201,22 +1201,45 @@ async function loadDownloads(){ li.style.cssText="padding:12px;border:1px solid var(--line);border-radius:10px;"+ "margin-bottom:8px;background:var(--sunk);font-size:14px;list-style:none"; if (m.ok && m.data && m.data.available){ - // `releases.file` is a protected file: the path alone 403s, it needs a - // short-lived file token. Minted per link, exactly as the landing page does. - var t=await api("POST","/api/files/token",null); - var href=(t.ok && t.data && t.data.token) - ? m.data.downloadPath+"?token="+encodeURIComponent(t.data.token) : null; + // **The token is minted when the button is pressed, not when this list is + // built.** `releases.file` is protected, so the path needs a file token, and a + // file token lives 180 seconds — measured, not assumed. Baking one into the + // href at render time meant every download button died three minutes after + // the page loaded, permanently, until someone thought to reload. PocketBase + // answers an expired token with 404 "The requested resource wasn't found.", + // which reads like the file is gone rather than like "press it again". + // + // That is not a hypothetical: a friend hit it on the Windows build. The setup + // flow makes it near-certain, because it walks somebody through buying + // subscriptions and fetching a TMDB key before it points them here, which is + // many minutes, and the download tab may have been rendered at sign-in. li.innerHTML='
'+esc(p[1])+'
'+ esc(m.data.version||"")+" · "+esc(m.data.variant||"")+'
'+ - (href ? 'Stáhnout' - : 'odkaz se nepodařilo vytvořit')+'
'; + ''; any=true; } else { li.innerHTML=""+esc(p[1])+'
zatím nic ke stažení
'; } box.appendChild(li); } + // Wired after the rows exist, so each press mints its own token. `download=1` + // makes PocketBase send the file as an attachment instead of leaving the browser + // to decide what to do with a .zip or an .exe. + Array.prototype.forEach.call(box.querySelectorAll("button[data-dl]"), function(b){ + b.onclick=async function(){ + busy(b,true,"Připravuji…"); + var t=await api("POST","/api/files/token",null); + busy(b,false); + if(!(t.ok && t.data && t.data.token)){ + setMsg($("dlMsg"),"Odkaz se nepodařilo vytvořit. Zkus to prosím znovu.","err"); + return; + } + setMsg($("dlMsg"),""); + window.location.href = b.getAttribute("data-dl") + + "?token=" + encodeURIComponent(t.data.token) + "&download=1"; + }; + }); if (!any) setMsg($("dlMsg"),"Zatím není publikovaná žádná verze.",""); }