a51b3aa79a12
end release download links in the stored filename (v3.0.217)
- time
- 2026-10-03 22:00 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- filenames in download links
- parent
- 1bbe304e31ca
↓ kepr-a51b3aa7.tar.gzatlas at this save
files changed6 files
package.json+1 −1src/pages/releases.ts+1 −1src/releases/feeds.ts+3 −3src/releases/routes.ts+3 −1tests/contract.test.ts+10 −2tests/releases.test.ts+5 −4package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.216",3+ "version": "3.0.217",44 "private": true,55 "type": "module",66 "scripts": {src/pages/releases.ts+1 −1
@@ -98,7 +98,7 @@9898 return html`<div class="release-file">9999 <p class="release-file-head"><span class="release-platform">${100100 PLATFORM_LABELS[file.platform]101- }</span>${html`<a class="release-download mono" href="${`/${repo}/releases/${version}/${repo}-${file.platform}`}">↓ ${file.name}</a>`}${html`<span class="release-meta mono">${formatSize(file.size)} · ${plural(downloads, "download")}</span>`}</p>101+ }</span>${html`<a class="release-download mono" href="${`/${repo}/releases/${version}/${encodeURIComponent(file.name)}`}">↓ ${file.name}</a>`}${html`<span class="release-meta mono">${formatSize(file.size)} · ${plural(downloads, "download")}</span>`}</p>102102 <p class="release-sha mono"><span class="muted">sha256</span> ${file.sha256}</p>103103 ${hint && html`<p class="release-hint muted">${hint}</p>`}104104 </div>`;src/releases/feeds.ts+3 −3
@@ -3,8 +3,8 @@33 44 // The machine-readable shapes of §22, written by hand rather than with JSON.stringify: clients in55 // the wild were built against Kepr 2's bytes, and Kepr 2 escaped `/`, `<` and `>` (its6-// `jsonEscapeStr`). URLs are assembled from validated repo names, versions and platforms, which need7-// no escaping, so their slashes stay bare, as Kepr 2 wrote them.6+// `jsonEscapeStr`). URLs are assembled from validated repo names, versions and platforms, and the7+// percent-encoded filename, which need no escaping, so their slashes stay bare, as Kepr 2 wrote them.88 99 const ESCAPES: Record<string, string> = {1010 '"': '\\"',@@ -48,7 +48,7 @@4848 export function releaseJson(domain: string, repo: string, release: Release): string {4949 const platforms = release.files.map(5050 (file) =>51- `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/${repo}/releases/${release.version}/${file.platform}","blake3":${str(file.blake3)},"sha256":${str(file.sha256)},"provenance":${provenance(release.version, file)},"signature":""}`,51+ `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/${repo}/releases/${release.version}/${encodeURIComponent(file.name)}","blake3":${str(file.blake3)},"sha256":${str(file.sha256)},"provenance":${provenance(release.version, file)},"signature":""}`,5252 );5353 return `${head(release)},"platforms":{${platforms.join(",")}}}`;5454 }src/releases/routes.ts+3 −1
@@ -50,7 +50,9 @@5050 ? requested.slice(name.length + 1)5151 : requested;5252 const release = f.releases.find((r) => r.version === version);53- const file = isPlatform(platform) && release?.files.find((x) => x.platform === platform);53+ const file =54+ release?.files.find((x) => x.name === requested) ||55+ (isPlatform(platform) && release?.files.find((x) => x.platform === platform));5456 return file ? download(c, name, version, file) : send(c, 404, RELEASE_NOT_FOUND);5557 };5658 // The newest visible release, or the response that says why there is none.tests/contract.test.ts+10 −2
@@ -132,14 +132,22 @@132132 });133133 134134 // koh's /api route is the frozen Koh 2 feed, tested on its own below.135+ // §30 2026-10-03: `url` alone departs from Kepr 2, ending in the file's stored name.135136 test.each(RELEASED.filter((r) => r !== "koh"))(136- "/api/%s/releases/latest is byte for byte Kepr 2's, with CORS",137+ "/api/%s/releases/latest is byte for byte Kepr 2's but for urls naming the file, with CORS",137138 async (repo) => {138139 const path = `/api/${repo}/releases/latest`;139140 const [res, rec] = [await get(path), await recorded(path)];140141 expect(res.status).toBe(rec.status);141142 expectHeaders(res, rec, ["content-type", "access-control-allow-origin"]);142- expect(await res.text()).toBe(rec.body);143+ const body = await res.text();144+ const blankUrls = (text: string) => text.replaceAll(/"url":"[^"]+"/g, '"url":""');145+ expect(blankUrls(body)).toBe(blankUrls(rec.body));146+ for (const [, url] of body.matchAll(/"url":"([^"]+)"/g)) {147+ const { pathname } = new URL(url as string);148+ const location = (await get(pathname)).headers.get("Location") ?? "";149+ expect(new URL(location).pathname).toEndWith(pathname.slice(pathname.lastIndexOf("/")));150+ }143151 },144152 );145153 tests/releases.test.ts+5 −4
@@ -191,7 +191,7 @@191191 const body = await (await get("/tool/releases")).text();192192 expect(body).toContain("Android · ARM64");193193 expect(body).toContain("macOS · Apple Silicon");194- expect(body).toContain('href="/tool/releases/0.10.0/tool-android-arm64"');194+ expect(body).toContain('href="/tool/releases/0.10.0/tool-0.10.0.apk"');195195 expect(body).toContain("3 B · 7 downloads");196196 expect(body).toContain(new Bun.CryptoHasher("sha256").update("apk").digest("hex"));197197 expect(body).toContain("unzip and drag to Applications");@@ -267,11 +267,11 @@267267 const body = await res.text();268268 expect(269269 body.startsWith(270- '{"version":"1.1.0","platforms":{"linux-x86_64":{"url":"https://kepr.example/cli/releases/1.1.0/linux-x86_64","blake3":"',270+ '{"version":"1.1.0","platforms":{"linux-x86_64":{"url":"https://kepr.example/cli/releases/1.1.0/cli","blake3":"',271271 ),272272 ).toBe(true);273273 expect(body).toContain(274- `"macos-aarch64":{"url":"https://kepr.example/cli/releases/1.1.0/macos-aarch64",`,274+ `"macos-aarch64":{"url":"https://kepr.example/cli/releases/1.1.0/Cli.app.zip",`,275275 );276276 expect(body).toContain(277277 `"sha256":"${sha}","provenance":{"version":"1.1.0","platform":"macos-aarch64","checksum":"blake3:`,@@ -316,11 +316,12 @@316316 for (const path of [317317 "/cli/releases/1.0.0/android-arm64",318318 "/cli/releases/1.0.0/cli-android-arm64",319+ "/cli/releases/1.0.0/cli-1.0.0.apk",319320 ]) {320321 expect((await get(path)).status).toBe(302);321322 }322323 expect(downloads()).toEqual([323- { version: "1.0.0", platform: "android-arm64", count: 3 },324+ { version: "1.0.0", platform: "android-arm64", count: 4 },324325 { version: "1.1.0", platform: "linux-x86_64", count: 1 },325326 ]);326327 });