TITLE=WarpEngine Client ROUTE=/stores/warp-engine-client LOCALE=en MODIFIED=1792417593 URL=https://wiki.teletypegames.org/stores/warp-engine-client L=1 https://git.teletypegames.org/stores/warp-engine-client/releases L=2 #54 L=3 #68 bWarpEngine Client0 is the desktop app for a WarpEngine store: the catalog as a grid of cards, one click to install a title into your application menu, one to play it, one to remove it. Linux, macOS and Windows. The current release is b3.1.00, which knows WarpEngine 1.0. i[Image: The WarpEngine Client window: the store and the filter axes on the ileft, the catalog as a grid of game cards on the right] bThe store engine is part of the app.0 Reading the catalog, picking the release that fits the machine, unpacking it, writing the menu entry and remembering what went where all happen in process. There is no interpreter to find and no child process to parse: the app has bno runtime dependencies0 at all, not even a zip library. What lands on disk has not changed, though: cconfig.json0 and cstate.json0 keep the shape the shell store wrote, so a library installed by the CLI is picked up as it is. Which store it installs is not baked in: the client asks a registry (cGET c/api/stores0 on the site), and a record there is a bname and a catalog0, nothing more. Behaviour is the client's own business, and the games folder is a slug of the catalog host, so two stores on one machine never touch each other's files. 1What it needs 1═════════════ Nothing. No interpreter, no package manager, no admin rights: the app carries its own runtime and the store installs under your own user account. To idevelop it you need Node 22 or newer, and the toolchain installs with cmake setup0. 1Install 1═══════ Take the package for your machine from the 1releases and open it. On first run, with no store on the machine, the window offers what the registry publishes, or takes a catalog address you type. That is the whole setup. 2Opening it on macOS 2─────────────────── The build is ad-hoc signed but bnot notarised0, so macOS asks before running a copy that came from a browser: p xattr -dr com.apple.quarantine "/Applications/WarpEngine Client.app" iOpen Anyway0 under bSystem Settings ▸ Privacy & Security0 works too, after a blocked attempt. Only notarisation removes the step, and that needs a paid Apple Developer ID. Files the store downloads later are unaffected: the app fetches them over its own HTTP client, which does not set the quarantine flag. bv1.0.0 could not be opened at all0 — it reported i"is damaged"0. The bundle was never signed; only its main executable had the linker's ad-hoc signature, so there was no resource seal and Gatekeeper refused instead of asking. cscripts/after-pack.js0 now signs the bundle during the build, and the result verifies as cvalid on disk0. 1Signing in, and titles that cost money 1══════════════════════════════════════ bNothing in this client knows anything about a particular store.0 Price, whether an account is needed, where to buy, where to sign in — all of it comes from the catalog's server: WarpEngine 0.5 answers cGET /api/service0 with what it offers and puts an caccess0 block on every entry. That is why one build serves any shop. The endpoints and shapes the client reads are the bWECP — WarpEngine bCatalog Protocol0, written up on 2its own page. Where the server offers no sign-in (every WarpEngine before 0.5, and any store that sells nothing) the window shows none. Where it does: • the side menu grows an bAccount0 block: iSign in…0, then iSign out0; • signing in shows a bshort code0. The browser opens the store's page, the code is typed there, and approving it signs this device in. No password ever reaches the client — that is the point of the detour. • the token lives in the bOS keychain0 (Keychain, libsecret, DPAPI) via Electron's csafeStorage0, one per store. With no keychain it is not stored at all rather than written in the clear; the cost is signing in again next run. On a card, what a person may do is separate from what the machine can run: t┌──────────────────────────────┬─────────────────────────────────────────────┐ t│h State t│h What the card offers t│ t├──────────────────────────────┼─────────────────────────────────────────────┤ t│0 owned, or free t│0 bInstall0 t│ t├──────────────────────────────┼─────────────────────────────────────────────┤ t│0 not owned t│0 the bprice0, and bBuy0, which opens the store t│ t│0 t│0 page in a browser t│ t├──────────────────────────────┼─────────────────────────────────────────────┤ t│0 signed out, catalog gates it t│0 bSign in to install0 — the catalog cannot say t│ t│0 t│0 whose it is until it knows who is asking t│ t└──────────────────────────────┴─────────────────────────────────────────────┘ Buying happens in the browser: a checkout rebuilt here would be a second place to get card handling wrong. bRefresh0 afterwards turns the card into an iInstall0. Two status filters come with this — bOwned0 and bTo buy0 — because owning is not the same as having installed. An unbought title is bnot0 dimmed. Dimming means the imachine0 cannot run it; nothing is wrong with the machine when a title simply costs money. A bearer token is never sent to a host that did not issue it. A gated download redirects to signed storage — often somebody else's server — and some object stores reject a request that carries both an cAuthorization0 header and a signature. 1Which store it installs 1═══════════════════════ On first run the client fetches the registry. One store and there is nothing to decide; several and the setup screen shows a picker. p [ p { "name": "Teletype Games", "catalogUrl": "https://teletypegames.org" }, p { "name": "Some Other Store", "catalogUrl": "https://games.example.org" } p ] bA name and a catalog are the whole record.0 The client already knows host-to-asset mapping, install modes, platforms and behaviour. What it cannot know is identity, and identity is all a registry says. From a record it derives: • bthe store id0 — a slug of the catalog host (cteletypegames.org0 → cteletypegames0), or of the display name if that fails. It names the store home and the games folder. Two records naming the same catalog are the same store and land in the same place, so reinstalling never orphans anything. • bthe games folder0 — that slug inside the OS's usual place for installed programs. It is also the only subtree this store ever deletes from. • bwhich titles are listed0 — released, archived and demo. A catalog that publishes a demo means it to be played. A catalog does not have to be in the registry at all: bany WarpEngine catalog baddress0 can be added from the settings. A bare host is enough (chttps0 is assumed), and the name is taken from it. The registry address is the one site-specific thing left in the client, decided in three places, most specific first: a runtime cSTORES_API0, the address the package was bbuilt0 with, then ours. p STORES_API=http://127.0.0.1:8731/stores npm start # trying someth» p ing out p make dist STORES_API=https://games.example.org/api/stores # shipping it The build variant is written into the packaged app's cpackage.json (cwarpEngine.registryUrl0), so a client built for someone else's catalog needs no source change and nothing set on the user's machine. Adding a store is a database row on the site, not a release of this app. 1A branded build 1═══════════════ A build can carry a bbrand0: a name and a store. cmake dist BRAND_NAME="Teletype cGames Store" BRAND_STORE=https://teletypegames.org BRAND_ID=teletype-store bakes them into the package; cBRAND_NAME=… BRAND_STORE=… npm start0 tries one out. The name is the window title, the bar heading and the cproductName0 (Dock, c/Applications0, NSIS folder, c.desktop0 entry); the store is installed on the first run without a registry or a picker, under the brand's name, into the same store home a generic client would use for that catalog. The brand is an balternative default, not a cage0: every store on the machine is still listed and the settings still add and remove them. cBRAND_ID0 names the cappId0 and the package files so a branded and a generic client coexist; cRELEASES_API0 points iCheck for updates0 at the branded build's own releases. The settings' General tab says iBased on WarpEngine Client 3.1.00. The brand is data the build supplies — nothing in the source names a store. 1Use 1═══ Everything that is not a title lives in the bside menu0; the menu button folds it away and the state survives restarts. t┌─────────┬──────────────────────────────────────────────────────────────────┐ t│h Section t│h What it holds t│ t├─────────┼──────────────────────────────────────────────────────────────────┤ t│0 bStores0 t│0 every store on this machine, the open one marked. Clicking t│ t│0 t│0 another switches the grid, the filters and the folders, and the t│ t│0 t│0 client reopens there next time. Two stores from the same catalog t│ t│0 t│0 in different folders show their folder instead of their id. t│ t│0 t│0 Adding, renaming and removing stores happen in the settings. t│ t├─────────┼──────────────────────────────────────────────────────────────────┤ t│0 bAccount0 t│0 iSign in…0 / iSign out0, only where the catalog offers a sign-in. t│ t├─────────┼──────────────────────────────────────────────────────────────────┤ t│0 bFilters0 t│0 four axes that combine: the bstatus0 of a title on this machine t│ t│0 t│0 (iEverything0, iInstalled0, iUpdates0, iNot installed0, and where the t│ t│0 t│0 catalog gates anything, iOwned0 and iTo buy0), the bplatform0, the t│ t│0 t│0 bkind0 (native or hosted) and the bcategory0, when the catalog t│ t│0 t│0 publishes one — WarpEngine 0.12 carries a ccategory0 from the t│ t│0 t│0 metadata file into the catalog, so the axis appears as soon as t│ t│0 t│0 titles are tagged, and 0.13's vocabulary (cGET0 t│ t│0 t│0 c/api/software/vocabulary0) gives the chips their labels. Every t│ t│0 t│0 count says how many titles the option would show given the other t│ t│0 t│0 axes as they stand, and iClear0 appears while anything narrows the t│ t│0 t│0 catalog. The axes are built from what the catalog contains: a t│ t│0 t│0 platform with no titles is not listed, and a choice that t│ t│0 t│0 disappears falls back to iEverything0. t│ t├─────────┼──────────────────────────────────────────────────────────────────┤ t│0 bFooter0 t│0 the bgear0 (settings), the version number, bLog0 and bRefresh0. iLog0 t│ t│0 t│0 opens the store's own output, verbatim, plus the two folders t│ t│0 t│0 everything lands in. iRefresh0 re-reads the catalog. Titles t│ t│0 t│0 install one at a time from their cards; there is no t│ t│0 t│0 install-everything button. t│ t└─────────┴──────────────────────────────────────────────────────────────────┘ bThe catalog arrives a page at a time, and the grid draws as it comes. WarpEngine 0.13 pages cGET /api/software0; the client reads every page in turn and shows each as it lands, with the bar counting titles until the last page. What needs the whole — the iUpdates0 group, iUpgrade all0, the prune — waits for it, and the cache always holds the stitched whole, never a partial one. The grid is bwindowed0: it draws 48 titles and 48 more each time you near the bottom, so a large catalog costs what is on screen. A filter or search starts from the top; a page landing does not move your scroll. The bar holds a bsearch box0: what is typed narrows the grid to the titles whose title or name contains it, case and accents ignored, so clove0 finds iLÖVE0. It is part of the filter — the counts on the chips allow for it, iClear0 empties it, and so does Escape. The bar's toggle switches between the bgrid0 and a blist0; the list draws with the card's art, version and menu. 2Settings 2──────── i[Image: The settings dialog on the Stores tab: the installed store with its id iand catalog address, and the two ways to add one] The gear opens a dialog with everything that is not about the catalog in front of you, on three tabs: • bGeneral0 — the blanguage0 (follows the system, switchable: English and Hungarian) and the bdefault view0, grid or list, the same preference the bar's toggle writes. • bStores0 — every store on this machine with its id and catalog address. The bpencil0 renames a store in place: Enter saves, Escape cancels. Only the name can change, because the id, the folder and the games folder all derive from the catalog, so a different catalog is a different store. The bbin0 removes a store. Under the list, bAdd a store from the list0 offers what the registry publishes and this machine does not have yet, and bOr a catalog of your own takes any catalog address. Adding a store closes the dialog and opens the new store. • bUpdates0 — two blocks, kept apart because they are two different things. bThis bclient0 shows the version and a bCheck for updates0 that asks the client's own release list on the forge and answers in place: iThis is the latest version0, or iVersion X is available0 with a button that opens the release page in the browser. The app does not update itself and does not ask on its own; nothing happens until you click. bTitles with a newer version0 lists the installed titles the catalog has moved past, each with an bUpgrade0, and bUpgrade all when there is more than one. bRemoving a store uninstalls what it installed.0 The bin asks first, and says how many titles will go with it. A store's cstate.json0 is the only record of which payloads, icons and menu entries belong to it, so leaving the games behind would leave orphans nothing could ever identify. The catalog cache, the settings and any sign-in token go too. 2Cards 2───── A card's button is bInstall0, bUpdate0, or bPlay0 / bOpen0 once installed. The three-dot menu on an installed card holds bUpgrade0 (live only when the catalog has something newer), bReinstall0 and bUninstall0. Reinstall is for the copy that looks installed and does not work: Refresh and Upgrade leave an intact install alone, Reinstall removes it first and unpacks it again from the catalog, icon and menu entry included. Each card says whether it is bnative0 — unpacked and run locally, works offline — or bhosted0: a browser build the catalog serves, so its entry opens a page and needs the network. bEverything in the catalog is listed, including what this machine cannot binstall.0 Those cards are dimmed, carry an iunsupported platform0 or ino build for ithis machine0 badge with the engine's explanation, and have nothing to press — a Raspberry Pi facing an x86-only build, say. A store that hides them leaves the visitor guessing whether the catalog is small or their machine is odd. They have their own status, iNot for this machine0, and stay out of the native/hosted counts, because a title with no build has no mode to count. Since 2.7.0 the bCommodore 64 and Plus/40 titles install too: since WarpEngine 0.11 their releases carry yapeSDL bundles with a launcher named after the game that sets the machine and autostarts the cartridge, and a program named after the game wins program discovery outright. The cards name every platform the way the catalog does — iPlus/40, iTIC-800, iLÖVE0 — read from the server's build matrix (cGET /api/builds0), so a catalog can add a platform without a new client; an engine without the endpoint keeps the slugs. Every card carries a band of box art of the same height — the first letter of the title when there is no image — so buttons line up across a row. This was quietly broken until the window was photographed: rows split the window height evenly instead of following their content, which collapsed the art and clipped the buttons, while every automated count passed because the nodes were all in the page. While the store is working, only the things that would start a second call are disabled. The menu, the log drawer and the filters keep working, because they change the screen and nothing on disk. Anything installed here is a normal menu entry, so it also shows up in your launcher, Dock or Start menu. The app does not have to be running to play. 1Development 1═══════════ cmake0 is the front door; it wraps the npm scripts. cmake0 on its own lists everything. t┌────────────────┬───────────────────────────────────────────────────────────┐ t│h Target t│h What it does t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake setup0 t│0 install dependencies (checks the Node version first) t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake build0 t│0 compile TypeScript, bundle the preload and the renderer t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake typecheck0 t│0 type-check, emit nothing t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake lint0 t│0 the strict rule set (cmake lint-fix0 fixes what it can) t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake check0 t│0 typecheck, lint and every test suite — the gate t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake start0 t│0 run against whatever store is installed t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake icons0 t│0 render every icon format from cresources/icon.svg0 t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake smoke0 t│0 drive the store with no window and no Electron at all t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake storetest0 t│0 add, rename and remove a store in a sandbox — the only t│ t│0 t│0 code that deletes a tree t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake uitest0 t│0 load the window once and report what rendered t│ t│0 t│0 (cSELFTEST_SHOT=shot.png0 photographs it, settings tabs t│ t│0 t│0 included) t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake test0 t│0 the three suites t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake dist0 t│0 package for this machine (cdist-mac0, cdist-win0, cdist-linux0) t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake publish0 t│0 upload the packages in cdist/0 to the Gitea release t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake release0 t│0 package and publish in one go t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake clean0 t│0 remove cbuild/0 and the packages (cdistclean0 also drops t│ t│0 t│0 cnode_modules0) t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cmake version0 t│0 the versions involved, including whether ctea0 is there t│ t└────────────────┴───────────────────────────────────────────────────────────┘ The npm scripts still work directly (cnpm start0, cnpm run dist:mac0). The Makefile adds no logic beyond the release step, and every script that runs the app builds first, so there is no way to test a stale bundle. 2Continuous integration 2────────────────────── c.woodpecker.yaml0 lives in this repository, not in the update server's config extension: that extension serves game-platform pipelines, which build a cartridge and publish into the site's catalog, while this builds an application and publishes to a release. t┌─────────┬────────────────────────────────┬─────────────────────────────────┐ t│h Step t│h Image t│h What it does t│ t├─────────┼────────────────────────────────┼─────────────────────────────────┤ t│0 ccheck0 t│0 cwarp-engine-client-ci:node220 t│0 cnode_modules0 (see below), t│ t│0 t│0 t│0 type-check, lint, the smoke t│ t│0 t│0 t│0 test and the store lifecycle t│ t│0 t│0 t│0 test t│ t├─────────┼────────────────────────────────┼─────────────────────────────────┤ t│0 clinux0 t│0 celectronuserland/builder:220 t│0 AppImage and deb t│ t├─────────┼────────────────────────────────┼─────────────────────────────────┤ t│0 cwindows0 t│0 celectronuserland/builder:22-0 t│0 NSIS installer and portable t│ t│0 t│0 cwine0 t│0 exe, through Wine t│ t├─────────┼────────────────────────────────┼─────────────────────────────────┤ t│0 crelease0 t│0 cci-tools0 (by digest) t│0 on a tag only: creates the t│ t│0 t│0 t│0 release and attaches what it t│ t│0 t│0 t│0 built; curl, jq, zip and git t│ t│0 t│0 t│0 are in the image, so nothing is t│ t│0 t│0 t│0 installed t│ t└─────────┴────────────────────────────────┴─────────────────────────────────┘ The ccheck0 image is celectronuserland/builder:220 with this repo's cnode_modules preinstalled under c/ci0, next to the sha256 of the cpackage-lock.json0 they came from. It is built by cci/warp-engine-client.Dockerfile0 in the 3teletypegames repo, and the nightly cron rebuilds it when cmaster0 has a lock it has no clock-0 image for; the tag moves, so the step always pulls. cscripts/ci-npm.sh0 copies those modules when the lock matches (a second instead of 15 s of cnpm ci0), and runs cnpm ci0 when it does not. bmacOS stays a local build0 — Apple's toolchain exists only on a Mac — so a release has two halves: pushing a cvX.Y.Z0 tag builds Linux and Windows and bcreates the release0 with cRELEASE_NOTES.md0 as its body, and cmake release0 on a Mac pushes the macOS package onto the same release. Either half can go first. The window test is local too: it needs a display and a store on the machine. Publishing needs a cgitea_token0 repository secret with write access. Woodpecker does hand steps a forge credential, and the script uses it when the secret is missing, but it cannot be relied on: a manual build has it, a build started by the tag webhook does not. Two publishers exist — cscripts/release.sh0 drives the ctea0 login on a workstation, cscripts/ci-upload.sh0 speaks the API with whatever credential CI has. Both build steps end by checking what they produced: a package under 10 MB did not finish. A half-finished Wine build leaves a stub inamed0 like the real installer, and cls0 is perfectly happy with that. The Windows step cannot be rehearsed on an Apple Silicon Mac: Wine assumes 4 KB memory pages and the host has 16 KB ones. The Windows installer is bnot signed0: Windows warns about an unknown publisher until there is a code-signing certificate. Linux packages carry no signature by convention. 2Publishing a release 2──────────────────── p make release p make publish TAG=v1.0.2 # a tag other than package.json's p scripts/release.sh dist/one-file.dmg # just one package The tag comes from cpackage.json0, so cnpm version patch0 is the only place a version is set. The release is created if missing, and an attachment whose name is already there is breplaced0, so a rebuild plus a second cmake publish lands instead of erroring. Release notes come from cRELEASE_NOTES.md0 when present. The repository is read from corigin0, so a fork publishes to the fork. Package names have no spaces (cWarpEngineClient-2.3.0-arm64.dmg0), because a space in a release asset is a space in every ccurl0 that ever touches it. It needs ctea0 installed and logged in — the devarea repo has cmake tea0. Overridable: cTAG0, cREPO0, cTEA_LOGIN0, cNOTES0, cDIST0. bNode 22+ is needed to install0, not to run: Electron's installer is ESM-only and older Node cannot crequire()0 it. The packaged app carries its own runtime. cnpm run uitest0 uses its own user-data directory and skips the single-instance lock. Otherwise a copy the user already has open swallows the test process, which exits 0 and reads as a pass. The test scripts accept a sandbox store, which is how this repo is tested without touching a working installation. cSTORE_ROOT0 breplaces0 the search path rather than prepending to it: now that a store can be removed, a sandbox that can reach a working installation is not a sandbox. p STORE_ROOT=/tmp/sandbox-root npm start p SMOKE_HOME=/tmp/sandbox-root/ttg-desktop npm run smoke p SMOKE_HOME= SMOKE_TOKEN= npm run smoke # as a signed-in pe» p rson 2How it is put together 2────────────────────── TypeScript in layers, dependencies pointing inward. cSTRUCTURE.md0 in the repo is the map — layers, every pattern in use, naming rules — and it is the file to read before adding anything. t┌─────────────────────┬──────────────────────────────────────────────────────┐ t│h Layer t│h What lives there t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/shared/0 t│0 the IPC channel table, the bridge contract, the t│ t│0 t│0 DTOs, the message bundles t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/domain/0 t│0 models, ports and errors — no Electron, no Node t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/application/0 t│0 services and the domain-to-DTO mappers t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/infrastructure/0 t│0 adapters: the store engine, HTTP, the archive t│ t│0 t│0 reader, the filesystem, Electron t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/main/0 t│0 the window, the IPC controllers, the composition t│ t│0 t│0 root, the self-test t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/preload/0 t│0 the bridge, bundled into one file — a sandboxed t│ t│0 t│0 preload cannot require modules t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/renderer/0 t│0 the state store, the views and the renderer t│ t│0 t│0 controllers t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 csrc/scripts/0 t│0 the smoke test and the store lifecycle test: the t│ t│0 t│0 same services with no window t│ t└─────────────────────┴──────────────────────────────────────────────────────┘ Two properties the layers buy: • the catalog can be driven with bno window and no Electron0 — the smoke test assembles the same services against the same ports; • the window bnever receives a filesystem path0. A title crosses the bridge without one, and launching is asked for by name and resolved in the main process from the store's own state. The window is drawn from its state alone: the log, the gate, the settings dialog and the filter are fields in one store, every view has a single crender(state)0, and wiring happens only in the three composition roots (the app, the smoke test, the lifecycle test). Data that came over the bridge or off the disk goes through a checked reader, never a cast. The linter is strict on purpose: explicit types on every signature, no cany0, no non-null assertions, exhaustive switches. A translation with a missing key does not compile, and neither does a channel the preload does not implement. bThe store engine0 is csrc/infrastructure/engine/0, behind the cStoreCatalogGateway port: the catalog client with its cache, the surveyor that says which titles this machine can install and why not the rest, the release picker, the host detector, the payload installer, the c.desktop0 / c.app0 / c.lnk0 launcher writers and the state file. Above the port nothing knows any of that exists. A RetroArch store would write playlists rather than menu entries, so it would be a second gateway and a second entry in cSTORE_ENGINES0, not a second code path. bReading a zip without a dependency.0 Node has no zip reader, so cZipArchive0 is one over cnode:zlib0: it walks the central directory, inflates cstored0 and cdeflate0 entries, restores the executable bit and symlinks from each entry's attributes — the SDL2 framework inside yapeSDL's c.app0 is a tree of them. It refuses zip64, unknown compression methods and paths that would escape the destination. bWhich WarpEngine served the catalog.0 Every WarpEngine API response carries a cWarpEngine-Version0 header. cSUPPORTED_WARP_ENGINE_VERSIONS0 lists the versions this client is written against, and each maps to the cCatalogDialect0 that reads its catalog shape. The switch is exhaustive, so adding a version stops the build until somebody says what it reads like. t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h What the header says t│h What happens t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 a supported version (0.2 to 1.0 t│0 its dialect reads the catalog, and t│ t│0 today, two dialects between them) t│0 the log names it t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 nothing at all t│0 read as the oldest supported version t│ t│0 t│0 — an engine older than 0.4.0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 older than anything supported t│0 the same, and the log says so t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 newer than anything supported t│0 the newest dialect is tried anyway, t│ t│0 t│0 with a warning that titles may be t│ t│0 t│0 missed t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ ccontextIsolation0 on, cnodeIntegration0 off, csandbox0 on, and a CSP that allows only the app's own script and stylesheet plus images over HTTPS. Links open in the real browser; the window never navigates. 1Verified, and not 1═════════════════ Every version is measured with cmake check0 — no type errors, no lint findings, three green suites — and the window photographs itself with cSELFTEST_SHOT0, which is how the collapsed grid rows (1.x) and the blank window of 2.6.0 were found: an inline c0 is not an cHTMLElement0, so the element check threw in the first constructor and nothing after it ran. The headless smoke test could not see that; the window check fails on it in the first second. • bThe store lifecycle0 — adding, renaming and removing a store — runs in a temporary root on every cmake check0, and the rename is read back from the file. • bThe self-test0 counts the cards, the filter rows and the card menus, opens the settings, counts its store rows against the side menu's, and closes it. • bThe registry path0 was exercised against a local endpoint serving the site's payload; the slug came out cteletypegames0 from the catalog host, and installing the same record twice landed in the same home. • bThe signing0 was measured, not assumed: quarantine flag set on a copy unzipped from the release artifact, ccodesign --verify --deep --strict0 clean, csyspolicy_check0 reporting only the expected i"adhoc signed"0 warning. bNot tried on Linux or Windows.0 The paths and the launch behaviour are written for them, and the CI builds the packages, but c.desktop0 and c.lnk0 launchers have been generated and read, not clicked. Notarisation, a Windows signing certificate and an in-app update are all open — see the 1releases for what each version changed.