mirror of
https://github.com/actualbudget/actual.git
synced 2026-07-27 17:14:30 -05:00
* [AI] fix(loot-core): replace adm-zip with fflate for zip handling
Replaces the adm-zip dependency (which requires Node's `process`/`Buffer`
and can break in browser bundle contexts) with fflate, a zero-dependency,
isomorphic zip library that works identically in node and browser builds.
Adds a small `zip-utils.ts` wrapper (`safeZip`/`safeUnzip`) around fflate
to preserve the safety guarantees a hardened zip library provides by
default, since all three call sites parse zips that may cross a trust
boundary (cloud sync downloads, user-supplied budget/import files):
- Rejects zip-slip/path-traversal entry names (absolute paths, `..`
segments, backslashes, drive letters, NUL bytes).
- Caps total archive size and per-entry uncompressed size, enforced via
fflate's `filter` callback before it allocates the decompression buffer
for each entry, to guard against decompression bombs.
- Rejects duplicate/case-variant entry names to avoid ambiguity attacks.
Updated call sites: budgetfiles/backups.ts (local backup zip write/read),
cloud-storage.ts (cloud sync export/import), importers/ynab4.ts (YNAB4
budget import).
* [AI] test(desktop-client): drop export/import perf e2e test
Removes packages/desktop-client/e2e/export-import-performance.test.ts,
added in an earlier commit to benchmark adm-zip vs fflate. Per review
feedback (matt-fidd): this zip-handling logic changes rarely, and a
future library swap would warrant its own fresh benchmarking rather
than maintaining a ~300 line permanent regression test for it.
The benchmark numbers already recorded in the PR description remain
accurate for this change.
* [AI] fix(loot-core): distinguish oversized-zip errors from generic parse failures
cloud-storage.ts's importBuffer (shared by both cloud-sync download and the
local "Import my budget -> Actual" file picker) caught every safeUnzip
failure the same way and threw FileDownloadError('not-zip-file'). Since
safeZip has no size limit but safeUnzip enforces one by default, a
self-generated export that grows past that cap (e.g. a large db.sqlite)
would fail to re-import with a misleading "this isn't a zip file" message,
even though the file is perfectly valid, just larger than the cap.
Catches UnsafeZipError separately and surfaces a new 'zip-too-large' reason,
with matching UI messages in both places that already handle
not-zip-file/invalid-zip-file/invalid-meta-file (loot-core's shared error
mapper and desktop-client's local import modal), reusing the same support-
link phrasing already used for invalid files.
* [AI] fix(loot-core): stop faking runtime config for zip upload size limit
Vite bakes process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB into the client
bundle at build time, so it was never actually runtime-configurable in
the browser build despite looking like sync-server's real env var.
Drop the env indirection and hardcode a fixed 20MB entry cap / 60MB
archive cap instead.
* [AI] fix(loot-core): tighten zip entry-name matching in importBuffer
* [AI] feat(loot-core): raise zip size limits to 500MB, warn on oversized/low-memory exports
Matt-fidd's review on #8393 argued 20MB was too tight for real budgets and
asked for (1) a more generous cap, (2) a warning when a backup is created
that won't be re-importable, and (3) test coverage for zip.ts's safety
checks (path traversal, size limits, duplicate entries, unsafe names,
round-trip). Later review passes asked to trim the verbose comments in
zip.ts and to use <Trans> instead of t() for the new warning strings.
- Collapse the three separate default caps into one MAX_ZIP_SIZE (500MB),
since archive size, per-entry size, and total uncompressed size all
shared the same value anyway. safeUnzip can run outside the browser
(Electron, or Node via @actual-app/api) where the whole archive is held
in memory at once, and some self-hosting providers (PikaPods) run
containers with as little as 256MB RAM, so the cap stays flat rather
than compounding into a multi-GB worst case.
- Add exceedsSafeUnzipLimits() to zip.ts so exportBuffer() can warn
upfront, using the same cap import enforces, instead of drifting.
- Add #platform/server/memory (default/api/electron) exposing
os.freemem() on node/electron and null in the browser, per the existing
platform-conditional-export pattern. exportBuffer() warns if the
uncompressed db.sqlite is bigger than currently-free memory; skipped
entirely when the platform has no such API.
- Surface both warnings in the Export settings screen via <Trans>.
- Add zip.test.ts covering round trip, path traversal, unsafe names,
duplicate entries, and all three size caps.
- Trim zip.ts's file-header comment per review feedback that it was too
verbose to be useful.
* [AI] feat: surface descriptive, translatable errors for unsafe zip imports
UnsafeZipError now carries structured meta (zipReason + entryName/maxSize)
instead of only a free-text English message. The meta rides on
FileDownloadError('zip-too-large', ...) through the download and import-budget
paths, and the desktop-client maps it to detailed, translated messages
(which file, which limit) in getUnsafeZipError(); loot-core's shared
getDownloadError() gets a plain-English equivalent for the headless API.
Why structured meta rather than translating the message itself: i18n must not
live in loot-core (see platform/client/connection/index.ts) - the backend
worker has no i18next instance and doesn't know the user's locale, and a
message translated at throw time would bake a locale into a string that also
feeds the headless @actual-app/api and server logs, which want stable English.
i18next also needs a static key plus interpolation params to extract and look
up translations, so the error must cross the worker boundary as code + params
(zipReason + entryName/maxSize) and be rendered with t() at display time in
the client. The English Error.message is kept for logs and the API.
* Update release note to include safe wrappers
* [AI] fix(loot-core): require db.sqlite and metadata.json from the same archive directory
importBuffer previously located db.sqlite and metadata.json independently,
so an archive with a/db.sqlite and b/metadata.json would pair unrelated
files, and duplicate matches were resolved by archive order. Now prefer the
root-level pair; otherwise both files must come from exactly one shared
directory, and anything ambiguous is rejected as invalid-zip-file.
* [AI] fix(loot-core): restrict safeZip to flat entries so every name is validated
safeZip only ran assertSafeEntryName on top-level keys, but fflate's
Zippable type permits nested directory objects whose child names would
reach zipSync unvalidated. Both callers pass flat records, so narrow the
parameter to Record<string, Uint8Array> and let the compiler rule out
nested input.
131 lines
4.9 KiB
JSON
131 lines
4.9 KiB
JSON
{
|
|
"name": "actual",
|
|
"version": "0.0.1",
|
|
"private": true,
|
|
"description": "A local-first personal finance system",
|
|
"homepage": "https://github.com/actualbudget/actual/",
|
|
"bugs": {
|
|
"url": "https://github.com/actualbudget/actual/issues/"
|
|
},
|
|
"license": "MIT",
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git@github.com:actualbudget/actual.git"
|
|
},
|
|
"workspaces": {
|
|
"packages": [
|
|
"packages/*"
|
|
]
|
|
},
|
|
"scripts": {
|
|
"start": "yarn start:browser",
|
|
"start:server": "yarn workspace @actual-app/sync-server start",
|
|
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
|
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
|
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
|
"start:docs": "yarn workspace docs start",
|
|
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:plugins-service",
|
|
"start:desktop-node": "yarn workspace @actual-app/core watch:node",
|
|
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
|
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
|
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
|
"start:browser": "npm-run-all --parallel 'start:browser-*' 'start:service-plugins'",
|
|
"start:service-plugins": "yarn workspace plugins-service watch",
|
|
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
|
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
|
"build": "lage build",
|
|
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
|
"build:browser": "./bin/package-browser",
|
|
"build:desktop": "./bin/package-electron",
|
|
"build:plugins-service": "yarn workspace plugins-service build",
|
|
"build:api": "yarn build --scope=@actual-app/api",
|
|
"build:cli": "yarn build --scope=@actual-app/cli",
|
|
"build:docs": "yarn workspace docs build",
|
|
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
|
"deploy:docs": "yarn workspace docs deploy",
|
|
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
|
"generate:release-notes": "node ./bin/release-note-generator.mts",
|
|
"test": "lage test --continue",
|
|
"test:debug": "lage test --no-cache --continue",
|
|
"e2e": "yarn workspace @actual-app/web run e2e",
|
|
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",
|
|
"playwright": "yarn workspace @actual-app/web run playwright",
|
|
"vrt": "yarn workspace @actual-app/web run vrt",
|
|
"vrt:docker": "./bin/run-vrt",
|
|
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt,argon2 --build-from-source -f",
|
|
"rebuild-node": "yarn workspace @actual-app/core rebuild",
|
|
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
|
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
|
"knip": "knip",
|
|
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
|
"constraints": "yarn constraints",
|
|
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
|
|
"check:tsconfig-references": "workspaces-to-typescript-project-references --check",
|
|
"sync:tsconfig-references": "workspaces-to-typescript-project-references",
|
|
"prepare": "husky"
|
|
},
|
|
"devDependencies": {
|
|
"@monorepo-utils/workspaces-to-typescript-project-references": "^2.11.0",
|
|
"@octokit/rest": "^22.0.1",
|
|
"@types/node": "^22.19.21",
|
|
"@types/prompts": "^2.4.9",
|
|
"@typescript/native-preview": "beta",
|
|
"@yarnpkg/types": "^4.0.1",
|
|
"eslint": "^10.5.0",
|
|
"eslint-plugin-perfectionist": "^5.9.0",
|
|
"eslint-plugin-typescript-paths": "^0.0.33",
|
|
"husky": "^9.1.7",
|
|
"knip": "^6.17.1",
|
|
"lage": "^2.15.13",
|
|
"minimatch": "^10.2.5",
|
|
"nano-staged": "^1.0.2",
|
|
"npm-run-all": "^4.1.5",
|
|
"oxfmt": "^0.44.0",
|
|
"oxlint": "^1.69.0",
|
|
"oxlint-tsgolint": "^0.23.0",
|
|
"p-limit": "^7.3.0",
|
|
"prompts": "^2.4.2",
|
|
"typescript": "^6.0.3",
|
|
"vitest": "^4.1.8"
|
|
},
|
|
"dependenciesMeta": {
|
|
"argon2": {
|
|
"built": true
|
|
},
|
|
"bcrypt": {
|
|
"built": true
|
|
},
|
|
"better-sqlite3": {
|
|
"built": true
|
|
},
|
|
"electron": {
|
|
"built": true
|
|
},
|
|
"esbuild": {
|
|
"built": true
|
|
},
|
|
"sharp": {
|
|
"built": true
|
|
}
|
|
},
|
|
"optionalDependencies": {
|
|
"glob-hasher-linux-arm64-gnu": "1.4.2"
|
|
},
|
|
"resolutions": {
|
|
"minimatch@3.1.2": "3.1.5",
|
|
"serialize-javascript": "^7.0.5",
|
|
"socks": ">=2.8.3",
|
|
"vite-plugin-node-polyfills": "patch:vite-plugin-node-polyfills@npm%3A0.28.0#~/.yarn/patches/vite-plugin-node-polyfills-npm-0.28.0-bac39616ee.patch",
|
|
"webpackbar": "^7.0.0"
|
|
},
|
|
"browserslist": [
|
|
"electron >= 35.0",
|
|
"defaults"
|
|
],
|
|
"engines": {
|
|
"node": ">=22.18.0",
|
|
"yarn": "^4.9.1"
|
|
},
|
|
"packageManager": "yarn@4.13.0"
|
|
}
|