* Migrate Launcher to egui 0.37 and preserve current-main behavior
* egui037: record final published pins and local qualification
* fix(ui): gate CJK language drawing until fonts are installed
* Document migrated launcher UI contracts and regression invariants
Resolves F-019 (CWE-400), F-020 (CWE-117), and F-021 (CWE-117).
- Enforces per-byte read deadlines and socket timeouts during header parsing.
- Bounds maximum line length (8 KiB) and maximum total headers size (64 KiB) to prevent slowloris/DoS.
- Sanitizes request method and target before logging to stderr to prevent ANSI escape sequence injection.
compare_versions compared pre-release tags as plain strings, so
1.0.0-rc.10 sorted before 1.0.0-rc.9. `cargo xtask local` uses it to pick
the newest mirrored build for `status` and to choose what `prune` keeps,
so a tenth release candidate would have been treated as older than the
ninth and pruned first.
Parse with the workspace's semver crate and compare by precedence, which
orders numeric pre-release parts as numbers.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two local-service hardening findings:
- Single-instance replacement authority (CWE-862): the listener on the fixed loopback
port 127.0.0.1:47413 granted a Replace (which can make an idle launcher quit) on a
peer's claimed version alone, so a different local OS account could end the victim's
launcher. A per-user token now gates Replace: it lives in the user's own data folder
(created owner-only, 0600 on Unix), the second start echoes it in its hello, and the
primary only honours Replace when the token matches. An unauthenticated peer may
still bring the window forward (Show, harmless), never quit the launcher. The token
is derived without a new RNG crate, hashing OS-seeded RandomState with sha2.
- Local mirror server DoS (xtask local serve, CWE-770): a thread was spawned per
connection with no admission limit, request reads had no deadline (idle clients held
threads forever), and a failed thread spawn could panic the listener. Add a bounded
handler cap (64; excess connections get 503), a 15s request-header read deadline
lifted before streaming the body, and graceful handling of spawn failure.
Addresses findings: single_instance.rs CWE-862, xtask/local.rs CWE-770.
`civil_from_unix` (core) and `rfc3339` (xtask) took `i64` Unix seconds, which
the old hand-rolled Hinnant code converted across the whole `i64` range. The
jiff rewrite built the date with `epoch.checked_add(whole_days).unwrap_or(epoch)`,
but jiff's civil calendar only spans years -9999..=9999 — far narrower than
chrono's — so any instant past year 9999 silently became the epoch, changing
existing output. `i64::MAX` used to read `Dec 4, 292277026596` /
`292277026596-12-04T15:30:07Z`, and `i64::MIN` `Jan 27, -292277022657`, not 1970.
Fix: move the instant into jiff's range by whole 400-year Gregorian cycles
(146_097 days) before converting, then restore the real year afterwards. `rest`
is always under one cycle, so the in-range date lands in 1970..=2369 (well inside
jiff) and `rest * 86_400` can't overflow; the year is restored through `i128` so
`cycles * 400` is exact at the `i64` extremes. jiff stays the only calendar logic
(it does the month/day); the cycle count just shifts the year. No epoch fallback
for any value the old code converted. core still builds for wasm32.
Tests now pin the old output instead of the epoch:
- core civil_from_unix(i64::MAX) == (292277026596, 12, 4),
civil_from_unix(i64::MIN) == (-292277022657, 1, 27),
civil_from_unix(253_402_300_800) == (10000, 1, 1)
- xtask rfc3339(i64::MAX) == "292277026596-12-04T15:30:07Z",
rfc3339(i64::MIN) == "-292277022657-01-27T08:29:52Z",
rfc3339(253_402_300_800) == "10000-01-01T00:00:00Z"
The jiff date conversions already fall back to the epoch instead of
panicking, and format years with {:04} (rfc3339) or a plain integer
(format_date), unchanged from the old hand-rolled code. Add tests that
civil_from_unix(i64::MAX/MIN) and rfc3339(i64::MAX) stay on the epoch date
and never panic.
This repo already uses jiff (ui-egui, for the system time zone), so the
hand-rolled Howard Hinnant civil/days-from-civil copies move to jiff rather than
pulling in a second time crate. jiff is lifted to a workspace dependency:
- crates/core/src/time.rs: parse_rfc3339's days_from_civil and civil_from_unix
- xtask/src/local.rs: rfc3339 (feed timestamps)
ui-egui now takes jiff from the workspace. core keeps only civil-date math (no
time zone), so the wasm build is unaffected. Output and parsing semantics are
unchanged and pinned by the existing tests (RFC 3339 parsing, leap days, day
rollover, the feed's YYYY-MM-DDTHH:MM:SSZ). AGENTS.md now says time goes through
jiff.
Follow-up to storytold/cadcraft#1785
* Add FigCraft to the launcher
FigCraft (storytold/figcraft): interface design and prototyping, the
thirteenth Craft App and the sixth in the New group.
- Catalog row (brand colour #02da96 from FigCraft's icon README), details
page (no pictures yet), Linux desktop categories, icon tables, tagline
in every catalog, xtask local mirror list.
- Icon copied unmodified from FigCraft's repo; ATTRIBUTION.md row.
- The Windows installer art fits five rows of icons.
- Test that a companion figcraft-server tarball is never picked as the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release 0.4.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Install and update Craft apps from a local mirror
Add an opt-in developer mode for installing the Craft apps from builds on
this machine instead of GitHub:
- ARTCRAFT_LAUNCHER_DOWNLOAD_BASE (loopback http only) points the launcher
at a local mirror. While set, every check reads the mirror's
release_info.json, trusts download links under the mirror, never treats
entries as stale, and never falls back to GitHub or ArtCraft's API.
- cargo xtask local sync pulls every sibling app checkout (git pull
--ff-only --recurse-submodules), packages each app whose version isn't
mirrored yet with its own packaging script, and files the results under
<mirror>/<app>/<version>/ with SHA256SUMS.txt and notes from git log.
- cargo xtask local serve serves the mirror on 127.0.0.1 (with range
requests, so paused downloads resume) and writes the feed fresh from the
mirror's contents on every request.
- cargo xtask local status compares each checkout with what's mirrored.
* Sync the local mirror on an interval while serving it
cargo xtask local serve --sync-every <30m|1h|1d…> runs the same sync as
cargo xtask local sync on a background thread, one after another, so the
mirror follows origin without a second process. Downloads keep being
served during a build, and a version only joins the feed once its build
is complete. Pulls are --no-stat to keep the server's log readable.
* Build every new commit as a dev version with --every-commit
cargo xtask local sync --every-commit (and serve --sync-every … --every-commit)
builds each new commit rather than only new versions. A commit of a 0.4.0
checkout is packaged as 0.4.1-dev.<commit time> through the app's
<APP>_VERSION override, so it orders after 0.4.0, after earlier commits,
and before the real 0.4.1. The full version lands in the bundle's
<App>Version key, which the launcher reads, so it offers each build as an
update and sees it as current once installed.
* Keep the local mirror out of release builds; parse loopback URLs strictly
ARTCRAFT_LAUNCHER_DOWNLOAD_BASE makes the launcher trust the mirror's own
checksums and skip GitHub, so a shipped launcher must not read it: it now
needs the local-mirror cargo feature (cargo run --features local-mirror),
which release builds never enable. The loopback check parsed the URL by
prefix, so http://127.0.0.1:1@evil.example/ passed; it now requires the host
to be exactly 127.0.0.1, localhost or [::1] with a numeric port. The same
check guards ARTCRAFT_LAUNCHER_FEED_URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: khutchings <7854672+khutchings@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The launcher now looks like the apps page on getartcraft.com: the site's
colour tokens, square corners, 1 px hairlines instead of shadows, app cards
in a shared-hairline grid with numbered tabs and "Photo*Craft*" wordmarks,
mono upper-case labels and buttons, and square badges for status.
Fonts are the site's own, embedded in the binary: Archivo (620, 118 % width)
for headings and app names, Inter for body text and Geist Mono for labels,
all variable fonts from google/fonts (OFL-1.1) set to the site's weights.
The window has a custom title bar on Windows and Linux (drag to move,
double-click to maximize, minimize/maximize/close, resize from the edges)
and a transparent one on macOS with the traffic lights in the header.
The MSI used to flash a progress bar and close. Double-clicked, it now opens
a WixUI wizard (Welcome, Ready, progress, Finish with "Launch ArtCraft
Launcher"), with art drawn by package.ps1 from the ArtCraft mark and the
Craft apps' icons. Self-update still runs it with /passive. CI installs the
WiX UI and Util extensions.
The app icon is now the blue "A" mark, getartcraft.com's favicon.
packaging/icons.sh runs on any OS: `cargo xtask icns` packs the .icns.
Also: two clippy fixes in crates/native that newer clippy rejects, and
refreshed README screenshots.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A native Rust/egui launcher for the 12 Craft Apps (7 classic + 5 new).
- Grid (icon cards) or sortable table view, filters and search
- Status per app: available, installed and up to date, or update
available with version and release date
- Installs where each OS keeps apps, from the official release packages:
/Applications (DMG), Program Files (MSI), deb/rpm via the system
package manager (Flatpak/AppImage fallback), /usr/local on FreeBSD
- Detects apps installed by hand; updates keep the installed format
- Every download verified against its published SHA-256; running apps
are never replaced
- Tray/menu-bar mode, start at login, one notification per new version
- GitHub API with ETag cache, falling back to release pages when
rate-limited; release source is pluggable for a future catalog API
- Web build lists latest releases with per-platform downloads
- Packaging and release workflows for macOS, Windows (x64/x86/arm64),
Linux (x86_64/aarch64), FreeBSD and web
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>