Monorepo for Aesthetic.Computer
aesthetic.computer
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};