// usemagpie.ai. Releases live on GitHub (yetone/magpie-releases); this // worker turns the newest one into the update feed the app reads and into // download links that never go stale. // // /api/latest {version, notes, url, published, assets: {name: {url, size, sha256}}} // /download the Apple Silicon dmg // /download/mac-arm64 the same; /download/mac-intel the Intel dmg // /download/windows the Windows app (x64); /download/windows-arm64 // /download/linux the Linux app (x86-64); /download/linux-arm64 // /download/ any file of the newest release, by name // /docs, /docs/zh the getting-started guide: /docs/start, /docs/zh/start // // Everything else is the static site in public/. const REPO = "yetone/magpie-releases"; const TTL = 300; // seconds the newest release is remembered const SHORT = { "mac-arm64": "magpie-darwin-arm64.dmg", "mac-intel": "magpie-darwin-amd64.dmg", "mac-amd64": "magpie-darwin-amd64.dmg", windows: "magpie-windows-amd64.exe", "windows-amd64": "magpie-windows-amd64.exe", "windows-arm64": "magpie-windows-arm64.exe", linux: "magpie-linux-amd64", "linux-amd64": "magpie-linux-amd64", "linux-arm64": "magpie-linux-arm64", }; // The bare docs paths open the getting-started guide. const DOCS = { "/docs": "/docs/start", "/docs/zh": "/docs/zh/start" }; export default { async fetch(req, env, ctx) { const url = new URL(req.url); if (url.hostname.startsWith("www.")) { url.hostname = url.hostname.slice(4); return Response.redirect(url.toString(), 301); } if (url.pathname === "/api/latest") { const rel = await latest(ctx); if (!rel) return json({ error: "no release yet" }, 503); return json(rel, 200, { "Cache-Control": `public, max-age=${TTL}` }); } if (url.pathname === "/download" || url.pathname.startsWith("/download/")) { const want = url.pathname.split("/")[2] || "mac-arm64"; const rel = await latest(ctx); const asset = rel && rel.assets[SHORT[want] || want]; if (!asset) return new Response("not found\n", { status: 404 }); return Response.redirect(asset.url, 302); } const guide = DOCS[url.pathname.replace(/\/+$/, "")]; if (guide) return Response.redirect(new URL(guide, url).toString(), 302); return beacon(await env.ASSETS.fetch(req)); }, }; // beacon adds Cloudflare Web Analytics to a page. The dashboard's automatic // injection skips whatever a worker returns, and every page passes through // this one. const BEACON = ``; function beacon(res) { if (!(res.headers.get("Content-Type") || "").startsWith("text/html")) return res; return new HTMLRewriter().on("body", { element: (el) => el.append(BEACON, { html: true }) }).transform(res); } // latest is the newest release, condensed, with each file's SHA-256 taken // from the release's SHA256SUMS. GitHub's API gives the notes and sizes but // limits anonymous callers by IP, and a worker shares its IP with many // others; when the API says no, the release page's redirect gives the // version and SHA256SUMS the files, which is all an update needs. async function latest(ctx) { const cache = caches.default; const key = new Request("https://usemagpie.ai/__latest"); const hit = await cache.match(key); if (hit) return hit.json(); const rel = (await fromAPI()) || (await fromPages()); if (!rel) return null; ctx.waitUntil(cache.put(key, json(rel, 200, { "Cache-Control": `max-age=${TTL}` }))); return rel; } const UA = { "User-Agent": "usemagpie.ai" }; async function fromAPI() { const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { headers: { ...UA, Accept: "application/vnd.github+json" }, }); if (!res.ok) { console.log("github api", res.status, await res.text()); return null; } const gh = await res.json(); const tag = gh.tag_name; const sums = await checksums(tag); if (!sums) return null; const rel = { version: tag.replace(/^v/, ""), notes: gh.body || "", url: gh.html_url, published: gh.published_at, assets: {} }; for (const a of gh.assets) { if (a.name === "SHA256SUMS") continue; rel.assets[a.name] = { url: a.browser_download_url, size: a.size, sha256: sums[a.name] || "" }; } return rel; } async function fromPages() { const res = await fetch(`https://github.com/${REPO}/releases/latest`, { headers: UA, redirect: "manual" }); const tag = (res.headers.get("Location") || "").split("/tag/")[1]; if (!tag) { console.log("github latest redirect", res.status); return null; } const sums = await checksums(tag); if (!sums) return null; const url = `https://github.com/${REPO}/releases/tag/${tag}`; const rel = { version: tag.replace(/^v/, ""), notes: "", url, published: null, assets: {} }; for (const [name, sha256] of Object.entries(sums)) { rel.assets[name] = { url: `https://github.com/${REPO}/releases/download/${tag}/${name}`, size: 0, sha256 }; } return rel; } // checksums reads a release's SHA256SUMS: {file name: hash}. async function checksums(tag) { const r = await fetch(`https://github.com/${REPO}/releases/download/${tag}/SHA256SUMS`, { headers: UA }); if (!r.ok) return null; const sums = {}; for (const line of (await r.text()).split("\n")) { const [hash, name] = line.trim().split(/\s+\*?/); if (hash && name) sums[name] = hash; } return sums; } function json(v, status = 200, headers = {}) { return new Response(JSON.stringify(v), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", ...headers }, }); }