A four-step dependency modernisation, ordered so that each step could only break
one thing and the next step started from a green baseline. The toolchain moves
forward substantially while the app itself stays put — the only behavioural
changes are two security fixes found alongside it: offboarding reset passwords
were coming from Math.random(), and log records could be forged through the
bulk-job path.
Changed
-
Electron 40 → 43 (Chromium 150, Node 24.17, V8 15). Electron 40 had reached
end-of-support. Startup is faster: the main process now boots from an embedded
Node startup snapshot, and preload scripts are cached as compiled V8 bytecode. -
Vite 5 → 8, the rolldown-vite merge — Rolldown and Oxc replace Rollup and
esbuild. Moved with@vitejs/plugin-react4 → 6,vite-plugin-electron
0.28 → 1.1 andvite-plugin-electron-renderer0.14 → 1.0, because
@vitejs/plugin-react@6pinsvite: ^8and no release bridges the two.
The main-process externals move frombuild.rollupOptionsto
build.rolldownOptions. -
better-sqlite3 12 → 13, which switches to N-API. This is the change with
the widest reach: v13 ships a prebuilt binary for every target and its loader
reads them directly, so one file now serves both Node and Electron. The
per-ABI compile-and-swap machinery the project had carried since the SQLite
migration is gone (see Removed), andnpm cidrops from minutes to seconds. -
i18next 23 → 26 and react-i18next 14 → 17 (they share a hard peer
range). The<Trans>serialisation fix in react-i18next 17 is a no-op here —
every locale string uses named placeholders — and there is now a test that
keeps it that way. -
ESLint 8 → 9 with the config format moved from
.eslintrc.cjsto
eslint.config.js, plus@typescript-eslint7 → 8,
eslint-plugin-react-hooks4 → 7 andeslint-plugin-react-refresh0.4 → 0.5.
The linted file set is unchanged and no rule was disabled to get there.
react-hooks 7'srecommendedpreset now enables 14 React Compiler rules by
default; the two rules this project actually used are configured explicitly
instead, so linting behaviour is identical. -
jsdom 28 → 29, @testing-library/jest-dom 6 → 7, lucide-react 0.575 →
1.27 and react-dropzone 15 → 19. No source changes were needed for the
icons — 1.x keeps the old names as aliases. react-dropzone 19 does change one
behaviour: dropping more files thanmaxFilesused to reject all of them and
now accepts the ones within the limit. -
Minimum Node.js is now 22.12 (was 20). Forced by Electron 43,
better-sqlite3 13 and jest-dom 7;.npmrcsetsengine-strict=true, so a
lower version fails at install rather than at runtime. -
Installer size grew with Electron 43 and the bundled prebuilds — macOS
126 MB → 135 MB, Windows 93 MB → 96 MB. The four Linux prebuilds are excluded
from packaging since GoWorks targets macOS and Windows. -
README.mdandREADME.tr.mdnow state the current toolchain — Electron 43,
Vite 8 on Rolldown, better-sqlite3 13 on N-API — in both languages. Every
version claim was checked againstpackage.jsonrather than written from
memory.
Added
-
scripts/check-lucide-icons.mjs(npm run icons:check, and part of
postlint): asserts every lucide icon imported undersrc/exists in the
installed package. A missing named export from an ESM barrel is not a build
error — it resolves toundefinedand only throws when that screen renders —
so an icon rename could otherwise pass lint, type-check, tests and build, then
break one page. -
A
<Trans>regression test covering the five shapes the app uses, including
thedefaults-without-i18nKeyform. The component had seven call sites and
no test. -
scripts/check-oss-attribution.mjs(npm run oss:check, and part of
postlint): asserts that every runtime dependency appears in Settings →
About's open-source list and that each credited license matches what is
actually installed. Neither kind of drift is visible to lint, types or tests —
the list is a plain array that always renders.
Fixed
-
Two shipped libraries were missing from the open-source attribution list
in Settings → About:@noble/hashes(the Argon2id implementation behind the
master-password vault) andclsx. Both are credited now, and the check above
keeps the list from drifting again. -
A
useRefcall with no initial value inJobHistory.tsx— the only
argument-less one of the eight call sites in the codebase. React 19 drops that
overload, so the call stops type-checking (TS2554) against React 19's types.
The fix is safe on the current major rather than a forward-port that breaks the
present: passingundefinedexplicitly resolves to React 18's own
useRef<T = undefined>(initialValue?: undefined)overload, so the ref keeps
its existing type and nothing about today's build changes.
Security
-
Offboarding generated the departing user's reset password with
Math.random(). V8 implements it with xorshift128+, whose internal state is
recoverable from a couple of observed outputs, so every password produced
after that point was predictable — and this password is set on a live Google
Workspace account.changePasswordAtNextLoginnarrows the window without
closing it: the first party to sign in sets the password and owns the account.
Because the offboarding steps are individually toggleable, an operator who
turns off "suspend" leaves this reset as the only line of defence. Passwords
now come fromcrypto.getRandomValues()via a new
src/utils/generatePassword.ts, with rejection sampling so the alphabet stays
uniform (256 % 63 == 4, so a plain modulo would favour the first four
symbols). The alphabet (63 ambiguity-free characters) and length (24, ~143
bits) are unchanged. Ano-restricted-propertiesESLint rule is the
regression guard — lint runs with--max-warnings 0, so it is a CI gate — and
the two remaining non-secretMath.random()uses carry inline disables
stating why. -
Log records could be forged by anything that reached the logger.
write()
appended formatted arguments to the log file with no sanitisation: no newline
handling, no control-character escaping, no length cap. A newline inside any
logged string produced a second, fully-formed[timestamp] [LEVEL] …record,
which is enough to make the log worthless as evidence after an incident. That
path is reachable:validateJobPayloadis asymmetric —BULK_SUSPENDand
BULK_DELETEgo throughrequireEmailList, whoseEMAIL_REexcludes
whitespace, butBULK_GROUP_ADDandBULK_SIGNATURE_PUSHgetrequireArray
alone, so raw CSV cell contents reach the log via the bulk workers and
retry.ts. C0 control characters and DEL are now escaped per argument, and
each argument is capped at 8 KB with an explicit truncation marker. The
console stream is deliberately untouched — it is ephemeral developer output
where readable multi-line stack traces are worth more than the
one-record-per-line invariant. Addslogger.test.ts, the module's first test.
Validating bulk-job row contents is left for later on purpose: the sanitiser
closes the logging consequence, and content validation is a separate concern
with its own risk of rejecting legitimate data. -
A stale lockfile entry was still pinning a vulnerable
brace-expansion.
Closes Dependabot alert #8 (GHSA-3jxr-9vmj-r5cp / CVE-2026-13149, high,
development scope) — exponential-time expansion inexpand(), where roughly
90 bytes of input can stall a single-threaded consumer indefinitely. Nine of
the ten copies in the tree were already patched by the upgrades above; the
exception was a lockfile entry pinning 1.1.14 undereslint-plugin-jsx-a11y's
nestedminimatch@3.1.5, which declaresbrace-expansion: ^1.1.7and had
therefore accepted the patched release all along. Nothing was holding it back
but the lockfile, so this is a lockfile-only change.
Removed
-
The better-sqlite3 dual-ABI apparatus, now that one binary serves both
runtimes:scripts/sqlite-binary.mjs,scripts/check-native-abi.mjs, the
predev/pretest/posttest/postinstall/test:prepare/rebuild
/postbuildhooks, and CI's native-binary preparation step. Running tests is
now justnpx vitest run.These had stopped protecting anything: v13's loader ignores the path they
operated on, so they reported success while doing nothing. -
The magic-byte architecture sniff in the startup check. Per-target prebuilds
are selected by platform and architecture, so a foreign binary is unreachable,
and an N-API binary is not tied to an ABI version. The load probe remains — a
missing or corrupt prebuild is the only failure left, and only the probe sees
it. -
@types/react-dropzone, a deprecated stub; react-dropzone has shipped its own
types since v11.