Phase 3 of the improvement plan (PR 10 of the sequence): honest Wayland
behavior and a least-privilege alternative to the broad `input` group the
audit flagged as a keylogging surface.
Least-privilege input access:
- packaging/linux/common/60-stepforge-input.rules grants the ACTIVE session
read access to MOUSE devices only (ID_INPUT_MOUSE, excluding
ID_INPUT_KEYBOARD) via a systemd uaccess ACL — session-scoped, device-
scoped, and never keyboards. This replaces `usermod -aG input`, which grants
every input device (keyboards included) to the user permanently.
- scripts/linux/enable-click-capture.sh installs it opt-in: it prints the exact
rule, requires confirmation, and documents the security tradeoff. It never
runs the broad-group command.
Honest trigger reporting:
- New chooseCaptureTrigger() (app/platform/linux/diagnostics.js, re-exported
from app/platform/index.js) maps a capability profile to the real trigger:
per-click with a marker on X11+xinput; per-click WITHOUT coordinates or a
marker on Wayland evdev (the platform exposes no pointer position); and an
honest hotkey/interval fallback otherwise. It never promises per-click
capture with coordinates on Wayland.
- platform:capabilities now includes the active trigger for the machine and
the user's fallback setting, for the diagnostics UI.
Docs:
- GETTING_STARTED_WITH_LINUX.md no longer instructs every Wayland user to join
the `input` group; it presents that as a warning, documents the least-
privilege script, corrects the capability table (per-click is opt-in, mice
only; hotkey/interval is the default Wayland trigger), and points at
Settings → Diagnostics.
Tests: trigger decisions for X11/xinput, Wayland evdev (no coordinates/marker),
Wayland fallback (interval/hotkey with an honest note), the platform facade
wiring, Windows; the udev rule (mouse-only, excludes keyboards, uaccess); the
opt-in enable script (confirms, installs the rule, never runs usermod -aG
input as a command); and docs guards. 289 unit tests pass; startup smoke and
click self-test unchanged (stream, markers 3/3, burst 8/8).
Co-Authored-By: Claude Fable 5 <[email protected]>
Phase 3 of the improvement plan (PR 9 of the sequence): the dnf/Fedora
packaging half, in separate Linux-specific files, mirroring the apt/.deb work.
- packaging/linux/common/stage-runtime.sh: shared runtime-only payload staging
extracted from the .deb builder so the two package formats never drift. It
copies app code + a fixed Electron runtime + production npm deps (npm ls
--omit=dev), guards against dev-dep leaks, and fails without node_modules.
The .deb builder now delegates to it.
- packaging/linux/fedora/stepforge.spec: runtime Requires (nss, nspr, gtk3,
…), Recommends (xinput, portal, pipewire), %license, and a %post that makes
chrome-sandbox setuid and refreshes desktop/MIME/icon caches. No compilation
— it packages the prebuilt BuildRoot.
- packaging/linux/fedora/package.sh: stages the shared payload, substitutes
version/maintainer into the spec, and runs rpmbuild against the prebuilt
BuildRoot with detected arch. Requires rpmbuild + node_modules; emits an
.rpm + sha256.
- scripts/linux/dnf/install-runtime-deps.sh and install-build-deps.sh:
separate runtime vs build dependency sets for dnf (runtime installs no build
tools).
- docs/linux/dnf.md: Fedora/RHEL install + build guide, distinct from apt.md.
Tests: packaging-linux.test.js gains Fedora/dnf structural checks (spec
Requires + MPL-2.0 + %license + sandbox setup, builder shares staging +
requires rpmbuild, dnf build/runtime dep separation, shared staging never
copies the whole dev tree). tests/integration/linux/package-rpm.test.sh builds
a real .rpm and asserts runtime-only contents (honest skip when rpmbuild is
absent, as on this apt host).
Verified: 13 packaging unit tests pass; the refactored .deb builder still
produces a valid runtime-only package (integration test green); the .rpm
integration test skips cleanly where rpmbuild is unavailable.
Co-Authored-By: Claude Fable 5 <[email protected]>
Phase 3 of the improvement plan (PR 8 of the sequence): the apt/X11 packaging
half of Linux support, in separate Linux-specific files. Replaces the old
scripts/package-linux.sh, which the audit flagged as "not production
packaging" (it copied the whole dev node_modules — including vulnerable build
deps — plus docs/prompts/examples/audit files, hardcoded amd64, declared only
xinput, lacked desktop/icon/MIME integration, and could build without
node_modules).
Production builder (packaging/linux/debian/package.sh):
- Stages ONLY runtime files: app code, a fixed Electron runtime, and the
production npm deps (enumerated via npm ls --omit=dev). Never copies the
development node_modules; guards against electron-builder/app-builder-lib
leaking in. Fails if node_modules is absent instead of shipping an unusable
artifact.
- Detects architecture (dpkg --print-architecture, x64/arm64) rather than
hardcoding amd64. Generates DEBIAN/control from control.in with proper
runtime Depends, real maintainer, and homepage.
- Installs a desktop entry, hicolor icons (16–512), .sfgz/.sfglt MIME
registration, the launcher, and the license. postinst makes chrome-sandbox
setuid and refreshes desktop/MIME/icon caches; postrm cleans them.
- Emits a .deb, a portable tarball that now INCLUDES /usr/bin/stepforge (the
old tarball omitted it), and a sha256 sums file.
Launcher (packaging/linux/common/launcher.sh):
- Runs sandboxed; prefers the user-namespace sandbox, accepts a root-owned
setuid helper, and otherwise refuses to launch with an actionable message.
--no-sandbox requires an explicit STEPFORGE_ALLOW_NO_SANDBOX opt-in. Never
installs anything at runtime.
Setup (separate build vs runtime, apt only):
- scripts/linux/apt/install-runtime-deps.sh (Chromium/Electron libs, X11
tools, portal/PipeWire) and install-build-deps.sh (dpkg-dev, fakeroot,
xvfb). Runtime script installs no build tools.
Assets: original StepForge icon — packaging/assets/stepforge.svg plus a
generator (scripts/make-icons.js) that renders the PNG set with the repo's own
rasterizer/PNG writer (no third-party art). npm run icons regenerates them.
Wiring: package.json gains package:linux:deb / package:linux:rpm / icons;
build-release.sh uses the production builder and requires node_modules; README
points at the apt/dnf guides.
Tests: tests/unit/packaging-linux.test.js (structural: files present in their
separate locations, old script gone, valid desktop entry, templated arch +
runtime Depends, launcher gates --no-sandbox, builder requires node_modules
and guards dev-dep leaks, apt build/runtime dep separation, original icon set
generates a valid PNG) runs in the normal suite;
tests/integration/linux/package-deb.test.sh builds a real .deb and asserts the
right files present and the dev tree / build tooling / app docs absent
(honest skip only when dpkg-deb/node_modules are genuinely missing).
Verified locally: 276 unit tests pass; the integration test builds and
validates stepforge_0.3.2_amd64.deb; build-release E2E passes with the new
production package.
Co-Authored-By: Claude Fable 5 <[email protected]>
Phase 0 of the improvement plan (ai_prompts/prompt4.md): make the baseline
reproducible and stop the test runner from masking real failures.
- Pin Node >= 22.12 (engines + .nvmrc + engine-strict); every entry point
fails fast with clear guidance instead of dying late with ERR_REQUIRE_ESM
inside the packaging dependency graph.
- electron-launcher.js is diagnostics-only: all runtime npm install/rebuild/
repair paths are removed. npm ci on the pinned toolchain is the only
supported install path (README + GETTING_STARTED updated).
- Refuse to silently launch unsandboxed on Linux: --no-sandbox now requires
an explicit STEPFORGE_ALLOW_NO_SANDBOX/ELECTRON_DISABLE_SANDBOX opt-in and
is otherwise a hard error with actionable fixes; user-namespace sandboxing
is detected and preferred.
- Click-capture E2E no longer converts startup crashes into "SKIPPED": the
only allowed skip is the upfront absence of a display server. A missing
shared library or crash now fails with the startup log. Same guard added
to the startup smoke check.
- GitHub CI: run on pull_request, pin Node from .nvmrc, drop the macOS matrix
entry (not a support target), and audit production and full dependency
trees as separate signals. Gitea CI: pull_request trigger + pinned Node.
- Refresh package-lock on Node 22/npm 10 and remediate the form-data and
undici advisories (npm audit: 0 vulnerabilities, prod and full tree).
- Stop tracking generated machine-specific build reports
(build/build_report.md, build/artifacts_manifest.json).
Verified: 203 unit tests pass; repo-structure, startup-smoke, unit-workflows,
sample-artifacts, and build-release checks pass locally with a real Electron
launch. The click self-test now truthfully reports the pre-existing Linux
arm/debounce capture failures (also red on Gitea CI main run 177) instead of
hiding behind SKIPPED; that defect is scheduled for the capture-fix PR.
Co-Authored-By: Claude Fable 5 <[email protected]>
- examples/: sample guide store, .sfgz share archive, and exported
output in all nine formats (JSON/MD/HTML×2/PDF/GIF/images/DOCX/PPTX)
- build_report.md now records packaging tool availability, fallback
policy, offline guarantee, and verification entrypoints
- artifacts_manifest.json with sha256 checksums for packages + samples
- CHANGELOG entries for the welcome screen and renderer fixes
Co-Authored-By: Claude Fable 5 <[email protected]>