Skip to content

Releases: umutcankurt/goworks

GoWorks v0.8.1

Choose a tag to compare

@umutcankurt umutcankurt released this 05 Aug 11:03

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-react 4 → 6, vite-plugin-electron
    0.28 → 1.1 and vite-plugin-electron-renderer 0.14 → 1.0, because
    @vitejs/plugin-react@6 pins vite: ^8 and no release bridges the two.
    The main-process externals move from build.rollupOptions to
    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), and npm ci drops 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.cjs to
    eslint.config.js, plus @typescript-eslint 7 → 8,
    eslint-plugin-react-hooks 4 → 7 and eslint-plugin-react-refresh 0.4 → 0.5.
    The linted file set is unchanged and no rule was disabled to get there.
    react-hooks 7's recommended preset 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 than maxFiles used 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; .npmrc sets engine-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.md and README.tr.md now state the current toolchain — Electron 43,
    Vite 8 on Rolldown, better-sqlite3 13 on N-API — in both languages. Every
    version claim was checked against package.json rather than written from
    memory.

Added

  • scripts/check-lucide-icons.mjs (npm run icons:check, and part of
    postlint): asserts every lucide icon imported under src/ exists in the
    installed package. A missing named export from an ESM barrel is not a build
    error — it resolves to undefined and 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
    the defaults-without-i18nKey form. 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) and clsx. Both are credited now, and the check above
    keeps the list from drifting again.

  • A useRef call with no initial value in JobHistory.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: passing undefined explicitly 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. changePasswordAtNextLogin narrows 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 from crypto.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. A no-restricted-properties ESLint rule is the
    regression guard — lint runs with --max-warnings 0, so it is a CI gate — and
    the two remaining non-secret Math.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: validateJobPayload is asymmetric — BULK_SUSPEND and
    BULK_DELETE go through requireEmailList, whose EMAIL_RE excludes
    whitespace, but BULK_GROUP_ADD and BULK_SIGNATURE_PUSH get requireArray
    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. Adds logger.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 in expand(), 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 under eslint-plugin-jsx-a11y's
    nested minimatch@3.1.5, which declares brace-expansion: ^1.1.7 and 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
    / postbuild hooks, and CI's native-binary preparation step. Running tests is
    now just npx 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.

GoWorks v0.8.0

Choose a tag to compare

@umutcankurt umutcankurt released this 20 Jul 11:01

Closes a static-analysis pass over the whole codebase, plus two bugs found while
verifying the fixes — one of which had been silently destroying the Google session
on every app close since long before the security work began.

Fixed

  • Quitting the app deleted the stored Google session. before-quit and, on macOS,
    window-all-closed both called logout(), which deletes the refresh token from the
    vault. Every close therefore forced a full browser OAuth round on the next launch,
    defeating the point of storing the token encrypted at rest. A full logout now happens
    only where it was always documented to: an explicit user logout or a factory reset.

  • Unlocking the vault dropped you back at the Google sign-in screen. The renderer
    kept the only copy of the signed-in identity in localStorage, and cleared it
    whenever auth:check reported authenticated: false. A vault lock reports exactly
    that — locking drops the in-memory OAuth credentials on purpose while the grant stays
    in the vault — so the lock erased the identity and the unlocked app found nothing to
    restore, landing on /login over a fully authenticated main process. The main process
    is now the authority: auth:check returns the profile, and the renderer's copy is a
    cache cleared only on a real logout. It still covers one gap — a silent restore fails
    open when Google's userinfo endpoint is unreachable, and the cached copy carries the
    identity through.

  • The signature preview did not match what Gmail received. SignaturePreview
    carried its own substitution engine that never sanitised and resolved conditional
    blocks in the opposite order from the real renderer. Most visibly, the manual
    signature editor substituted {{ad_soyad}} into a value while Save pushed the buffer
    through the raw path, which substitutes nothing. Preview now runs through the same
    main-process engine as the push, via a new templates:renderPreview IPC channel that
    mirrors both push modes explicitly.

Security

  • IPC errors no longer leak filesystem paths. 26 handlers under config:*,
    media:* and jobs:* returned error.message verbatim to the renderer, so a failed
    disk operation shipped its absolute path — ENOENT: … open '/Users/…/vault.enc'
    into a toast, in production as well as development. Beyond the disclosure this was a
    file-existence oracle: code running in a compromised renderer could call these
    channels and tell ENOENT from EACCES to probe the filesystem. They now log the
    full stack to the log file and return a generic message. Validation rules that the
    operator can actually act on (invalid domain, name too long, unsupported logo format)
    are preserved as UserFacingError, and an expired Google session now says so instead
    of "an unexpected error occurred".

  • A long manual lock now requires Google re-authorization. Locking with the lock
    button arms a 59-minute window; unlocking after it expires opens the vault with the
    master password but does not silently restore the Google session. This bounds how long
    a deliberately locked machine keeps working Google access if the master password is
    obtained. The idle auto-lock is exempt — its own default is 60 minutes, so a shorter
    window would expire the instant it fired — and closing the app does not arm it, since
    closing is not locking.

  • Preview media tokens are resolved in the main process from the template's own assets
    and applied last, so a value supplied by the renderer can no longer reach an <img>
    source in a rendered signature.

Changed

  • templates:renderPreview is new. templates:preview is unchanged and still renders a
    saved template by id.
  • Installers no longer carry third-party documentation. Beyond the size saving, this is
    what let the build's secret guard pass: dotenv's README demonstrates multiline env
    vars with a -----BEGIN RSA PRIVATE KEY----- block, and the guard cannot distinguish
    a documented example from a real leak — so the docs go, rather than the check.
  • Test coverage grew from 404 to 447. New suites cover the lock/unlock identity
    round-trip, the manual-lock window, the preview component's debounce and
    out-of-order-response handling, and the raw/template render distinction — each of them
    a bug that had already shipped once.

GoWorks v0.7.9

Choose a tag to compare

@umutcankurt umutcankurt released this 14 Jul 11:40

Installers removed — superseded by v0.8.0.
v0.8.0 closes a full static-analysis pass, including an IPC error path that leaked
filesystem paths to the renderer. Please use v0.8.0. The source for this release is
still available at the v0.7.9 tag.

Security

  • Installers could ship a build-time OAuth credential; they no longer can. Up to
    v0.7.8, a build could pack a stale compiled chunk from an older generation of
    auth-service that had the OAuth client ID and secret baked in as string literals.
    The mechanism: dotenv loaded .env into process.env at build time and the bundler
    folded those reads into literals — and because dist-electron/ was never cleaned
    between builds, such a chunk could outlive the source change that removed it and end
    up inside app.asar. The source tree never contained a secret; only the compiled
    artifact did. Two changes close this off:
    • prebuild now wipes dist/ and dist-electron/ before every build, so no artifact
      can outlive its generation.
    • A new gate, scripts/check-bundle-secrets.mjs, scans the compiled output for
      credential-shaped strings (OAuth secrets, client IDs, API keys, private-key blocks)
      and fails the build before electron-builder packs an asar. Standalone:
      npm run secrets:check.

Changed

  • The local database drops an orphaned googleApiKey row (migration v3 → v4). It came
    from an abandoned Google Picker design and was hand-written into some installs, but no
    code path ever read it — it is not part of the app config schema.
  • The README now states plainly that GoWorks was built with AI assistance, and asks you
    to run your own review before pointing it at a production tenant.

Verifying this build

Every installer below was produced by a build that cleans dist/ and dist-electron/
first and then refuses to package if the compiled output contains anything
credential-shaped. You can run the same check against your own build:

npm run build          # prebuild clean → vite → secret gate → electron-builder
npm run secrets:check  # the gate on its own

Install

  • macOSGoWorks-Mac-0.7.9-Installer.dmg (Apple silicon)
  • WindowsGoWorks-Windows-0.7.9-Setup.exe (x64)

The app is ad-hoc signed, so on first launch macOS will ask you to confirm; open it from
Finder with right-click → Open.