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
6 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 −4
package.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": {

view file

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>`;

view file

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 }

view file

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.

view file

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 

view file

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 	});

view file