Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,11 @@ jobs:
# Proves uid 100 could actually write through the bind mount — the
# core of BUG 2. /api/ready only goes green once the data dir is
# writable, but assert the file exists too for an unambiguous signal.
test -f data/privacy.db
# sudo because the app chmods data/ to 0700 (owned by uid 100) on
# open — see tightenDataPermissions in lib/db.ts — so the runner
# user can no longer stat inside it. That lockdown is the intended
# behaviour, and this assertion must not depend on it being absent.
sudo test -f data/privacy.db

- name: Stop root compose (bind mount)
if: always()
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ The timeline renders wayback rows with a purple dot (`.timeline-dot.wayback`), a

Pragmas set on open: `journal_mode = WAL`, `busy_timeout = 5000`, `foreign_keys = ON`. The DB file path is always `<cwd>/data/privacy.db`, created on demand. `better-sqlite3` is declared in top-level `serverExternalPackages` in `next.config.js` — keep it there so Next doesn't try to bundle it.

**Permissions are private by contract:** every open runs `tightenDataPermissions()` (`lib/db.ts`) — `0700` on the data dir, `0600` on `privacy.db` + `-wal`/`-shm` — so pre-existing loose installs are fixed on their next boot, and the WAL pragma runs *after* the chmod so sidecar files inherit `0600`. The Tauri sidecar sets `0700` on the dir at creation too (`src-tauri/src/sidecar.rs`). Best-effort on purpose (no-op on Windows; never fatal). Consequences: on a bind-mount deploy the host user needs `sudo` to look inside `./data` (CI's `compose-smoke` does `sudo test -f`), and anything new that writes secrets inside the data dir should still set its own `0600` like `lib/backup.ts` does. Pinned by `tests/app/data-dir-permissions.test.ts`.

All DB calls are **synchronous**. Multi-step writes use `db.transaction(() => { … })()` (see `saveToDb` in `lib/scraper.ts`) — follow that pattern for any new write path that touches more than one table.

### API surface (`app/api/*/route.ts`)
Expand Down
35 changes: 34 additions & 1 deletion lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,46 @@ export const dataDir = process.env.PRIVACYTRACKER_DATA_DIR
? path.resolve(process.env.PRIVACYTRACKER_DATA_DIR)
: path.join(process.cwd(), "data");
if (!(isBuildPhase || fs.existsSync(dataDir))) {
fs.mkdirSync(dataDir, { recursive: true });
fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
}

export const dbPath = isBuildPhase
? ":memory:"
: path.join(dataDir, "privacy.db");

/*
* Tighten on-disk permissions: 0700 on the data dir, 0600 on the DB and
* its WAL/SHM sidecars. The file holds the user's full app inventory,
* notes, and (today) AI provider keys; the process umask would otherwise
* leave everything world-readable on multi-user machines. Runs on every
* open so existing installs are fixed on the next boot, and BEFORE the
* journal_mode=WAL pragma so freshly-created -wal/-shm files inherit the
* 0600 (SQLite copies the main DB file's mode onto its sidecar files).
* Best-effort by design: chmod is meaningless on Windows and can fail on
* exotic mounts — a permissions failure must never prevent the DB from
* opening.
*/
export function tightenDataPermissions(): void {
if (isBuildPhase) {
return;
}
const targets: [string, number][] = [
[dataDir, 0o700],
[dbPath, 0o600],
[`${dbPath}-wal`, 0o600],
[`${dbPath}-shm`, 0o600],
];
for (const [target, mode] of targets) {
try {
fs.chmodSync(target, mode);
} catch {
// Target missing (WAL not created yet) or FS without POSIX modes.
}
}
}

const db = new Database(dbPath);
tightenDataPermissions();

db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"better-sqlite3": "^12.11.1",
"dompurify": "3.4.11",
"dompurify": "3.4.12",
"echarts": "^6.1.0",
"marked": "^18.0.5",
"next": "16.2.9",
"next": "16.2.11",
"next-intl": "4.13.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
Expand Down
Loading
Loading