Skip to content

Upgrade dependencies: 79 advisories down to 24, no criticals - #719

Merged
micahflee merged 21 commits into
mainfrom
deps-upgrade-2026-09
Sep 21, 2026
Merged

micahflee merged 21 commits into
mainfrom
deps-upgrade-2026-09

Conversation

@micahflee

@micahflee micahflee commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Brings the dependency tree up to date, takes Electron off an end-of-support line, and moves the archive site off a build tool that stopped shipping releases.

Before After
Advisories 79 24
Critical / High / Moderate / Low 3 / 44 / 27 / 5 0 / 24 / 0 / 0
Packages 1821 1219
Electron 41.7.1 (EOL) 44.4.3
better-sqlite3 12.10.0 13.0.3

All 24 remaining advisories are one package: extract-zip, which is vulnerable at every published version and reaches us through @electron/packager. Build-time only, and there is nothing to upgrade to.

Commits, in the order they should be read

Each is independently green, so a bad one can be reverted alone.

  1. Take the last Electron and better-sqlite3 of their lines — minor bumps, plus the allowScripts re-approval they invalidate.
  2. Take the Electron tooling minors — @electron/osx-sign, @electron/rebuild.
  3. Take the ecosystem cluster minors — vitest, vite, eslint, vue, @atproto, leaves. Includes a Prettier reformat of three files.
  4. Let npm audit fix update the transitive pins — lockfile only, 16 advisories.
  5. Override the dependencies upstream will not patch — clears all three criticals.
  6. Move better-sqlite3 onto the N-API build — 13.0.3.
  7. Move to Electron 44 — three majors.
  8. Move x-archive from Vue CLI to Vite — removes 464 packages.
  9. Write down how to upgrade this repo's dependencies — the skill, compiled from this run.
  10. Reconcile allowScripts with what the tree installs — prunes two dead pins the majors left behind.
  11. Drop the last git dependency from the tree — unblocks clean installs on npm 12.
  12. Build releases on Node 24 LTS and the npm developers run — Dockerfile and CI.
  13. Drop a Vite option that does nothing — removes a warning on every x-archive build.
  14. Load the Vite configs as ESM — clears the Vite 8.3 configLoader warnings and a rolldown deprecation.
  15. Inject the archive.js tag instead of shipping it in index.html — clears the last build warning.

Warnings from the Vite bump

Vite 8.0.16 → 8.3.0 made npm start noisy. Confirmed against the published tarballs rather than assumed: the configLoader warning string is present in 8.3.0 and absent from 8.0.16, and the inlineDynamicImports deprecation comes from rolldown 1.2.x, which 8.3.0 pulls in place of 1.0.3.

The four Vite configs were ESM loaded as CommonJS, which Vite warns will break when configLoader: 'native' becomes the default. They are now .mts, which pulled in the three things native ESM always wants: an import attribute on the package.json import, a full specifier on the relative import, and import.meta.dirname for __dirname. forge.config.ts stays CommonJS.

npm start now launches with no Vite warnings at all — only Node's pre-existing punycode deprecation, which predates this branch.

Things worth a reviewer's attention

allowScripts pins install-script approval to an exact name@version. Every bump of Electron or better-sqlite3 silently invalidates its entry, the script never runs, and the Electron binary disappears — which fails the entire suite at once, since the tests run inside that binary. Approving a script does not re-run it either; that needs npm rebuild <pkg>.

npm audit fix did more than every direct upgrade combined (16 advisories against 5), because nearly all of them sit in transitive packages. It needs --allow-git=all, since Forge resolves @electron/node-gyp from a git URL. It was never run with --force: npm's proposed "fix" there is to take @electron-forge/cli from 7.11.2 back to 7.6.1, and Vue CLI back to 3.12.1.

"fixAvailable": false does not mean no patched release exists — only that npm cannot reach one by bumping a direct dependency. tar, tmp and image-size all had safe versions on the registry. The interesting one is mhtml2html, which pins jsdom ^15.1.1 and dragged in request, form-data, qs and tough-cookie — two of the three criticals. archive.ts passes its own parseDOM built from the root jsdom, so the bundled copy was never doing any work.

better-sqlite3 went first on purpose. v13 is built on N-API and ships portable prebuilds, so it survived the ABI jump from 145 to 149 with no rebuild. That also retires the glibc question: the prebuild needs glibc 2.34, under node:22-bookworm's 2.36. The 2.41 figure in the release notes applies to the old per-ABI assets.

Electron 44 needed one override. node-abi only knew Electron through 43, so electron-rebuild failed outright — and postinstall runs it and exits non-zero.

The x-archive migration fixed a bug that would have shipped. Archives are read by double-clicking index.html, and browsers refuse to fetch ES modules over file://. Vite tags its entry as a module whatever the output format, so vite.config.ts rewrites it to a classic script — and then it needs defer, because a classic script in <head> runs before #app is parsed and mounts onto nothing. Neither failure is visible to npm test or to the build; both were caught by loading the built archive over file:// with seeded window.archiveData and asserting on the rendered title, body text, computed layout and fonts.

Verification

Green after every commit: npm test (119 files, 1499 tests), npm run lint, npm run build --workspace=x-archive, a better-sqlite3 smoke test under the Electron binary, and npx electron-forge package.

Verified in Docker on the real release path — Node 24 LTS, npm 12, glibc 2.36 — producing all three Linux distributables:

Artifact Size
out/make/zip/linux/x64/ 170 MB
out/make/rpm/x64/ 130 MB
out/make/deb/x64/ 116 MB

Still worth a look before release: Electron 44 drops macOS 12, and the macOS and Windows makers are unexercised here.

A clean install was broken on npm 12

Worth calling out separately, because it predates this branch and is invisible until you clone fresh.

Forge pins @electron/rebuild@3.7.2, which resolves node-gyp from a git URL. npm from 12 refuses git fetches by default, so npm install fails with EALLOWGIT. It appears to work for anyone who already has the package unpacked in node_modules, which is why nobody hit it.

@electron/rebuild@4.2.0 takes node-gyp from the registry, so an override removes the git dependency rather than reopening git fetching to work around one transitive. It drops 102 packages, dedupes the two copies Forge and the root each carried, and supersedes the node-abi override added for Electron 44 — two overrides become one.

Release toolchain

The image was Node 22 with npm 10, two runtime lines behind and a major behind the npm developers run. The npm gap was not cosmetic: npm 12 records a libc field on optional platform packages that npm 10 silently strips, so every release build quietly rewrote package-lock.json and every developer rewrote it back.

Bookworm stays on purpose. postinstall rebuilds better-sqlite3 from source against the image's glibc, so the base image sets the floor for every Linux user — bookworm is 2.36, trixie would make it 2.41. Node 24 LTS is available on bookworm, so the runtime moves and the floor does not.

micahflee and others added 17 commits September 21, 2026 10:28
Electron 41.7.1 -> 41.10.7 clears GHSA-r4w5-6pfg-jxp5 and
GHSA-9f4c-93c8-jc8g; better-sqlite3 12.10.0 -> 12.11.1 is the
highest 12.x actually published to npm (12.11.2 and 12.12.0 exist
as GitHub tags only).

npm pins allowScripts to an exact name@version, so both bumps
invalidated their entry and the install scripts were blocked. The
Electron binary silently disappeared, which breaks every test,
since the suite runs inside Electron. Re-approved both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@electron/osx-sign 2.4.0 -> 2.7.0 and @electron/rebuild 4.0.4 ->
4.2.0, both additive releases.

@electron-forge/* is already at its latest published version, so
there is nothing to take there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vitest, vite, eslint/typescript-eslint, vue and @atproto each move
as a unit, plus the independent leaves.

Two snags worth recording. npm's incremental resolver deadlocked on
the typescript-eslint cluster and only cleared after uninstalling
and reinstalling the four packages together. npm outdated reported
typescript-eslint's latest as 8.70.0 when the registry had 8.70.1,
and @typescript-eslint/eslint-plugin peer-requires a matching
parser, so the stale number was itself the deadlock.

Prettier 3.9.8 reformats three files. No behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolves 16 advisories, against 5 for every direct upgrade
combined, because nearly all of them sit in transitive
dependencies that a direct bump never reaches. Lockfile only;
package.json is untouched.

Needs --allow-git=all: @electron-forge/cli pulls @electron/rebuild
3.7.2, which resolves node-gyp from a git URL, and npm now refuses
git fetches by default.

Never --force. npm offers to "fix" tar, tmp and extract-zip by
downgrading @electron-forge/cli from 7.11.2 to 7.6.1, and the Vue
CLI advisories by taking @vue/cli-plugin-typescript back to 3.12.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears all three criticals and both remaining lows.

npm reports fixAvailable: false for these, which means it cannot
reach them by upgrading a direct dependency, not that no patched
release exists. tar, tmp and image-size all had safe versions on
the registry.

mhtml2html is the interesting one. It pins jsdom ^15.1.1, which
drags in request, form-data, qs and tough-cookie, and that chain
held two of the three criticals. archive.ts passes its own
parseDOM built from the root jsdom, so the bundled copy was never
doing any work. Overriding it to ^29.1.1 drops the chain; verified
by converting an MHTML fixture end to end.

An override only re-resolves a subtree npm has not already pinned,
so mhtml2html needed an uninstall and reinstall to take effect.

uuid is forced to ^11.1.1 for http-mitm-proxy and sockjs.
http-mitm-proxy imports only the named v4 export, which v11 keeps.

extract-zip is left alone: every published version is vulnerable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12.11.1 -> 13.0.3. Version 13 is the first release built on
node-addon-api, so it publishes portable prebuilds instead of
compiling per ABI, and it was the only native module in the tree.
electron-rebuild now has nothing to build, and the binary survives
an Electron major on its own.

Types follow to @types/better-sqlite3 9.6.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
41.10.7 -> 44.4.3, crossing three majors off an end-of-support
line. ABI 145 -> 149, Chromium 152, Node 24.21.

Cyd is barely exposed to the breaking changes. It does not touch
the clipboard, login item settings or subframe workers.
clearStorageData is called with no arguments, so losing
options.quotas costs nothing, and showHiddenFiles survives as a
type union member. Forge builds for the host arch and CI covers
only x64 and arm64, so dropping ia32 and armv7l changes nothing
here. macOS 12 users are dropped, which is a support decision
rather than a code one.

better-sqlite3 needed no rebuild across the ABI jump, which is why
it moved to 13 first.

node-abi is overridden to ^4.35.0: the version @electron/rebuild
pins only knows Electron up to 43, and electron-rebuild fails
outright on 44 without it.

Verified: tests, lint, the renderer vite build, the x-archive
build, and electron-forge package. The makers and the
scripts/clean.mjs path are untested here, as clean.mjs needs an
interactive sudo to chown chrome-sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vue CLI stopped at 5.0.9 and will not be patched again, so its
webpack toolchain was the last large source of advisories and the
only one npm offered to "fix" by downgrading to 3.12.1. Dropping it
removes 464 packages and every remaining moderate.

The archive is opened by double-clicking index.html, and browsers
refuse to fetch an ES module over file://. Vite tags its entry as a
module regardless of the output format, so a small plugin rewrites
the tag back to a classic deferred script and drops crossorigin
from the entry and the stylesheet. defer matters: the tag sits in
head, and a bare classic script runs before #app is parsed and
mounts onto nothing. Verified by loading the built archive over
file:// with seeded archive data, checking the document title, a
rendered tweet, Bootstrap layout and Font Awesome fonts.

index.html moves to the project root, which empties public/, so
build-archive-sites.sh no longer has a stale archive.js to delete.

vue-class-component was unused and is dropped. The components
imported defineProps from vue, which collides with the compiler
macro; Vue CLI never type-checked strictly enough to notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiled from the 79 -> 24 pass, so every claim in it is something
that actually bit during that run rather than general npm advice.

The load-bearing parts are the ones no amount of reading the
manifest would tell you: allowScripts pins install-script approval
to an exact name@version, so any bump of Electron or better-sqlite3
silently blocks the script and the test suite loses the binary it
runs inside; npm audit fix moves the transitive pins where most of
the advisories actually live; and fixAvailable: false means npm
cannot reach a fix by bumping a direct dependency, not that no
patched release exists.

The major-upgrade playbook is a separate file, since only some runs
reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The majors left two dead pins behind: electron@41.10.7, which no
longer runs a script at all since v42 downloads on first run, and
better-sqlite3@12.11.1, superseded by 13.0.3. A stale pin is
invisible until someone's install blocks, which is what
make-local surfaced.

better-sqlite3 13 declares no install script and the lockfile sets
no hasInstallScript for it, but npm still runs an implicit
node-gyp rebuild for anything shipping a binding.gyp, so it keeps
an entry. It only matters where no prebuild exists and the source
build is the fallback.

Adds the reconciliation pass to the skill as its own phase, since
approving as you go is what creates the mess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Forge pins @electron/rebuild 3.7.2, which resolves node-gyp from a
git URL. npm from 12 refuses git fetches by default, so a clean
install of this repo failed outright with EALLOWGIT. It only
appeared to work here because the package was already unpacked in
node_modules; a fresh clone on npm 12 could not install at all.

@electron/rebuild 4.2.0 takes node-gyp from the registry, so
overriding it removes the git dependency rather than reopening git
fetching to work around one transitive. It also dedupes the two
copies Forge and the root were each carrying, and drops 102
packages.

That supersedes the node-abi override from the Electron 44 commit:
4.2.0 depends on node-abi ^4.2.0, which resolves to a version that
already knows Electron 44 and 45. Two overrides become one.

Verified electron-rebuild still builds better-sqlite3 and Forge
still drives it, by making all three Linux distributables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release image was Node 22 with npm 10, two lines behind the
runtime and a major behind the npm on a developer's machine.

The npm gap was not cosmetic. npm 12 records a libc field on
optional platform packages that npm 10 does not understand and
silently strips, so every release build quietly rewrote
package-lock.json and every developer rewrote it back. Matching
versions also means the allowScripts allowlist is exercised where
releases are built, instead of only failing on a laptop.

Bookworm stays. The postinstall rebuilds better-sqlite3 from source
against the image's glibc, so the base image sets the floor for
every Linux user: bookworm is 2.36, trixie would make it 2.41.
Node 24 LTS is available on bookworm, so the runtime moves and the
floor does not.

Verified by building the deb, rpm and zip in the container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
inlineDynamicImports is ignored when codeSplitting is off, which
cssCodeSplit: false already implies, and Vite says so on every
build. Output hash is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vite 8.3 warns that a config written in ESM but loaded as CommonJS
will break when configLoader: 'native' becomes the default. The
root tsconfig sets module: CommonJS, so all four configs were in
that position.

Renaming them to .mts makes them genuinely ESM, which then wants
what native ESM always wants: an import attribute on the
package.json import, full specifiers on relative imports, and
import.meta.dirname in place of __dirname. forge.config.ts stays
CommonJS and keeps using __dirname.

Also takes the rolldown deprecation that arrived with the same
bump: inlineDynamicImports is now spelled codeSplitting: false.

Verified by packaging and by starting the app; both build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
archive.js is written into the archive folder by Cyd, long after
this build runs, so a tag for it in index.html points at a file
Vite cannot resolve and Vite says so on every build.

Moving the tag into the plugin that already handles the other
file:// concerns keeps the emitted HTML byte-identical while
removing the warning, and puts everything file:// needs in one
place. It stays undeferred so it still sets window.archiveData
before the entry script runs.

Re-verified by loading the built archive over file:// with seeded
data: title, tweet body, Bootstrap layout and fonts all intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding an X account on Electron 44 failed at login with "ct0 is
null", then crashed the getProgressInfo handler.

getCookie read a map that handleCookieTracking filled by watching
the Cookie header go past on outgoing requests. Chromium stopped
exposing that header to webRequest in Electron 44, so the map was
always empty. Probed both runtimes with the same listener: the
header is visible on 41.10.7 with Chromium 146 and null on 44.4.3
with Chromium 152.

Asking session.cookies.get is the fix, and it also removes the
ordering dependency the old approach carried: the map only held a
cookie after some request had already carried it, so the very
first lookup on a newly added account was never going to succeed.
That leaves X's tracking map with no readers, so it goes, and the
base class keeps a no-op default for Facebook, which still
harvests headers and is still affected.

getAccountDataPath gains a username guard. Login failing this way
leaves an account with no username, and path.join threw on it,
turning a handled error into a crash that hid the real one. initDB
already gives up on an empty path.

The suite could not have caught this: the mock supplies a Cookie
header, so the tests described a browser that no longer exists.
The new tests seed the session's cookie jar and never simulate a
request, and they fail against the old implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Missed in the earlier pass, so every test run still printed the
configLoader warning. Its coverage exclude also still named
vite.*.config.ts, which stopped matching when those were renamed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@micahflee

Copy link
Copy Markdown
Member Author

Facebook's side of the cookie regression is tracked separately in #720. It turned out not to need a port: nothing reads Facebook's cookie map, so there is no user-visible breakage there, only dead code that also stopped working. Left out of this PR deliberately.

(This was written by an LLM.)

micahflee and others added 4 commits September 21, 2026 13:23
sleep is async, and four call sites called it without await, so
none of them waited. The most consequential is in loadURL, whose
own comment says what it is for: "Sleep 2 seconds after loading
each URL, to make everything more stable." That settle never
happened, and loadURL returned roughly two seconds earlier than
every caller has been written to expect.

The other three are retry backoffs -- two in loadURL's catch, one
in graphqlGetViewerUser -- which meant a failed load retried
immediately instead of after a second, and burned all three tries
in the time one was supposed to take.

The test gates the settle specifically, since that is the one
with a behavioural contract. It flushes a macrotask rather than a
microtask before asserting: with only a microtask the promise
chain has not settled yet and the assertion passes whether or not
the sleep is awaited, which is how the first version of this test
passed against the unfixed code.

Found while investigating the archive SIGSEGV in #721. It is not
the cause of that crash -- restoring the settle leaves a
byte-identical stack -- so this stands on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renaming the four Vite configs and vitest.config to .mts left six
entries naming files that no longer exist: three ignores in
eslint.config.mjs and three excludes in src/renderer/tsconfig.json.

Deleted rather than repointed at .mts, because repointing would
imply they do something. They do not, and neither did the .ts
versions by the end. eslint only lints what a files pattern
matches, and the TypeScript block matches **/*.ts, which does not
match .mts -- eslint processes zero .mts files out of the 580 it
walks. TypeScript is the same: with all three excludes removed,
--listFiles still pulls in no config file, because an include of
./**/*.ts does not match .mts either.

So the four .mts configs are linted by nothing and typechecked by
nothing. That was already true before this commit and is left
alone here; it is a gap worth closing separately, not by keeping
dead entries around to suggest otherwise. Prettier does still
cover them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The script wanted the private-use callback URL, but that is not
what a person has in front of them. The authorization server
sends the browser to its own redirect endpoint, which carries the
callback in redirect_uri and the OAuth response -- iss, state,
code -- beside it. That endpoint URL is what sits in the address
bar when the handoff fails.

Assembling the callback from it by hand invites joining the
response to the path with an ampersand instead of a question
mark. There is no query string then, the whole tail parses as one
long pathname, it matches no route, and the app reports "Invalid
Cyd URL" with no hint as to which part is wrong.

So take the redirect URL and do what the browser would have done.
A URL carrying no redirect_uri is passed through untouched, which
covers a callback that was already assembled correctly, and so is
a string that is not a URL at all, so a typo still reaches the
app's own error rather than throwing out of the script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
openCydURL says only the path is ever logged or shown, because
the query string can carry an OAuth authorization code. It did
that by clearing url.search, which does nothing when there is no
query string to clear.

Joining the parameters to the path with "&" instead of "?" is
exactly that case: the whole tail parses as pathname, so clearing
search removes nothing and the code was written to the log and
displayed in a dialog in full. Cutting the pathname at the first
"&" restores the invariant the comment claims.

The same cut says what went wrong. Landing on the error with
parameters buried in the path means the route was right and only
the separator was not, so the dialog now names the character to
fix. It stays quiet for a genuinely unknown path, where there is
nothing specific to say.

Routing is left strict on both routes. Matching the truncated
path would accept a mangled callback and then resolve no flow
from its empty query, reporting that the sign-in link had already
been used -- which is worse than reporting the real problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@micahflee
micahflee merged commit e0d9e10 into main Sep 21, 2026
1 check passed
@micahflee
micahflee deleted the deps-upgrade-2026-09 branch September 21, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant