Living reference of every third-party dependency in PortOS, why it's kept, and what the current verdict is. Updated by /do:depfree runs.
Last audited: 2026-08-04 (scoped audit of the keyv/cacheable supply-chain compromise); prior follow-up 2026-07-14 (issue #2547), prior full audit 2026-04-28 (default mode), tables corrected 2026-07-01 during a docs audit.
Verdict: All dependencies justified. The 2026-08-04 audit replaced the entire eslint stack with @biomejs/biome, dropping 110 net client packages including the file-entry-cache → flat-cache → keyv chain named in the August 2026 Shai-Hulud npm compromise (PortOS held safe versions throughout — see the detailed finding below). The same pass closed a latent hole where ignore-scripts=true was only active for repo-root installs, not for any workspace install or CI. Since the last full audit: sax was removed (replaced with an owned parser, issue #1824), portos-ai-toolkit was vendored in-tree (server/lib/aiToolkit/), and monolithic googleapis was replaced with scoped @googleapis/* packages. The 2026-07-14 follow-up bumped kokoro-js to its latest patch 1.2.1 (still on maintenance watch — no publish since 2025-05) and aligned the dual pm2 pins (root + server both 7.0.4).
Each dependency is classified into one of three tiers:
- Tier 1 (ACCEPTABLE) — large, widely-audited, foundational libraries. Kept without question.
- Tier 2 (SUSPECT) — smaller libraries that may be replaceable. Audited for actual usage.
- Tier 3 (REMOVABLE) — clear candidates for owned-code replacement.
Before removing a Tier 3 candidate, run a transitive-dep check (npm ls <pkg>). If a kept package already pulls the same major version, the candidate is downgraded to KEEP (transitive) — direct removal saves no supply chain attack surface.
| Package | Tier | Verdict | Where Used | Notes |
|---|---|---|---|---|
| Root devDeps | ||||
pm2 |
1 | KEEP | top-level scripts | Process manager, foundational. Pinned 7.0.4 (aligned with server pin) |
| Server deps | ||||
@novnc/novnc |
1 | KEEP | PortDeck remote desktop viewer | Mature RFB/VNC protocol implementation; replacing it would require owning multiple security types and framebuffer encodings |
@googleapis/calendar |
1 | KEEP | Calendar integration | Scoped official Google SDK (replaced monolithic googleapis) |
@googleapis/gmail |
1 | KEEP | Messages/Gmail integration | Scoped official Google SDK |
chokidar |
1 | KEEP | server file watching | Mature cross-platform file-system watcher used by server services |
express |
1 | KEEP | server/index.js + routes |
Framework |
google-auth-library |
1 | KEEP | Google OAuth | Pairs with @googleapis/* |
kokoro-js |
2 | KEEP | server/services/voice/tts-kokoro.js |
Only pure-JS in-process TTS; replacement = Python subprocess + pooling |
node-pty |
1 | KEEP | shell/terminal services | Native PTY binding (N-API) |
pdf-lib |
1 | KEEP | PDF generation/manipulation | |
pg |
1 | KEEP | Postgres access | Official pg driver |
pm2 |
1 | KEEP | app lifecycle | Process manager. Pinned 7.0.4 (aligned with root pin) |
sharp |
1 | KEEP | image processing | Native, widely-audited |
socket.io |
1 | KEEP | realtime | Foundational |
socket.io-client |
1 | KEEP | server-to-client | Paired with socket.io |
undici |
1 | KEEP | HTTP client | Node core team project |
ws |
1 | KEEP | remote desktop + browser WebSockets | Foundational WebSocket transport; also used transitively by Socket.IO |
zod |
1 | KEEP | input validation | Widely-audited |
| Server devDeps | ||||
vitest |
1 | KEEP | test runner | |
@vitest/coverage-v8 |
1 | KEEP | coverage | Paired with vitest |
| Client deps | ||||
@dnd-kit/core |
1 | KEEP | drag/drop | |
@dnd-kit/sortable |
1 | KEEP | drag/drop | |
@react-three/drei |
1 | KEEP | CyberCity 3D | Three.js helpers |
@react-three/fiber |
1 | KEEP | CyberCity 3D | React renderer for Three |
@scalar/api-reference-react |
1 | KEEP | Dev Tools → API Explorer | Interactive OpenAPI reference UI rendering |
@xterm/xterm |
1 | KEEP | browser terminal | |
@xterm/addon-fit |
1 | KEEP | xterm sizing | |
@xterm/addon-web-links |
1 | KEEP | xterm links | |
lucide-react |
1 | KEEP | icons | Widely-used |
react |
1 | KEEP | UI | |
react-dom |
1 | KEEP | UI | |
react-router |
1 | KEEP | routing | v8 dropped the react-router-dom alias package |
recharts |
1 | KEEP | charts | |
socket.io-client |
1 | KEEP | realtime client | |
three |
1 | KEEP | 3D | |
three-stdlib |
1 | KEEP | CyberCity 3D | Community-maintained Three.js utilities used by the client renderer |
| Client devDeps | ||||
@biomejs/biome |
1 | KEEP | linting | Replaced the whole eslint stack 2026-08-04; native binary, 0 regular deps + 8 platform optionals (1 installed) |
eslint |
— | REMOVED | linting | 2026-08-04 → @biomejs/biome. 53 packages were reachable only via eslint itself (incl. the file-entry-cache → flat-cache → keyv chain); 110 net once the plugin subtrees and orphaned typescript go too |
@eslint/js |
— | REMOVED | linting | Removed with eslint |
@eslint-react/eslint-plugin |
— | REMOVED | linting | Removed with eslint; its 8 enabled rules map to Biome rules + one GritQL plugin |
eslint-plugin-react-hooks |
— | REMOVED | linting | Removed with eslint; rules-of-hooks → Biome useHookAtTopLevel |
typescript |
— | REMOVED | (none) | 2026-08-04. Was only here for @eslint-react's peer dep; client has no tsconfig / .ts sources / tsc script |
@tailwindcss/postcss |
1 | KEEP | styling | |
tailwindcss |
1 | KEEP | styling | |
@vitejs/plugin-react |
1 | KEEP | build | |
vite |
1 | KEEP | build | |
vitest |
1 | KEEP | client test runner | jsdom environment |
jsdom |
1 | KEEP | test DOM | Paired with vitest |
@testing-library/jest-dom |
1 | KEEP | test matchers | |
@testing-library/dom |
1 | KEEP | DOM test utilities | Foundation for the client Testing Library stack |
@testing-library/react |
1 | KEEP | component tests | |
@testing-library/user-event |
1 | KEEP | interaction tests | |
rollup-plugin-visualizer |
2 | KEEP | bundle-size analysis | Dev-only, opt-in |
- Usage: 1 dynamic import in
server/services/voice/tts-kokoro.js(~80 LOC module). 3 call sites:KokoroTTS.from_pretrained(),tts.generate(text, {voice, speed}),audio.toWav(). - Maintenance: pinned at
1.2.1(latest; published 2025-05-03). Maintenance watch — no publish since ~May 2025, so the package is effectively stale even at latest. This is not disqualifying today (small, pure-JS, no CVEs), but re-evaluate on the trigger below. - Vulns: None (npm audit clean).
- Replacement complexity: Moderate (~50–80 LOC) but requires Python subprocess + JSON IPC + process pooling + lifecycle management. Operational overhead exceeds the supply-chain risk.
- Decision: KEEP (on maintenance watch). The only pure-JS in-process TTS option; Web Speech API is cloud-dependent and Piper requires CLI install.
- Re-audit trigger: the >12-months-stale trigger already fired and was actioned by this audit (bumped to latest, put on watch). Re-evaluate on any of: a CVE reported, the model-load path breaking against a newer Transformers.js, or the package still showing no upstream publish at the next dependency audit — then revisit and migrate (see escape hatch below).
- Piper escape hatch (if dropped later): Piper is already implemented as a peer backend —
server/services/voice/tts-piper.js(synthesizePiper(text, cfg, signal) → { wav, latencyMs }, pluslistPiperVoices), whichserver/services/voice/tts.jsalready dispatches to alongsidesynthesizeKokoro. Both backends share the same(text, cfg, signal) → { wav, latencyMs }contract, so droppingkokoro-jsis a delete, not a rewrite: remove thekokorobranch (and thetts-kokoro.jsmodule + itskokoro-jsdependency) from the dispatcher intts.jsand let Piper be the default engine. The tradeoff Piper carries — and the reason it isn't the default today — is a nativepiperbinary + ONNX voice download (vs.kokoro-js's npm-only, in-process install).
- Trigger: the August 2026 Shai-Hulud npm compromise hit
keyv,flat-cache, andfile-entry-cache(among ~430 packages). PortOS was never exposed — it heldkeyv@4.5.4,flat-cache@4.0.1,file-entry-cache@8.0.0, while the malicious releases were the next major in each line (6.0.0/6.1.24/11.1.6), unreachable from the^4/^4.0.0/^8.0.0ranges. npm has since unpublished all three, and no install hook ever existed in the tree. - Why they were here at all: not direct dependencies. A single chain under one root —
eslint → file-entry-cache → flat-cache → keyv— client workspace, devDependencies only.eslint@10.8.0(latest) hard-requiresfile-entry-cache@^8.0.0; no eslint release drops it. So the only way to shed them was to stop using eslint. - Semver headroom was already zero: the highest published
file-entry-cache@8.xis8.0.0,flat-cache@4.xis4.0.1,keyv@4.xis4.5.4. Anoverridespin would have been a no-op — there was nowhere to float. Removal was therefore a maintenance win, not a security fix. - Resolution: replaced eslint with
@biomejs/biome@2.5.7. Client lockfile 447 → 337 packages (−110; the eslint tree out, 9 Biome entries in, only 1 platform binary installed). All four eslint-chain packages now report 0 occurrences inclient/package-lock.json. typescriptwent with it. It was a client devDependency solely to satisfy@eslint-react/eslint-plugin's peer dep — the client has notsconfig, zero.ts/.tsxsources, notscscript, and nothing peer-depends on it. Removing it also retires the.github/dependabot.ymlignore rule that pinned it below TS7 (and issue #3351, which tracked waiting on typescript-eslint for TS7 support — now moot).- Rule parity was proven with fixtures, not inferred from a clean run — a linter with no rules also reports "0 problems". Every rule the old config enforced fires under Biome;
npm run lintcovers the same 1859 files with 0 problems.exhaustive-depswas alreadyoff(documented, deliberate), which is what made this a half-day swap instead of a risky one. - The
crypto.randomUUIDban survives as a GritQL plugin (client/lint-no-random-uuid.grit). This rule is load-bearing:crypto.randomUUIDis undefined on insecure origins, and PortOS is routinely reached over plain HTTP via Tailscale. It matches on the CST node kind rather than code snippets, so it catchescrypto.,globalThis.crypto.,window.,self., optional chaining, baretypeofreferences, and assignment targets — and, matching the old ESLint rule exactly, notcrypto['randomUUID']. Exemptions forsrc/lib/uuid.jsandsrc/**/*.test.{js,jsx}are expressed as negated globs, because a Biome override'spluginslist is additive:plugins: []does NOT disable an inherited plugin. Do not "simplify" that back. - Deliberately scoped: Biome's formatter and
assistare disabled (enabling them would rewrite all 1859 files in one unreviewable diff), and the rule set usespreset: "none"with an explicit list rather than Biome's broaderrecommended— keeping this a tooling swap, not a smuggled lint-policy change. Adopting more Biome rules is a separate decision. - Config is
biome.jsonc, notbiome.json— the latter rejects comments, failing with a misleading "expected an object, received an array", and the rationale comments inherited fromeslint.config.jsare worth keeping.
- History: was used by
server/services/claudeChangelog.jsand the Apple Health XML import; originally kept as "transitive via pm2→needle" (the 2026-04 audit above). - Resolution: replaced with an owned streaming parser (
server/services/appleHealthXmlParser.js) in issue #1824, and pm2 7.x no longer pulls it. No longer inserver/package.json.
If /do:depfree --heavy is run, the following Tier 1 entries would drop to Tier 2/3 and become replacement candidates regardless of popularity:
@googleapis/calendar/@googleapis/gmail— narrow surface used; could be replaced with direct REST calls + owned auth.- Many of the
@dnd-kit/*,@xterm/*,lucide-react,rechartsdeps would be re-evaluated. pm2would NOT be replaced (foundational process manager).
This is intentionally NOT done in default mode — current dependency footprint is reasonable for the project's deployment context (single-user, Tailscale-private).
Defined in package.json (root + server + client) — kept current to dodge known upstream advisories:
ws@8.21.3(all three)lodash@4.18.1,follow-redirects@1.16.0,js-yaml@4.3.1,ip-address@10.5.0(root + server)nanoid@3.3.18,socket.io-parser@4.2.7(server + client)path-to-regexp@8.4.2(server only)body-parser@2.3.0(server only)qs@6.15.3(server only)tar@7.5.22(server only)engine.io@6.6.9(server only)postcss@8.5.26(server only)protobufjs@7.6.5+@protobufjs/utf8@1.1.2(server only)sharp@0.35.4(server only, collapses the nested copy@huggingface/transformersrequests)brace-expansion@5.0.9(client only)minimatch@3 → brace-expansion@1.1.18(client only, scoped)three@0.185.1(client only, keeps drei/fiber on one three copy)
These exist purely to force-bump transitive deps; revisit if npm audit flags new advisories.
Keep this list in sync with the manifests — a stale entry here reads as a pin that exists when it doesn't. server/dependency-overrides.test.js guards the pins themselves (cross-manifest version parity, plus a MINIMUM_SAFE floor per remediated advisory), but it does not read this document. When a floor moves because a new advisory covers the version already pinned — as js-yaml@4.3.0 did under GHSA-5p4m-2wfm-xmqj — bump the pin, the MINIMUM_SAFE row, and this list together.
Not every compromised package warrants a pin. A pin only helps when the installed version is below the top of its permitted range — otherwise there is nothing to force. The August 2026 keyv / flat-cache / file-entry-cache compromise deliberately got no pin: each range was already at its ceiling (highest published keyv@4.x is 4.5.4, etc.), so a pin would have been a no-op, and the packages were removed outright instead. Check headroom (npm view <pkg>@<major> version) before adding an entry here.
ignore-scripts=true is pinned in every workspace's own .npmrc (root, client/, server/, autofixer/, browser/) — not just the repo root. The list is not maintained by hand: discoverWorkspaces() in scripts/trusted-rebuilds.js globs every top-level directory carrying a package.json, and the test asserts each discovered one has the setting — so a workspace added later is caught rather than silently unguarded. npm resolves the project .npmrc from the local prefix and never walks up the directory tree, so a root-only setting does not cover cd client && npm install or npm ci --prefix server (what CI runs). Deleting any workspace .npmrc silently re-grants every dependency in that workspace an install-time code-execution slot — the vector the Shai-Hulud worm used.
Packages that legitimately need an install script are named explicitly in the allowlist in scripts/trusted-rebuilds.js, the single source consumed by npm run setup, scripts/ensure-deps.js, setup.ps1, update.sh / update.ps1, and CI. scripts/trusted-rebuilds.test.js fails if a workspace .npmrc loses the setting, or if a dependency appears with an install hook that nobody has explicitly decided about. Because CI caches server/node_modules between jobs, scripts/trusted-rebuild-stamp.js writes a mark into the tree in the same step that rebuilds it and checks that mark after a cache restore — a tree cached before the rebuild is otherwise indistinguishable from a good one, since the allowlisted packages all ship prebuilt bindings and import fine either way.