Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

os: enforce EFI image path, strengthen download validation, and clean native usb probes

+229 -82
+17 -5
fedac/native/ac-usb
··· 44 44 docker_usb " 45 45 apk add --quiet dosfstools 46 46 mkdir -p /mnt/usb 47 - mount ${USB_DEV}1 /mnt/usb 2>/dev/null && { 48 - echo '--- ${USB_DEV}1 ---' 47 + mounted='' 48 + for p in ${USB_DEV}1 ${USB_DEV}2; do 49 + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } 50 + done 51 + [ -n \"\$mounted\" ] && { 52 + echo \"--- \$mounted ---\" 49 53 find /mnt/usb -type f | head -50 50 54 echo '' 51 55 du -sh /mnt/usb/EFI/BOOT/BOOTX64.EFI 2>/dev/null 52 56 umount /mnt/usb 53 - } || echo 'Could not mount ${USB_DEV}1' 57 + } || echo 'Could not mount ${USB_DEV}1 or ${USB_DEV}2' 54 58 " 55 59 ;; 56 60 sha256|hash) ··· 58 62 docker_usb " 59 63 apk add --quiet dosfstools 60 64 mkdir -p /mnt/usb 61 - mount ${USB_DEV}1 /mnt/usb 65 + mounted='' 66 + for p in ${USB_DEV}1 ${USB_DEV}2; do 67 + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } 68 + done 69 + [ -z \"\$mounted\" ] && { echo 'Could not mount USB'; exit 1; } 62 70 sha256sum /mnt/usb/EFI/BOOT/BOOTX64.EFI 2>/dev/null || echo 'No EFI file found' 63 71 umount /mnt/usb 64 72 " ··· 100 108 docker_usb " 101 109 apk add --quiet dosfstools 102 110 mkdir -p /mnt/usb 103 - mount ${USB_DEV}1 /mnt/usb 111 + mounted='' 112 + for p in ${USB_DEV}1 ${USB_DEV}2; do 113 + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } 114 + done 115 + [ -z \"\$mounted\" ] && { echo 'Could not mount USB'; exit 1; } 104 116 cat '/mnt/usb/${FILE}' 2>/dev/null || echo 'File not found: ${FILE}' 105 117 umount /mnt/usb 106 118 "
-4
fedac/native/initramfs/init
··· 1 1 #!/bin/sh 2 - <<<<<<< Updated upstream 3 2 # AC Native OS init — DRM direct boot with crash recovery 4 - ======= 5 - # AC Native OS init — simple DRM direct boot (proven working on all ThinkPads) 6 - >>>>>>> Stashed changes 7 3 8 4 mount -t proc proc /proc 2>/dev/null 9 5 mount -t sysfs sysfs /sys 2>/dev/null
+12 -5
fedac/native/src/ac-native.c
··· 271 271 // Wait for USB block devices to appear (up to 2s after EFI handoff) 272 272 fprintf(stderr, "[ac-native] Waiting for USB block devices...\n"); 273 273 for (int w = 0; w < 100; w++) { 274 - if (access("/dev/sda1", F_OK) == 0 || access("/dev/sdb1", F_OK) == 0) break; 274 + if (access("/dev/sda1", F_OK) == 0 || access("/dev/sda2", F_OK) == 0 || 275 + access("/dev/sdb1", F_OK) == 0 || access("/dev/sdb2", F_OK) == 0) break; 275 276 usleep(20000); 276 277 } 277 - fprintf(stderr, "[ac-native] sda1=%s sdb1=%s\n", 278 + fprintf(stderr, "[ac-native] sda1=%s sda2=%s sdb1=%s sdb2=%s\n", 278 279 access("/dev/sda1", F_OK) == 0 ? "yes" : "no", 279 - access("/dev/sdb1", F_OK) == 0 ? "yes" : "no"); 280 + access("/dev/sda2", F_OK) == 0 ? "yes" : "no", 281 + access("/dev/sdb1", F_OK) == 0 ? "yes" : "no", 282 + access("/dev/sdb2", F_OK) == 0 ? "yes" : "no"); 280 283 const char *devs[] = { 281 - "/dev/sda1", "/dev/sdb1", "/dev/sdc1", "/dev/sdd1", 282 - "/dev/nvme0n1p1", "/dev/nvme1n1p1", 284 + "/dev/sda1", "/dev/sda2", 285 + "/dev/sdb1", "/dev/sdb2", 286 + "/dev/sdc1", "/dev/sdc2", 287 + "/dev/sdd1", "/dev/sdd2", 288 + "/dev/nvme0n1p1", "/dev/nvme0n1p2", 289 + "/dev/nvme1n1p1", "/dev/nvme1n1p2", 283 290 NULL 284 291 }; 285 292
+140 -40
oven/server.mjs
··· 2929 2929 const RELEASES_BASE = 'https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os'; 2930 2930 const TEMPLATE_ISO_URL = `${RELEASES_BASE}/native-notepat-latest.iso`; 2931 2931 const TEMPLATE_GZ_URL = `${RELEASES_BASE}/native-notepat-latest.img.gz`; // legacy fallback 2932 + const TEMPLATE_VMLINUZ_URL = `${RELEASES_BASE}/native-notepat-latest.vmlinuz`; 2933 + const TEMPLATE_CL_VMLINUZ_URL = `${RELEASES_BASE}/cl-native-notepat-latest.vmlinuz`; 2932 2934 const CONFIG_MARKER_LEGACY = '{"handle":"","piece":"notepat","sub":"","email":""}'; 2933 2935 const CONFIG_PAD_SIZE_LEGACY = 4096; 2934 2936 const IDENTITY_MARKER = 'AC_IDENTITY_BLOCK_V1'; ··· 2964 2966 templateCacheTime = Date.now(); 2965 2967 console.log(`[os-image] Template cached: ${(templateCache.length / 1048576).toFixed(1)}MB`); 2966 2968 return templateCache; 2969 + } 2970 + 2971 + function kernelUrlForVariant(variant) { 2972 + return variant === 'cl' ? TEMPLATE_CL_VMLINUZ_URL : TEMPLATE_VMLINUZ_URL; 2973 + } 2974 + 2975 + async function buildPersonalizedEfiImage({ kernelUrl, configJson }) { 2976 + const id = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; 2977 + const tmpBase = `/tmp/os-image-${id}`; 2978 + const kernelPath = `${tmpBase}-BOOTX64.EFI`; 2979 + const configPath = `${tmpBase}-config.json`; 2980 + const imagePath = `${tmpBase}.img`; 2981 + const efiOffsetSectors = 2048; 2982 + const efiOffsetBytes = efiOffsetSectors * 512; 2983 + let kernelData = null; 2984 + 2985 + try { 2986 + const kRes = await fetch(kernelUrl); 2987 + if (!kRes.ok) { 2988 + throw new Error(`Kernel download failed (${kRes.status})`); 2989 + } 2990 + kernelData = Buffer.from(await kRes.arrayBuffer()); 2991 + await fs.promises.writeFile(kernelPath, kernelData); 2992 + await fs.promises.writeFile(configPath, configJson); 2993 + 2994 + // Keep headroom for FAT metadata and future kernel size growth. 2995 + const minBytes = kernelData.length + Buffer.byteLength(configJson) + (32 * 1024 * 1024); 2996 + const imageSizeMiB = Math.max(384, Math.ceil(minBytes / 1048576) + 32); 2997 + 2998 + execSync(`dd if=/dev/zero of="${imagePath}" bs=1M count=${imageSizeMiB} status=none`); 2999 + execSync( 3000 + `printf 'label: gpt\nstart=${efiOffsetSectors}, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\n' | ` + 3001 + `sfdisk --force --no-reread "${imagePath}" >/dev/null`, 3002 + ); 3003 + execSync(`mkfs.vfat -F 32 --offset=${efiOffsetSectors} "${imagePath}" >/dev/null`); 3004 + execSync(`mmd -i "${imagePath}@@${efiOffsetBytes}" ::EFI ::EFI/BOOT`); 3005 + execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${kernelPath}" ::EFI/BOOT/BOOTX64.EFI`); 3006 + execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${configPath}" ::config.json`); 3007 + 3008 + return await fs.promises.readFile(imagePath); 3009 + } finally { 3010 + await Promise.allSettled([ 3011 + fs.promises.unlink(kernelPath), 3012 + fs.promises.unlink(configPath), 3013 + fs.promises.unlink(imagePath), 3014 + ]); 3015 + } 2967 3016 } 2968 3017 2969 3018 // User config endpoint for edge worker ISO patching ··· 3095 3144 3096 3145 console.log(`[os-image] Building personalized image for @${handle} (boot: ${bootPiece}, wifi: ${wifiEnabled}, claude: ${!!claudeToken}, git: ${!!githubPat})`); 3097 3146 3098 - // Get template (cached in memory) 3099 - let imgData; 3100 - try { 3101 - const template = await getTemplate(); 3102 - imgData = Buffer.from(template); // copy so we don't mutate cache 3103 - } catch (err) { 3104 - return res.status(503).json({ error: `Template not available: ${err.message}` }); 3105 - } 3147 + const variant = String(req.query.variant || '').toLowerCase() === 'cl' ? 'cl' : 'c'; 3148 + const layout = String(req.query.layout || '').toLowerCase(); 3149 + const preferEfiLayout = layout === 'efi' || layout === 'img' || layout === 'raw'; 3150 + const strictEfi = 3151 + preferEfiLayout && 3152 + String(req.query.strict || '1').toLowerCase() !== '0' && 3153 + String(req.query.strict || '1').toLowerCase() !== 'false'; 3106 3154 3107 3155 // Build personalized config JSON 3108 3156 const configObj = { ··· 3117 3165 if (!wifiEnabled) configObj.wifi = false; 3118 3166 const configJson = JSON.stringify(configObj); 3119 3167 3120 - // Try new identity block format first (32KB, marker-prefixed) 3121 - const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); 3122 - let idx = imgData.indexOf(identityMarkerBuf); 3123 - let patchCount = 0; 3124 - 3125 - if (idx !== -1) { 3126 - // New format: marker + newline + JSON + zero-padding to 32KB 3127 - while (idx !== -1) { 3128 - const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); 3129 - const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); 3130 - header.copy(block); 3131 - block.copy(imgData, idx); 3132 - patchCount++; 3133 - idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); 3168 + // Build a direct EFI image (single ESP partition) when requested. 3169 + // This layout matches local ac-os flash and is more firmware-compatible 3170 + // than some hybrid ISO scanners on older BIOS/UEFI implementations. 3171 + let imgData = null; 3172 + let contentType = 'application/x-iso9660-image'; 3173 + let extension = 'iso'; 3174 + let servedLayout = 'iso'; 3175 + let efiError = null; 3176 + if (preferEfiLayout) { 3177 + try { 3178 + const kernelUrl = kernelUrlForVariant(variant); 3179 + imgData = await buildPersonalizedEfiImage({ kernelUrl, configJson }); 3180 + contentType = 'application/octet-stream'; 3181 + extension = 'img'; 3182 + servedLayout = 'efi'; 3183 + console.log( 3184 + `[os-image] Built EFI image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB, variant=${variant})`, 3185 + ); 3186 + } catch (err) { 3187 + efiError = err; 3188 + console.warn(`[os-image] EFI layout build failed, falling back to ISO patch path: ${err.message}`); 3189 + if (strictEfi) { 3190 + return res.status(503).json({ 3191 + error: `EFI layout build failed: ${err.message}`, 3192 + requestedLayout: 'efi', 3193 + }); 3194 + } 3134 3195 } 3135 - console.log(`[os-image] Patched ${patchCount} identity block(s) for @${handle} (v1, 32KB)`); 3136 - } else { 3137 - // Legacy format: plain JSON padded to 4KB with spaces 3138 - const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); 3139 - idx = imgData.indexOf(legacyMarkerBuf); 3140 - if (idx === -1) { 3141 - return res.status(500).json({ error: 'Template image missing config placeholder' }); 3196 + } 3197 + 3198 + // Fallback: patch the template ISO in-place 3199 + if (!imgData) { 3200 + try { 3201 + const template = await getTemplate(); 3202 + imgData = Buffer.from(template); // copy so we don't mutate cache 3203 + } catch (err) { 3204 + return res.status(503).json({ error: `Template not available: ${err.message}` }); 3142 3205 } 3143 - const padded = configJson + ' '.repeat(Math.max(0, CONFIG_PAD_SIZE_LEGACY - configJson.length)); 3144 - const configBytes = Buffer.from(padded); 3145 - while (idx !== -1) { 3146 - configBytes.copy(imgData, idx); 3147 - patchCount++; 3148 - idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); 3206 + 3207 + // Try new identity block format first (32KB, marker-prefixed) 3208 + const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); 3209 + let idx = imgData.indexOf(identityMarkerBuf); 3210 + let patchCount = 0; 3211 + 3212 + if (idx !== -1) { 3213 + // New format: marker + newline + JSON + zero-padding to 32KB 3214 + while (idx !== -1) { 3215 + const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); 3216 + const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); 3217 + header.copy(block); 3218 + block.copy(imgData, idx); 3219 + patchCount++; 3220 + idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); 3221 + } 3222 + console.log(`[os-image] Patched ${patchCount} identity block(s) for @${handle} (v1, 32KB)`); 3223 + } else { 3224 + // Legacy format: plain JSON padded to 4KB with spaces 3225 + const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); 3226 + idx = imgData.indexOf(legacyMarkerBuf); 3227 + if (idx === -1) { 3228 + return res.status(500).json({ error: 'Template image missing config placeholder' }); 3229 + } 3230 + const padded = configJson + ' '.repeat(Math.max(0, CONFIG_PAD_SIZE_LEGACY - configJson.length)); 3231 + const configBytes = Buffer.from(padded); 3232 + while (idx !== -1) { 3233 + configBytes.copy(imgData, idx); 3234 + patchCount++; 3235 + idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); 3236 + } 3237 + console.log(`[os-image] Patched ${patchCount} config location(s) for @${handle} (legacy, 4KB)`); 3149 3238 } 3150 - console.log(`[os-image] Patched ${patchCount} config location(s) for @${handle} (legacy, 4KB)`); 3151 3239 } 3152 3240 3153 - // Stream the patched ISO (Fedora Media Writer / Balena Etcher / dd compatible) 3154 - addServerLog('success', '💿', `OS ISO for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); 3155 - res.setHeader('Content-Type', 'application/x-iso9660-image'); 3241 + // Stream the personalized image (ISO patch path or EFI-first image path) 3242 + addServerLog('success', '💿', `OS image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); 3243 + if (preferEfiLayout && servedLayout !== 'efi') { 3244 + addServerLog('warn', '⚠️', `OS image fallback for @${handle}: requested EFI but served ISO`); 3245 + } 3246 + res.setHeader('Content-Type', contentType); 3247 + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); 3248 + res.setHeader('Pragma', 'no-cache'); 3249 + res.setHeader('Expires', '0'); 3250 + res.setHeader('X-AC-OS-Requested-Layout', preferEfiLayout ? 'efi' : 'iso'); 3251 + res.setHeader('X-AC-OS-Layout', servedLayout); 3252 + if (efiError) { 3253 + res.setHeader('X-AC-OS-Fallback', '1'); 3254 + res.setHeader('X-AC-OS-Fallback-Reason', String(efiError.message || 'unknown').slice(0, 180)); 3255 + } 3156 3256 // Get latest build name for filename 3157 3257 let releaseName = 'native'; 3158 3258 try { ··· 3166 3266 const d = new Date(); 3167 3267 const p = (n) => String(n).padStart(2, '0'); 3168 3268 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 3169 - res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.iso"`); 3269 + res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.${extension}"`); 3170 3270 res.setHeader('Content-Length', imgData.length); 3171 3271 res.end(imgData); 3172 3272 });
+1 -1
system/deno.lock
··· 612 612 "workspace": { 613 613 "packageJson": { 614 614 "dependencies": [ 615 - "npm:@atproto/api@0.18", 615 + "npm:@atproto/api@~0.19.4", 616 616 "npm:@aws-sdk/client-s3@^3.975.0", 617 617 "npm:@aws-sdk/s3-request-presigner@^3.975.0", 618 618 "npm:@ffmpeg/core@~0.12.10",
+32 -15
system/public/aesthetic.computer/boot.mjs
··· 713 713 const IMPORT_MAX_RETRIES = 3; 714 714 const IMPORT_RETRY_DELAY = 1500; 715 715 716 - async function importWithRetry(modulePath, retries = IMPORT_MAX_RETRIES, useWsBundle = false) { 717 - const loader = window.acModuleLoader; 718 - const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; 719 - const retryDelay = isLocalhost ? 300 : IMPORT_RETRY_DELAY; 720 - let triedWs = false; 716 + async function importWithRetry(modulePath, retries = IMPORT_MAX_RETRIES, useWsBundle = false) { 717 + const loader = window.acModuleLoader; 718 + const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; 719 + const retryDelay = isLocalhost ? 300 : IMPORT_RETRY_DELAY; 720 + let triedWs = false; 721 + 722 + const clearLoaderCacheForPath = async (relativePath) => { 723 + if (!loader || !relativePath) return; 724 + try { loader.modules?.delete?.(relativePath); } catch (_) {} 725 + try { loader.blobUrls?.delete?.(relativePath); } catch (_) {} 726 + try { loader.bundleContents?.delete?.(relativePath); } catch (_) {} 727 + if (loader.db) { 728 + try { 729 + const tx = loader.db.transaction("modules", "readwrite"); 730 + tx.objectStore("modules").delete(relativePath); 731 + } catch (_) {} 732 + } 733 + }; 721 734 722 735 const waitForWsConnection = async () => { 723 736 if (!loader?.connecting) return; ··· 727 740 ]); 728 741 }; 729 742 730 - const tryLoadViaWs = async () => { 731 - if (!loader?.loadWithDeps) throw new Error('ws-unavailable'); 732 - triedWs = true; 733 - await waitForWsConnection(); 734 - if (!loader.connected) throw new Error('ws-not-connected'); 735 - const relativePath = modulePath.replace(/^\.\//, '').split('?')[0]; 736 - const blobUrl = await loader.loadWithDeps(relativePath, 5000); 737 - if (blobUrl && blobUrl.startsWith('blob:')) { 738 - return await import(blobUrl); 739 - } 743 + const tryLoadViaWs = async () => { 744 + if (!loader?.loadWithDeps) throw new Error('ws-unavailable'); 745 + triedWs = true; 746 + await waitForWsConnection(); 747 + if (!loader.connected) throw new Error('ws-not-connected'); 748 + const relativePath = modulePath.replace(/^\.\//, '').split('?')[0]; 749 + // Respect cache-busted URLs by clearing cached module before WS load. 750 + if (modulePath.includes('?v=')) { 751 + await clearLoaderCacheForPath(relativePath); 752 + } 753 + const blobUrl = await loader.loadWithDeps(relativePath, 5000); 754 + if (blobUrl && blobUrl.startsWith('blob:')) { 755 + return await import(blobUrl); 756 + } 740 757 throw new Error('ws-missing-blob'); 741 758 }; 742 759
+27 -12
system/public/aesthetic.computer/disks/os.mjs
··· 916 916 downloadMB = 0; 917 917 downloadTotalMB = 0; 918 918 const piece = BOOT_PIECES[bootPieceIdx]; 919 - downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; 920 - console.log("[os] Starting download:", osLabel(), "piece:", piece); 921 - needsPaint(); 922 - 919 + downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; 920 + console.log("[os] Starting download:", osLabel(), "piece:", piece); 921 + needsPaint(); 922 + 923 923 try { 924 - const query = "?piece=" + encodeURIComponent(piece) + "&wifi=" + (wifiEnabled ? "1" : "0") + (variantIdx === 1 ? "&variant=cl" : ""); 924 + const query = 925 + "?piece=" + encodeURIComponent(piece) + 926 + "&wifi=" + (wifiEnabled ? "1" : "0") + 927 + "&layout=efi&strict=1&cb=" + Date.now() + 928 + (variantIdx === 1 ? "&variant=cl" : ""); 925 929 const isoCandidates = [ 926 930 OVEN_ORIGIN + "/os-image" + query, 927 931 OVEN + "/os-image" + query, 928 932 ]; 929 - const MIN_EXPECTED_ISO_BYTES = 100 * 1024 * 1024; 933 + const MIN_EXPECTED_IMAGE_BYTES = 50 * 1024 * 1024; 930 934 931 935 let res = null; 932 936 let usedUrl = ""; ··· 949 953 continue; 950 954 } 951 955 956 + const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); 957 + if (servedLayout && servedLayout !== "efi") { 958 + console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); 959 + try { attempt.body?.cancel(); } catch (_) {} 960 + lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; 961 + continue; 962 + } 963 + 952 964 const len = parseInt(attempt.headers.get("content-length") || "0"); 953 - if (len > 0 && len < MIN_EXPECTED_ISO_BYTES) { 954 - console.warn("[os] Rejecting suspiciously small ISO response:", len, "bytes from", url); 965 + if (len > 0 && len < MIN_EXPECTED_IMAGE_BYTES) { 966 + console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url); 955 967 try { attempt.body?.cancel(); } catch (_) {} 956 968 lastErr = "Received suspiciously small image (" + len + " bytes)"; 957 969 continue; ··· 965 977 if (!res) { 966 978 throw new Error(lastErr || "Download failed"); 967 979 } 968 - console.log("[os] Download source:", usedUrl); 980 + console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); 969 981 970 982 const contentLength = parseInt(res.headers.get("content-length") || "0"); 971 983 downloadTotalMB = contentLength / 1048576; ··· 989 1001 downloadStatus = "preparing file..."; 990 1002 needsPaint(); 991 1003 992 - const total = chunks.reduce((s, c) => s + c.length, 0); 993 - const combined = new Uint8Array(total); 1004 + const total = chunks.reduce((s, c) => s + c.length, 0); 1005 + if (total < MIN_EXPECTED_IMAGE_BYTES) { 1006 + throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); 1007 + } 1008 + const combined = new Uint8Array(total); 994 1009 let offset = 0; 995 1010 for (const chunk of chunks) { 996 1011 combined.set(chunk, offset); ··· 1003 1018 const d = new Date(); 1004 1019 const p = (n) => String(n).padStart(2, "0"); 1005 1020 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 1006 - const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.iso`; 1021 + const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; 1007 1022 1008 1023 console.log("[os] Download complete:", filename, (total / 1048576).toFixed(1) + "MB"); 1009 1024 dlFn(filename, combined, { type: "application/octet-stream" });