From b61e7cf4e0ca834dad994424d4005a5be6da6852 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 1 Oct 2026 15:55:57 -0700 Subject: [PATCH] Handle WebView2 preflight in Windows tests and verify installed package --- aesel/windows/SmokeTest.cs | 4 ++++ aesel/windows/package.ps1 | 12 +++++++++++ aesel/windows/publish.mjs | 35 ++++++++++++++++++++++++++++++++ aesel/windows/setup.iss | 2 ++ system/public/aesel/index.html | 2 +- system/public/aesel/support.html | 2 +- 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 aesel/windows/publish.mjs diff --git a/aesel/windows/SmokeTest.cs b/aesel/windows/SmokeTest.cs index 307127f85a..419ad93f40 100644 --- a/aesel/windows/SmokeTest.cs +++ b/aesel/windows/SmokeTest.cs @@ -36,6 +36,10 @@ internal static class SmokeTest core.WebResourceRequested += (_, e) => { var uri = new Uri(e.Request.Uri); if (uri.Host == "aesel.app") return; // Packaged assets; no network. + if (e.Request.Method == "OPTIONS") { + e.Response = core.Environment.CreateWebResourceResponse(new MemoryStream(), 204, "No Content", "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, PUT, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\n"); + return; + } var body = "{}"; var type = "application/json"; if (uri.AbsolutePath == "/userinfo") body = "{\"sub\":\"auth0|windows-fixture\"}"; else if (uri.AbsolutePath == "/handle") body = "{\"handle\":\"windows-fixture\"}"; diff --git a/aesel/windows/package.ps1 b/aesel/windows/package.ps1 index 4524e718e6..6fb056b32d 100644 --- a/aesel/windows/package.ps1 +++ b/aesel/windows/package.ps1 @@ -4,6 +4,11 @@ $version = (Get-Content "$root/aesel/package.json" -Raw | ConvertFrom-Json).vers $revision = (git -C $root rev-parse HEAD).Trim() $dist = "$PSScriptRoot/dist" New-Item -ItemType Directory -Force $dist | Out-Null +New-Item -ItemType Directory -Force "$dist/deps" | Out-Null +$runtimeInstaller = "$dist/deps/WebView2Setup.exe" +Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $runtimeInstaller +$signature = Get-AuthenticodeSignature $runtimeInstaller +if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { throw 'WebView2 installer signature check failed' } dotnet publish "$PSScriptRoot/Aesel.csproj" -c Release -r win-x64 --self-contained true -p:Version=$version -p:InformationalVersion="$version+$revision" -o "$dist/app" if ($LASTEXITCODE) { throw 'Windows build failed' } $process = Start-Process "$dist/app/Aesel.exe" -ArgumentList @('--smoke-test', "`"$dist/smoke`"") -PassThru @@ -15,6 +20,13 @@ if (-not (Test-Path $iscc)) { throw 'Install Inno Setup 6 to package the install & $iscc "/DAppVersion=$version" "$PSScriptRoot/setup.iss" if ($LASTEXITCODE) { throw 'Installer build failed' } $name = "aesel-$version-windows-x64-setup.exe" +# Exercise the actual per-user installer as well as the publish output. +$installed = "$dist/installed" +$setup = Start-Process "$dist/$name" -ArgumentList @('/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART',"/DIR=`"$installed`"") -Wait -PassThru +if ($setup.ExitCode -ne 0 -or -not (Test-Path "$installed/Aesel.exe")) { throw 'Installer smoke failed' } +$process = Start-Process "$installed/Aesel.exe" -ArgumentList @('--smoke-test', "`"$dist/installed-smoke`"") -PassThru +if (-not $process.WaitForExit(120000)) { $process.Kill(); throw 'Installed app smoke timed out' } +if ($process.ExitCode -ne 0) { Get-Content "$dist/installed-smoke/error.txt" -ErrorAction SilentlyContinue; throw 'Installed app smoke failed' } $sha = (Get-FileHash "$dist/$name" -Algorithm SHA256).Hash.ToLowerInvariant() @{version=$version; revision=$revision; file=$name; sha256=$sha; architecture='x64'; signed=$false; minimumWindows='10'; channel='beta'} | ConvertTo-Json | Set-Content "$dist/latest.json" -Encoding utf8NoBOM "$sha $name" | Set-Content "$dist/SHA256SUMS.txt" -Encoding utf8NoBOM diff --git a/aesel/windows/publish.mjs b/aesel/windows/publish.mjs new file mode 100644 index 0000000000..31c6f41e86 --- /dev/null +++ b/aesel/windows/publish.mjs @@ -0,0 +1,35 @@ +// Publish only a Windows-tested build already preserved on canonical main. +// Usage: SPACES_KEY=… SPACES_SECRET=… node aesel/windows/publish.mjs ARTIFACT_DIR +import {readFile} from 'node:fs/promises'; +import {resolve,dirname,basename} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import {execFileSync,spawnSync} from 'node:child_process'; +const root=resolve(dirname(fileURLToPath(import.meta.url)),'../..'); +const dir=resolve(process.argv[2] || 'aesel/windows/dist'); +const manifest=JSON.parse(await readFile(resolve(dir,'latest.json'),'utf8')); +const result=JSON.parse(await readFile(resolve(dir,'result.json'),'utf8').catch(()=>readFile(resolve(dir,'smoke/result.json'),'utf8'))); +if(!result.passed) throw Error('Windows smoke must pass before publication.'); +if(!/^\d+\.\d+\.\d+$/.test(manifest.version) || manifest.file!==`aesel-${manifest.version}-windows-x64-setup.exe` || !/^[0-9a-f]{40}$/.test(manifest.revision)) throw Error('Invalid Windows release manifest.'); +execFileSync('git',['-C',root,'merge-base','--is-ancestor',manifest.revision,'origin/main']); +const sourceVersion=JSON.parse(execFileSync('git',['-C',root,'show',`${manifest.revision}:aesel/package.json`],{encoding:'utf8'})).version; +if(sourceVersion!==manifest.version) throw Error('Manifest version differs from its source.'); +const file=resolve(dir,manifest.file), bytes=await readFile(file); +if(bytes[0]!==0x4d || bytes[1]!==0x5a || createHash('sha256').update(bytes).digest('hex')!==manifest.sha256) throw Error('Windows installer checksum/type mismatch.'); +if(!process.env.SPACES_KEY || !process.env.SPACES_SECRET) throw Error('SPACES_KEY and SPACES_SECRET are required.'); +const env={...process.env,AWS_ACCESS_KEY_ID:process.env.SPACES_KEY,AWS_SECRET_ACCESS_KEY:process.env.SPACES_SECRET}; +const endpoint=process.env.SPACES_ENDPOINT || 'https://sfo3.digitaloceanspaces.com'; +const bucket='releases-aesthetic-computer', prefix='aesel/windows/'; +const aws=args=>execFileSync('aws',[...args,'--endpoint-url',endpoint],{env,stdio:['ignore','pipe','pipe'],encoding:'utf8'}); +const existing=spawnSync('aws',['s3api','head-object','--bucket',bucket,'--key',prefix+manifest.file,'--endpoint-url',endpoint],{env,encoding:'utf8'}); +if(existing.status===0) { + if(JSON.parse(existing.stdout).Metadata?.sha256!==manifest.sha256) throw Error('An immutable release with this name already exists. Bump the version.'); +} else { + if(!/404|Not Found|NotFound/.test(existing.stderr)) throw Error('Could not check the release bucket: '+existing.stderr); + aws(['s3','cp',file,`s3://${bucket}/${prefix}${manifest.file}`,'--acl','public-read','--content-type','application/vnd.microsoft.portable-executable','--cache-control','public, max-age=31536000, immutable','--metadata',`sha256=${manifest.sha256},revision=${manifest.revision}`]); +} +const url=`https://releases.aesthetic.computer/${prefix}${manifest.file}`; +const response=await fetch(url); +if(!response.ok || createHash('sha256').update(new Uint8Array(await response.arrayBuffer())).digest('hex')!==manifest.sha256) throw Error('Public installer bytes did not verify; feed was not updated.'); +for(const name of ['SHA256SUMS.txt','latest.json']) aws(['s3','cp',resolve(dir,name),`s3://${bucket}/${prefix}${basename(name)}`,'--acl','public-read','--content-type',name.endsWith('.json')?'application/json':'text/plain','--cache-control','no-cache']); +console.log(`Published and verified ${url}`); diff --git a/aesel/windows/setup.iss b/aesel/windows/setup.iss index e81c5da0bd..e67120fa1e 100644 --- a/aesel/windows/setup.iss +++ b/aesel/windows/setup.iss @@ -25,9 +25,11 @@ SetupMutex=AestheticComputer.Aesel.Setup AppMutex=Local\AestheticComputer.Aesel.Windows [Files] Source: "dist\app\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "dist\deps\WebView2Setup.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall [Icons] Name: "{autoprograms}\Aesel"; Filename: "{app}\Aesel.exe" [Run] +Filename: "{tmp}\WebView2Setup.exe"; Parameters: "/silent /install"; StatusMsg: "Checking Microsoft WebView2 Runtime…"; Flags: waituntilterminated Filename: "{app}\Aesel.exe"; Description: "Open Aesel"; Flags: nowait postinstall skipifsilent ; Account and notebook data are deliberately retained on uninstall. Reinstalling ; or updating must not delete art. Remove LocalAppData/Aesthetic Computer/Aesel diff --git a/system/public/aesel/index.html b/system/public/aesel/index.html index 5fcc46357e..bf649283c5 100644 --- a/system/public/aesel/index.html +++ b/system/public/aesel/index.html @@ -22,7 +22,7 @@

aesel

Download the Mac beta

0.8.19 beta · Apple silicon · macOS 14+

-Download the Windows beta +Download the Windows beta

0.8.19 beta · Windows 10/11 · x64 · AC braincells

diff --git a/system/public/aesel/support.html b/system/public/aesel/support.html index 97d6b24f72..b8a7cd94a7 100644 --- a/system/public/aesel/support.html +++ b/system/public/aesel/support.html @@ -1 +1 @@ -Aesel supportAesel

Aesel support

Email mail@aesthetic.computer for help. Include your Aesel version, macOS version, and what happened. Please leave passwords, tokens, and private conversations out of your report.

Getting started

Open Aesel, review the sharing notice, and sign into Aesthetic Computer. Claim a handle if you do not have one. Choose a media type, describe what you want to make, and refine it through conversation.

Hosted AI works without installing Claude or Codex. An internet connection is required, and generation limits apply.

Your work

Use /about for commands, /new for a new thread, and /versions to view revisions. Keep a copy of important work.

Privacy and transcript sharing

+Aesel supportAesel

Aesel support

Email mail@aesthetic.computer for help. Include your Aesel version, macOS or Windows version, and what happened. Please leave passwords, tokens, and private conversations out of your report.

Getting started

Open Aesel, review the sharing notice, and sign into Aesthetic Computer. Claim a handle if you do not have one. Choose a media type, describe what you want to make, and refine it through conversation.

Hosted AI works without installing Claude or Codex. An internet connection is required, and generation limits apply.

Windows beta

Use New piece and Saved here to manage local notebooks. The first Windows beta uses AC hosted generation; Claude/Codex CLI providers and automatic updates are not included. Sign-in opens your browser. If another AC sign-in is using port 44233, finish it before trying again.

The installer is unsigned. It requires Microsoft WebView2 Runtime. Uninstall keeps notebooks; sign out to remove saved credentials. Your app data is in %LOCALAPPDATA%\Aesthetic Computer\Aesel.

Your work

Use /about for commands, /new for a new thread, and /versions to view revisions. Keep a copy of important work.

Privacy and transcript sharing

-- 2.51.2