Monorepo for Aesthetic.Computer aesthetic.computer
at main 325 lines 11 kB view raw
1// oven-edge — Cloudflare Worker that serves AC OS images from the edge. 2// Template disk images are cached in R2 (or DO Spaces fallback). Personalized 3// images are patched on-the-fly by overwriting the identity block and 4// legacy config placeholder. 5// 6// Routes: 7// /os/latest.img → latest template image (cached 24h at edge) 8// /os/<name>.img → specific build image (cached 24h) 9// /os/<name>.vmlinuz → specific build kernel (cached 24h) 10// /os-releases → build list (cached 1 min) 11// /os-image → personalized image (streaming patch at edge) 12// /* → proxy to oven origin (cached 5 min) 13 14const ORIGIN = "https://oven-origin.aesthetic.computer"; 15const SPACES = "https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com"; 16 17const IDENTITY_MARKER = "AC_IDENTITY_BLOCK_V1\n"; 18const IDENTITY_BLOCK_SIZE = 32768; 19const CONFIG_MARKER = '{"handle":"","piece":"notepat","sub":"","email":""}'; 20const DEFAULT_CONFIG_PATCH_SIZE = 4096; 21const EXPOSED_HEADERS = [ 22 "Content-Length", 23 "Content-Disposition", 24 "X-AC-OS-Requested-Layout", 25 "X-AC-OS-Layout", 26 "X-AC-OS-Fallback", 27 "X-AC-OS-Fallback-Reason", 28 "X-Build", 29 "X-Patch", 30].join(", "); 31 32function edgeHeaders(request, extra = {}) { 33 return { 34 "Access-Control-Allow-Origin": "*", 35 "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 36 "Access-Control-Allow-Headers": "Content-Type, Authorization", 37 "Access-Control-Expose-Headers": EXPOSED_HEADERS, 38 "X-Edge-Pop": request.cf?.colo || "unknown", 39 ...extra, 40 }; 41} 42 43function applyHeaders(res, request, extra = {}) { 44 const out = new Response(res.body, res); 45 for (const [k, v] of Object.entries(edgeHeaders(request, extra))) 46 out.headers.set(k, v); 47 return out; 48} 49 50// Build a 32KB identity block: marker + JSON + zero-padding 51function makeIdentityBlock(config) { 52 const json = JSON.stringify(config); 53 const header = IDENTITY_MARKER + json; 54 const encoder = new TextEncoder(); 55 const headerBytes = encoder.encode(header); 56 const block = new Uint8Array(IDENTITY_BLOCK_SIZE); 57 block.set(headerBytes); 58 return block; 59} 60 61function makeLegacyConfigBlock(config, size = DEFAULT_CONFIG_PATCH_SIZE) { 62 const json = JSON.stringify(config); 63 const encoder = new TextEncoder(); 64 const jsonBytes = encoder.encode(json); 65 const block = new Uint8Array(size); 66 block.fill(0x20); 67 block.set(jsonBytes.subarray(0, size)); 68 return block; 69} 70 71// Stream a template image from source, patching configured ranges on-the-fly. 72function streamPatchedImage(templateBody, patches) { 73 let bytesSeen = 0; 74 75 const { readable, writable } = new TransformStream({ 76 transform(chunk, controller) { 77 const chunkStart = bytesSeen; 78 const chunkEnd = bytesSeen + chunk.byteLength; 79 bytesSeen = chunkEnd; 80 81 let buf = null; 82 for (const patch of patches) { 83 const offset = patch.offset; 84 const size = patch.bytes.byteLength; 85 if (offset < 0 || chunkEnd <= offset || chunkStart >= offset + size) { 86 continue; 87 } 88 if (!buf) buf = new Uint8Array(chunk); 89 const patchStart = Math.max(0, offset - chunkStart); 90 const patchOffset = Math.max(0, chunkStart - offset); 91 const patchLen = Math.min(size - patchOffset, buf.length - patchStart); 92 buf.set( 93 patch.bytes.subarray(patchOffset, patchOffset + patchLen), 94 patchStart, 95 ); 96 } 97 98 if (!buf) { 99 controller.enqueue(chunk); 100 return; 101 } 102 controller.enqueue(buf); 103 }, 104 }); 105 106 templateBody.pipeTo(writable); 107 return readable; 108} 109 110// Get the latest manifest (build name, identity block offset, etc.) 111async function getManifest(env) { 112 // Try R2 first 113 if (env?.OS_IMAGES) { 114 const obj = await env.OS_IMAGES.get("latest-manifest.json"); 115 if (obj) return await obj.json(); 116 } 117 // Fallback: fetch from DO Spaces 118 const res = await fetch(SPACES + "/os/latest-manifest.json"); 119 if (res.ok) return await res.json(); 120 return null; 121} 122 123// Get template image body as a ReadableStream 124async function getTemplateStream(env, manifest) { 125 const buildName = manifest?.name; 126 if (!buildName) return null; 127 128 // Try R2 first 129 if (env?.OS_IMAGES) { 130 const obj = await env.OS_IMAGES.get(`builds/${buildName}/template.img`); 131 if (obj) { 132 const size = Number(obj.size || 0); 133 if (!manifest?.imageSize || !size || size === manifest.imageSize) { 134 return { body: obj.body, size: size || manifest?.imageSize || 0 }; 135 } 136 } 137 } 138 139 // Fallback: fetch from DO Spaces (with edge caching) 140 const res = await fetch(SPACES + "/os/native-notepat-latest.img", { 141 cf: { cacheTtl: 86400, cacheEverything: true }, 142 }); 143 if (res.ok) { 144 const size = Number(res.headers.get("content-length") || "0"); 145 if (!manifest?.imageSize || !size || size === manifest.imageSize) { 146 return { body: res.body, size: size || manifest?.imageSize || 0 }; 147 } 148 } 149 150 return null; 151} 152 153export default { 154 async fetch(request, env) { 155 const url = new URL(request.url); 156 const path = url.pathname; 157 158 // CORS preflight 159 if (request.method === "OPTIONS") { 160 return new Response(null, { headers: edgeHeaders(request) }); 161 } 162 163 // --- /os-image → personalized image (streaming edge patch) --- 164 if (path === "/os-image") { 165 const auth = request.headers.get("Authorization") || ""; 166 if (!auth) { 167 return new Response( 168 JSON.stringify({ error: "Authorization required" }), 169 { status: 401, headers: { ...edgeHeaders(request), "Content-Type": "application/json" } }, 170 ); 171 } 172 173 // 1. Fetch user config from oven origin (tiny JSON, fast) 174 const configRes = await fetch(ORIGIN + "/api/user-config" + url.search, { 175 headers: { Authorization: auth }, 176 }); 177 if (!configRes.ok) { 178 const body = await configRes.text(); 179 return new Response(body, { 180 status: configRes.status, 181 headers: { ...edgeHeaders(request), "Content-Type": "application/json" }, 182 }); 183 } 184 const config = await configRes.json(); 185 186 // 2. Get manifest (has patch offsets) 187 const manifest = await getManifest(env); 188 const hasImgManifest = 189 manifest?.artifactType === "img" && 190 Number.isFinite(manifest?.imageSize) && 191 manifest.imageSize > 0; 192 const hasIdentity = Number.isFinite(manifest?.identityBlockOffset) && manifest.identityBlockOffset >= 0; 193 const configOffsets = Array.isArray(manifest?.configOffsets) ? manifest.configOffsets : []; 194 if (!manifest || !hasImgManifest || (!hasIdentity && configOffsets.length === 0)) { 195 // No manifest or no offsets — fall through to oven origin for legacy patching 196 const ovenRes = await fetch(ORIGIN + "/os-image" + url.search, { 197 headers: { Authorization: auth }, 198 }); 199 return applyHeaders(ovenRes, request, { "X-Patch": "origin-fallback" }); 200 } 201 202 // 3. Get template image stream 203 const template = await getTemplateStream(env, manifest); 204 if (!template) { 205 // R2 + Spaces both failed — fall through to oven 206 const ovenRes = await fetch(ORIGIN + "/os-image" + url.search, { 207 headers: { Authorization: auth }, 208 }); 209 return applyHeaders(ovenRes, request, { "X-Patch": "origin-fallback" }); 210 } 211 212 // 4. Build patch payloads and stream with patch 213 const patches = []; 214 if (hasIdentity) { 215 patches.push({ 216 offset: manifest.identityBlockOffset, 217 bytes: makeIdentityBlock(config), 218 }); 219 } 220 const configBlock = makeLegacyConfigBlock( 221 config, 222 Number(manifest.configPatchSize || DEFAULT_CONFIG_PATCH_SIZE), 223 ); 224 for (const offset of configOffsets) { 225 if (Number.isFinite(offset) && offset >= 0) { 226 patches.push({ offset, bytes: configBlock }); 227 } 228 } 229 patches.sort((a, b) => a.offset - b.offset); 230 const patched = streamPatchedImage(template.body, patches); 231 232 const handle = config.handle || "unknown"; 233 const filename = `@${handle}-os-${config.piece || "notepat"}-AC-${manifest.name}.img`; 234 const requestedLayout = (url.searchParams.get("layout") || "img").toLowerCase(); 235 236 return new Response(patched, { 237 headers: { 238 ...edgeHeaders(request), 239 "Content-Type": "application/octet-stream", 240 "Content-Disposition": `attachment; filename="${filename}"`, 241 "Content-Length": String(template.size || manifest.imageSize), 242 "X-AC-OS-Requested-Layout": requestedLayout, 243 "X-AC-OS-Layout": "img", 244 "X-Build": manifest.name, 245 "X-Patch": "edge", 246 }, 247 }); 248 } 249 250 // --- /os/latest.img → latest template image from DO Spaces --- 251 if (path === "/os/latest.img" || path === "/os-template-img") { 252 const imgUrl = SPACES + "/os/native-notepat-latest.img"; 253 const res = await fetch(imgUrl, { 254 cf: { cacheTtl: 86400, cacheEverything: true }, 255 }); 256 const out = new Response(res.body, res); 257 out.headers.set( 258 "Content-Disposition", 259 "attachment; filename=ac-os-latest.img", 260 ); 261 for (const [k, v] of Object.entries(edgeHeaders(request))) 262 out.headers.set(k, v); 263 return out; 264 } 265 266 // --- /os/<name>.vmlinuz → named build kernel from DO Spaces --- 267 const vmlinuzMatch = path.match(/^\/os\/([a-z]+-[a-z]+)\.vmlinuz$/); 268 if (vmlinuzMatch) { 269 const name = vmlinuzMatch[1]; 270 const vmzUrl = SPACES + "/os/builds/" + name + ".vmlinuz"; 271 const res = await fetch(vmzUrl, { 272 cf: { cacheTtl: 86400, cacheEverything: true }, 273 }); 274 if (!res.ok) 275 return new Response("Build not found: " + name, { status: 404 }); 276 const out = new Response(res.body, res); 277 out.headers.set( 278 "Content-Disposition", 279 "attachment; filename=" + name + ".vmlinuz", 280 ); 281 for (const [k, v] of Object.entries(edgeHeaders(request))) 282 out.headers.set(k, v); 283 return out; 284 } 285 286 // --- /os/<name>.img → named build image from DO Spaces --- 287 const imgMatch = path.match(/^\/os\/([a-z]+-[a-z]+)\.img$/); 288 if (imgMatch) { 289 const name = imgMatch[1]; 290 const imgUrl = SPACES + "/os/builds/" + name + ".img"; 291 const res = await fetch(imgUrl, { 292 cf: { cacheTtl: 86400, cacheEverything: true }, 293 }); 294 if (!res.ok) 295 return new Response("Build not found: " + name, { status: 404 }); 296 const out = new Response(res.body, res); 297 out.headers.set( 298 "Content-Disposition", 299 "attachment; filename=" + name + ".img", 300 ); 301 for (const [k, v] of Object.entries(edgeHeaders(request))) 302 out.headers.set(k, v); 303 return out; 304 } 305 306 // --- Proxy everything else to oven origin --- 307 const originUrl = ORIGIN + path + url.search; 308 309 let ttl = 300; 310 if (/\/os-releases/.test(path)) ttl = 60; 311 else if (/\/health/.test(path)) ttl = 10; 312 313 const originReq = new Request(originUrl, { 314 method: request.method, 315 headers: request.headers, 316 body: 317 request.method === "GET" || request.method === "HEAD" 318 ? undefined 319 : request.body, 320 }); 321 const response = await fetch(originReq); 322 323 return applyHeaders(response, request, { "X-Cache-Ttl": String(ttl) }); 324 }, 325};