diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 234e026..c345bdd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to the privacytracker docs -Thanks for helping make the docs better. This file is for **contributing to the docs site itself**. If you're looking for how to contribute to the privacytracker app, see [Contributing](https://docs.privacytracker.io/develop/contributing) inside the docs (or `develop/contributing.mdx` in this repo if you're previewing locally) — that's a different workflow with different tests. +Thanks for helping make the docs better. This file is for **contributing to the docs site itself**. If you're looking for how to contribute to the privacytracker app, see [Contributing](https://docs.privacytracker.privacykey.org/develop/contributing) inside the docs (or `develop/contributing.mdx` in this repo if you're previewing locally) — that's a different workflow with different tests. ## Quick start diff --git a/README.md b/README.md index 66d0387..0197095 100644 --- a/README.md +++ b/README.md @@ -117,14 +117,14 @@ npm run linkcheck 2. Sign in at with your GitHub account. 3. Click **Add deployment** → select this repo → confirm `docs.json` as the config root. 4. Mintlify auto-deploys to `.mintlify.app` and rebuilds on every push to `main`. -5. Optional: under **Settings → Custom domain**, point a CNAME at `cname.mintlify.app` to host the docs at e.g. `docs.privacytracker.io`. +5. Optional: under **Settings → Custom domain**, point a CNAME at `cname.mintlify.app` to host the docs at e.g. `docs.privacytracker.privacykey.org`. 6. Optional: install the Mintlify GitHub App so PRs get a preview-link comment with rendered changes inline. The docs are intentionally decoupled from the main repo's wiki so hosting can move (Mintlify → Vercel → self-hosted) without rewriting the source. ## Adding a language -The docs site is English-only today; the framework is in place to add others. See [Translating the docs](https://docs.privacytracker.io/develop/translating-the-docs) (or `develop/translating-the-docs.mdx` if you're previewing locally) for the step-by-step. +The docs site is English-only today; the framework is in place to add others. See [Translating the docs](https://docs.privacytracker.privacykey.org/develop/translating-the-docs) (or `develop/translating-the-docs.mdx` if you're previewing locally) for the step-by-step. ## Capturing screenshots diff --git a/about-these-docs.mdx b/about-these-docs.mdx index a914f01..078b7ef 100644 --- a/about-these-docs.mdx +++ b/about-these-docs.mdx @@ -64,7 +64,7 @@ Three options, in order of preference: 2. **Click the *Suggest edits* link** in the page footer (Mintlify renders this when feedback is enabled). It opens a PR against the offending MDX file directly. 3. **Mention it in the Plane board.** Useful for *whole-page-is-misleading* feedback that doesn't fit a single PR. -Security-sensitive corrections (the docs claim something about the security posture that isn't true) follow [SECURITY.md](https://github.com/privacykey/privacytracker/blob/main/SECURITY.md) — never a public issue. +Security-sensitive corrections (the docs claim something about the security posture that isn't true) follow [SECURITY.md](https://github.com/privacykey/privacytracker/blob/main/.github/SECURITY.md) — never a public issue. ## Where this lands in practice diff --git a/configuration.mdx b/configuration.mdx index 3a5f775..5e582c8 100644 --- a/configuration.mdx +++ b/configuration.mdx @@ -9,11 +9,39 @@ privacytracker stores almost all configuration in the SQLite `app_settings` key/ | Variable | Default | Purpose | |---|---|---| -| `AUDITOR_ADMIN_TOKEN` | (unset) | Optional shared secret. When set, destructive routes (`POST /api/reset`, `DELETE /api/apps`, `POST /api/settings`) require an `X-Auditor-Admin-Token` header verified with `crypto.timingSafeEqual`. Failed attempts are logged to `audit_log` with IP + user agent. | -| `BUILD_STANDALONE` | (unset) | Build-time flag for `npm run build:standalone` (used by the Tauri sidecar build). | -| `NODE_V8_COVERAGE` | (unset) | Enables V8 coverage for `npm run test:coverage`. | +| `AUDITOR_ADMIN_TOKEN` | (unset) | Shared secret. When set, destructive routes (`POST /api/reset`, `DELETE /api/apps`, `POST /api/settings`) require an `X-Auditor-Admin-Token` header verified with `crypto.timingSafeEqual`. Failed attempts are logged to `audit_log` with IP + user agent. Optional on a loopback-only install; **mandatory** once the instance is network-exposed (see below). | +| `PRIVACYTRACKER_DATA_DIR` | `/data` | Absolute path to the data directory holding `privacy.db`. Honoured unconditionally — the Tauri shell injects it for the desktop build, and it's the escape hatch for a custom Docker mount. Created on demand at mode `0700`. | -That's the entire surface area. Everything else (AI provider, sync schedule, Wayback toggles, notification prefs, focus state, feature-flag overrides) lives in `app_settings` and is changed through the UI. +### Network exposure + +privacytracker's default posture is **loopback-only**, and it enforces that by +checking the request's `Host` header against an allowlist. Deploying it +anywhere other than `127.0.0.1` means setting these — without them a LAN +browser cannot load the app at all. + +| Variable | Default | Purpose | +|---|---|---| +| `PRIVACYTRACKER_ALLOWED_HOSTS` | (unset) | Comma-separated `Host` values to accept, **appended** to the always-allowed loopback set (`localhost`, `127.x`, `::1`). Supports `*.suffix` wildcards. Listing a non-loopback entry also flips the instance to network-exposed, which makes `AUDITOR_ADMIN_TOKEN` mandatory. | +| `PRIVACYTRACKER_NETWORK_EXPOSED` | (unset) | Boolean (`1`/`true`/`yes`/`on`). Forces the network-exposed posture without naming a host — for a reverse proxy that rewrites `Host`. | +| `PRIVACYTRACKER_TRUST_PROXY` | (unset) | Boolean. Honour `X-Forwarded-Host` / `X-Forwarded-For` for the host allowlist, rate-limit keys, and audit IPs. This is an operator assertion that a trusted proxy sits in front — leave it unset for a direct bind, where those headers are attacker-controlled. | +| `PRIVACYTRACKER_BIND_HOST` | (unset) | Explicit bind interface. A specific non-loopback IP implies network-exposed. `HOSTNAME` is deliberately *not* trusted as a bind signal, because Docker sets it to the container ID. | + + + Trust is derived from deployment config, never from request headers — a + `Host: localhost` from a LAN attacker cannot downgrade the instance to + "local". `lib/deployment-trust.ts` is the single source of truth, and it's + read fresh on every call. + + +### Build and runtime + +| Variable | Default | Purpose | +|---|---|---| +| `PRIVACYTRACKER_RUNTIME` | (unset) | Set to `desktop` by the Tauri sidecar launcher. Gates desktop-only surfaces and feature-flag resolution. | +| `WORKER_DISABLED` | (unset) | Suppresses the background scheduler tick — used by tests and by processes that should not run sync. | +| `BUILD_STANDALONE` | (unset) | Build-time flag for `pnpm build:standalone` (the Tauri sidecar bundle). | + +Everything else (AI provider, sync schedule, Wayback toggles, notification prefs, focus state, feature-flag overrides) lives in `app_settings` and is changed through the UI. ## AI providers @@ -106,12 +134,23 @@ When an empty quarter has no Wayback capture anywhere in the ±42-day tolerance ## Translations -The UI ships through next-intl, and **today that means English only** — the localisation framework is wired up, but the English bundle is the only one that currently ships. The active language is selected at render time from your app settings (a flat route tree, with no per-locale URL prefix), so additional locales appear automatically as they're translated. +The UI ships through next-intl. Two locales currently ship — **English (`en`) +and Simplified Chinese (`zh`)** — at full key parity. The supported set is +declared as `SUPPORTED_LOCALES` in `i18n.ts`. + +The active language is resolved per request from the `NEXT_LOCALE` cookie, +falling back to `en` when the cookie is absent or holds an unsupported value. +Routes are flat, with no per-locale URL prefix, so additional locales appear +automatically as they're translated — drop a `locales/.json` and append +the code to `SUPPORTED_LOCALES`. -`locales/en.json` is the source of truth; every other `locales/.json` is round-tripped through Crowdin (free OSS plan). Adding or changing locales is a developer task — see [Translations](/develop/translations) under the Develop tab for the full workflow. +`locales/en.json` is the source of truth; every other `locales/.json` is +round-tripped through Crowdin (free OSS plan). Adding or changing locales is a +developer task — see [Translations](/develop/translations) under the Develop +tab for the full workflow. ```bash -npm run lint:i18n # check key parity against en.json +pnpm lint:i18n # check key parity against en.json ``` ## Health and readiness diff --git a/cookbook.mdx b/cookbook.mdx index 7985a63..32e9bfc 100644 --- a/cookbook.mdx +++ b/cookbook.mdx @@ -39,10 +39,10 @@ The job: get a clean view of everything installed on a child's device, surface w Plug the iPad into your Mac, run a Finder backup (no encryption needed), then from your privacytracker source checkout (or any clone of the main repo): ```bash - python3 tools/ios-app-import/export_ios_apps.py --mode backup + python3 scripts/ios-app-import/export_ios_apps.py --mode backup ``` - The helper writes a `.txt` of every installed app's bundle ID and display name. If you're not running from source, the helper is published as a stand-alone script under `tools/ios-app-import/` in the [main repo](https://github.com/privacykey/privacytracker/tree/main/tools/ios-app-import) — clone just that directory and run it; you don't need the rest of the codebase. + The helper writes a `.txt` of every installed app's bundle ID and display name. If you're not running from source, the helper is published as a stand-alone script under `scripts/ios-app-import/` in the [main repo](https://github.com/privacykey/privacytracker/tree/main/scripts/ios-app-import) — clone just that directory and run it; you don't need the rest of the codebase. Alternatively, if you have `ideviceinstaller` installed (`brew install libimobiledevice`), use `--mode device` against the connected iPad without taking a backup. diff --git a/develop/architecture.mdx b/develop/architecture.mdx index 452b1ab..ffb780b 100644 --- a/develop/architecture.mdx +++ b/develop/architecture.mdx @@ -204,7 +204,7 @@ The data and helpers live in `lib/privacy-profile.ts`: - `PROFILE_PRESET_META` — label, icon, and `severityCls` so the active-pill accent walks the green → yellow → orange → red gradient. - `matchPreset(profile)` — returns the matching preset key or `null`. -`PROFILE_PRESETS.balanced` is locked to `DEFAULT_PROFILE` by reference (`{ ...DEFAULT_PROFILE }`), and `tests/profile-presets.test.ts` pins the equality. +`PROFILE_PRESETS.balanced` is locked to `DEFAULT_PROFILE` by reference (`{ ...DEFAULT_PROFILE }`), and `tests/app/profile-presets.test.ts` pins the equality. If you change `DEFAULT_PROFILE` you change the Balanced preset. That's intentional, but note that returning users currently sitting on the Balanced highlight will silently migrate to the new tier set. Communicate the change in release notes if it's user-visible. @@ -223,7 +223,7 @@ The data and helpers live in `lib/privacy-profile.ts`: 2. Add a complete tier map under `PROFILE_PRESETS`. 3. Add meta under `PROFILE_PRESET_META` (pick a `severityCls` so the active-pill accent fits the gradient). 4. Add `labels.` and `descriptions.` strings under `settings.profile_editor.presets` in `locales/en.json`. Crowdin handles the other locales — see [Translations](/develop/translations). -5. Extend the asserts in `tests/profile-presets.test.ts` if the new preset has invariants worth pinning (e.g. "must keep `SENSITIVE_INFO` at `not_collected`"). +5. Extend the asserts in `tests/app/profile-presets.test.ts` if the new preset has invariants worth pinning (e.g. "must keep `SENSITIVE_INFO` at `not_collected`"). ### Carried into audit bundles diff --git a/develop/build-from-source.mdx b/develop/build-from-source.mdx index 180d092..62d571b 100644 --- a/develop/build-from-source.mdx +++ b/develop/build-from-source.mdx @@ -65,7 +65,7 @@ npm run typecheck # tsc --noEmit ``` ```bash test -npm test # focused node:test suite (uses tsx + tests/setup-env.ts) +npm test # focused node:test suite (uses tsx + tests/helpers/setup-env.ts) npm run test:coverage # adds V8 coverage in coverage/v8/ ``` @@ -80,11 +80,11 @@ npm run build:standalone # build the Tauri sidecar bundle (BUILD_STANDALONE=1) ## Running the iPhone import helper -The companion Python tool in `tools/ios-app-import/` is stdlib-only and unrelated to the Node app — it produces a text file the web onboarding accepts. +The companion Python tool in `scripts/ios-app-import/` is stdlib-only and unrelated to the Node app — it produces a text file the web onboarding accepts. ```bash -python3 tools/ios-app-import/export_ios_apps.py --mode backup -python3 tools/ios-app-import/export_ios_apps.py --mode device +python3 scripts/ios-app-import/export_ios_apps.py --mode backup +python3 scripts/ios-app-import/export_ios_apps.py --mode device ``` Run its tests with: diff --git a/develop/contributing.mdx b/develop/contributing.mdx index 44266bf..9117c30 100644 --- a/develop/contributing.mdx +++ b/develop/contributing.mdx @@ -77,7 +77,7 @@ If you touched build configuration (`next.config.js`, `tsconfig.json`, `package. npm run build # production build, catches Next config errors ``` -If you touched `tools/ios-app-import/`: +If you touched `scripts/ios-app-import/`: ```bash npm run test:ios-import-helper @@ -97,7 +97,7 @@ Three things, in priority order. **The change should be small enough to review in one sitting.** If your branch touches a parser, a UI component, a chart, and a migration, that's four PRs. Reviewers approve smaller PRs faster, and bisecting later is easier. -**Tests for parser changes are non-negotiable.** Apple's HTML is volatile. The pattern in `tests/scraper.test.ts` is to drop a captured payload into `tests/fixtures/scraper/` and assert against parsed output. When you fix a parser bug, capture the payload that was breaking, and add a test against it — that prevents regression next time Apple iterates. +**Tests for parser changes are non-negotiable.** Apple's HTML is volatile. The pattern in `tests/app/scraper-fixture.test.ts` is to stub `global.fetch` so it returns a captured App Store payload from an in-file helper, then assert against the parsed output stored in the DB. When you fix a parser bug, capture the payload that was breaking, add it the same way, and assert on it — that prevents regression next time Apple iterates. The sibling `scraper-shoebox`, `scraper-lookup`, `scraper-advanced` and `scraper-related-shelves` tests cover the other parser eras and endpoints. **Database changes need both `CREATE TABLE` and `migrations` updates.** Forgetting one breaks upgrade paths for existing installs. The pattern is in `lib/db.ts`; copy it. @@ -158,7 +158,7 @@ A few conventions to know before you open a PR: - **`@/*` is the path alias.** It maps to the repo root. Use it instead of `../../../lib/foo`. - **Server-only modules go in `lib/` with the `'server-only'` import where they must not be bundled.** Examples: `lib/feature-flags-server.ts`. Importing one of these from a Client Component fails the build. - **All DB writes through transactions.** Multi-step writes use `db.transaction(() => { … })()`. Don't open ad-hoc connections. -- **Tests use `node:test` plus `tsx`.** Run via `npm test`. Look at `tests/setup-env.ts` for the standard env shape. +- **Tests use `node:test` plus `tsx`.** Run via `npm test`. Look at `tests/helpers/setup-env.ts` for the standard env shape. - **Don't add new top-level dependencies casually.** Privacy is the value prop; every dep is a supply-chain question. Justify in the PR description. ## Reporting issues vs. opening PRs diff --git a/develop/feature-flags.mdx b/develop/feature-flags.mdx index a9e1a1b..e172dd3 100644 --- a/develop/feature-flags.mdx +++ b/develop/feature-flags.mdx @@ -7,10 +7,12 @@ Every user-facing surface in privacytracker is gated by the **focus system**: a This page is the ergonomic summary. The full inventory and rollout reasoning live on the [privacytracker Plane board](https://sites.plane.so/issues/39b6604351894f09a5e903acce37d265). -## The five-module split +## The module split Next 16 will refuse to build if these get tangled. Don't break the layering. +The five modules that carry the resolver: + | Module | Role | Constraint | |---|---|---| | `lib/feature-flag-rules.ts` | Sparse rule tables: `HARD_DEFAULTS`, `AUDIENCE_RULES`, `GOAL_RULES`, `ACCESSIBILITY_RULES`, `FLAG_DEPENDENCIES`, `TOUR_STEPS` | Pure data + helpers. Server-safe. | @@ -19,6 +21,14 @@ Next 16 will refuse to build if these get tangled. Don't break the layering. | `lib/feature-flags-server.ts` | `getResolverContextFromDb`, `resolveFlagFromDb` | `'server-only'`. | | `lib/feature-flag-storage.ts` | SQLite reads/writes via better-sqlite3 | All synchronous. | +Plus two that exist only to make the Developer Options panel legible. Neither +affects resolution — nothing in the app branches on them: + +| Module | Role | +|---|---| +| `lib/feature-flag-usage.ts` | Curated map of where each flag lives in the codebase and which route shows its effect. Powers the panel's hover-preview and its "Show me where" link. Deliberately partial — unlisted flags fall through to "no preview available". | +| `lib/feature-flag-wired.ts` | The set of flags actually consumed by component code today. The panel badges everything outside it "(no effect yet)", so a tester can tell a live toggle from an inert one. | + ## Flag-key convention All keys are lowercase, dot-separated, and prefixed `flag.` so they grep cleanly and won't collide with existing `app_settings` keys. diff --git a/develop/overview.mdx b/develop/overview.mdx index e6a67d9..1d8a204 100644 --- a/develop/overview.mdx +++ b/develop/overview.mdx @@ -73,4 +73,4 @@ Anything not here — release pipeline, code signing, the GitHub Actions that pu ## Reporting issues -[github.com/privacykey/privacytracker/issues](https://github.com/privacykey/privacytracker/issues). Bug reports use the `bug_report.yml` template; security reports follow [SECURITY.md](https://github.com/privacykey/privacytracker/blob/main/SECURITY.md). +[github.com/privacykey/privacytracker/issues](https://github.com/privacykey/privacytracker/issues). Bug reports use the `bug_report.yml` template; security reports follow [SECURITY.md](https://github.com/privacykey/privacytracker/blob/main/.github/SECURITY.md). diff --git a/develop/tauri.mdx b/develop/tauri.mdx index 8f2f0c0..31ad0fd 100644 --- a/develop/tauri.mdx +++ b/develop/tauri.mdx @@ -124,7 +124,7 @@ If a signature ever fails to verify, Tauri: 3. Surfaces a notification to the user. 4. Does not retry the same patch. -For Tier 3 (public-internet) deploys, you can disable auto-updates entirely with `AUDITOR_DISABLE_AUTO_UPDATE=1` — see [Hardening → Disable inbound auto-update on Tier 3](/hardening#disable-inbound-auto-update-on-tier-3). +The updater has no opt-out switch — the plugin is registered unconditionally in `src-tauri/src/main.rs` and `plugins.updater.active` is `true`. For Tier 3 (public-internet) deploys that need deterministic update timing, see [Hardening → Disable inbound auto-update on Tier 3](/hardening#disable-inbound-auto-update-on-tier-3) for the workarounds that actually exist. ## Filesystem layout on disk diff --git a/devices.mdx b/devices.mdx new file mode 100644 index 0000000..298229f --- /dev/null +++ b/devices.mdx @@ -0,0 +1,125 @@ +--- +title: Devices +description: "Tracking apps across more than one iPhone or iPad — re-sync, orphan handling, and the Apple Configurator actions." +--- + +Most people track one phone. If you track more than one — your own plus a +partner's, or a couple of the kids' — privacytracker keeps a row per device and +remembers which apps came from where. + +Devices are created by the **import** flow, not by hand. Whenever you import an +app list, privacytracker either matches an existing device or creates a new one. +Imports that carry an **ECID** (the Apple Configurator export, or the Python +helper reading a connected device) match on that identifier, so re-importing the +same phone updates the existing row rather than duplicating it. Imports without +one — screenshots, a pasted list — land against a placeholder device you can +rename once you know which phone they came from. + + + The Devices page is gated behind `flag.settings.devices_page`. If + **Settings → Devices** isn't in your sidebar, that flag is off for your + current focus — see [Feature flags](/develop/feature-flags). + + +## The Devices page + +**Settings → Devices** (`/dashboard/settings/devices`) lists one row per device +with its app count. Each row can be renamed, deleted, or re-synced. + +Per device, privacytracker stores a name, the ECID if it has one, the model, +device class and iOS version reported at import, when it was created, and when +it last synced. + +Elsewhere in the app, an app's detail page shows a **Tracked on** row of device +chips, and an "Installed on *N* devices" panel that links back here. + +## Re-syncing a device + +Re-sync answers "what's changed on this phone since last time". It runs in two +phases, deliberately — nothing is written until you confirm. + + + + `POST /api/device-sync/preview` diffs the incoming app list against the set + currently linked to that device and returns `adds`, `removes` and + `unchanged`. Matching is keyed on App Store track ID; resolving bundle IDs + or names to a track ID is the importer's job, done before this point. + + + You tick the subset you actually want applied. `POST /api/device-sync/commit` + writes it in a single transaction. + + + +### Orphans + +An app can be linked to several devices. Removing it from one device usually +just drops that link. + +But if the app isn't on any *other* device, removing it here untracks it +**entirely** — you lose its history along with it. Every row in the `removes` +list is flagged with `wouldOrphan` for exactly this reason, and the UI warns you +before you commit. The same sweep runs when you delete a whole device. + + + Deleting a device untracks every app that existed only on it. The confirmation + dialog tells you how many apps that is. There is no undo — restore from a + [backup](/backup-and-restore) if you delete the wrong one. + + +After a successful re-sync you'll see a summary: how many apps were added, +removed, untracked entirely, and how many duplicate device rows were collapsed. + +## Device actions + +On the desktop build, privacytracker can drive Apple Configurator's `cfgutil` +against a connected device to **back it up** or **uninstall an app**. + +The subprocess runs on the Tauri side, behind a Touch ID prompt. The server +never executes the command — the API routes exist to check whether the action is +permitted and to write the audit record. + +### What has to be true first + +Uninstall is guarded three ways, and all three must pass: + +| Gate | Requirement | +|---|---| +| Audience | Your active focus must be **self**. Loved-one and guardian audiences can build an uninstall list but cannot execute it. | +| Flag | `flag.devopts.cfgutil_uninstall` must be `on`. It is **off by default**. | +| Backup freshness | A successful backup of that device within the last 24 hours. Per-call opt-out exists, but the audience and flag gates do not bend. | + +The audience gate is the important one. It means privacytracker will not let +someone in guardian mode remotely remove apps from the device they're +supervising — the tool reports, it doesn't reach across and act. + +### The audit trail + +Every attempt writes an activity row — `cfgutil_backup` or `cfgutil_uninstall` — +whether or not it succeeded. Those rows are visible in the Developer Options +activity log and travel with your [audit bundle](/backup-and-restore). + +## API reference + +| Endpoint | Methods | Purpose | +|---|---|---| +| `/api/devices` | `GET`, `POST` | List devices; create one | +| `/api/devices/{id}` | `GET`, `PATCH`, `DELETE` | Fetch, rename, delete | +| `/api/devices/{id}/bundles` | `GET` | Bundle IDs seen on this device | +| `/api/devices/{id}/tracked-apps` | `GET` | Apps currently linked to it | +| `/api/devices/for-app/{appId}` | `GET` | Which devices an app sits on | +| `/api/device-sync/preview` | `POST` | Diff without writing | +| `/api/device-sync/commit` | `POST` | Apply the ticked subset | +| `/api/device-actions/backup` | `POST` | Record a completed `cfgutil` backup | +| `/api/device-actions/uninstall` | `GET`, `POST` | Check the gates; record the attempt | + +## Related + + + + The four import routes, and which ones carry an ECID. + + + What a backup contains, and how to undo a bad delete. + + diff --git a/docs.json b/docs.json index 0181e88..0aabe15 100644 --- a/docs.json +++ b/docs.json @@ -37,7 +37,8 @@ "alternatives", "quickstart", "installation", - "configuration" + "configuration", + "devices" ] }, { @@ -171,7 +172,7 @@ }, "integrations": { "plausible": { - "domain": "docs.privacytracker.io" + "domain": "docs.privacytracker.privacykey.org" } }, "metadata": { diff --git a/faq.mdx b/faq.mdx index f0a58ef..2358be5 100644 --- a/faq.mdx +++ b/faq.mdx @@ -90,7 +90,7 @@ description: "Common questions about what privacytracker is, what it isn't, and - Yes. The Tauri shell (`src-tauri/`), the Next.js bundle, the iPhone import helper (`tools/ios-app-import/`), and everything else in the repo is Apache-2.0 licensed. The signed binaries we publish on GitHub releases are built deterministically from the same source — you can build your own from `develop/build-from-source` if you want to verify. + Yes. The Tauri shell (`src-tauri/`), the Next.js bundle, the iPhone import helper (`scripts/ios-app-import/`), and everything else in the repo is Apache-2.0 licensed. The signed binaries we publish on GitHub releases are built deterministically from the same source — you can build your own from `develop/build-from-source` if you want to verify. diff --git a/hardening.mdx b/hardening.mdx index 0699cee..b4c1d1f 100644 --- a/hardening.mdx +++ b/hardening.mdx @@ -214,17 +214,21 @@ Periodically verify a backup by restoring it into a throwaway instance — a bac ## Disable inbound auto-update on Tier 3 -The Tauri auto-updater fetches signed patches from GitHub releases on a recurring schedule. The ed25519 signature check makes this safe in principle, but for a Tier 3 (public-internet) deploy you may want to *disable* automatic updates and manually pull-and-verify each release instead. Tradeoff: you lose silent security patches; you gain deterministic timing of every code change. +The Tauri auto-updater fetches signed patches from GitHub releases on a recurring schedule, verified against the bundled minisign public key. For a Tier 3 (public-internet) deploy you may want to stop automatic updates and pull-and-verify each release by hand instead. Tradeoff: you lose silent security patches; you gain deterministic timing of every code change. -```bash -# In the desktop app's launchd plist: -EnvironmentVariables - - AUDITOR_DISABLE_AUTO_UPDATE1 - -``` + + **There is currently no supported switch for this.** The updater plugin is + registered unconditionally in `src-tauri/src/main.rs` and + `tauri.conf.json` sets `plugins.updater.active: true` — there is no + environment variable and no in-app toggle that turns it off. + + +If you need deterministic update timing today, the options are: + +- **Run the Docker build instead of the desktop app** on Tier 3 hosts. Image updates are explicit (`docker compose pull && up`), which is the posture this tier wants anyway. +- **Block the update endpoint at the host firewall** — the updater reads `https://github.com/privacykey/privacytracker/releases/latest/download/latest.json`. A failed check now backs off (15 minutes, doubling, capped at a day) rather than retrying tightly, so a blackholed endpoint is not a busy loop. -Docker installs don't have this concern — image updates are explicit (`docker compose pull && up`). +Tracking a first-class opt-out: [privacytracker issues](https://github.com/privacykey/privacytracker/issues). ## Network restrictions diff --git a/installation.mdx b/installation.mdx index c60ee22..6b86e7f 100644 --- a/installation.mdx +++ b/installation.mdx @@ -78,10 +78,10 @@ Companion Python tool (stdlib-only, Python 3.9+) that exports installed-app list ```bash # From a Finder / iTunes backup -python3 tools/ios-app-import/export_ios_apps.py --mode backup +python3 scripts/ios-app-import/export_ios_apps.py --mode backup # From a connected device (requires libimobiledevice) -python3 tools/ios-app-import/export_ios_apps.py --mode device +python3 scripts/ios-app-import/export_ios_apps.py --mode device ``` You can run the helper from the source repo even when the rest of privacytracker is installed as a desktop app or Docker container — its output is just a text file the web onboarding accepts. diff --git a/security.mdx b/security.mdx index 3e5f44a..2981d63 100644 --- a/security.mdx +++ b/security.mdx @@ -258,7 +258,7 @@ privacytracker ships as source in the main repository and as a Docker image. **I - The Next.js web application (`app/`, `lib/`, `components/`). - The HTTP API surface (`app/api/**`). -- The companion Python CLI (`tools/ios-app-import/`). +- The companion Python CLI (`scripts/ios-app-import/`). - The official Docker image and the reference `docker-compose.yml`. - The default configuration shipped in this repo — including the database schema in `lib/db.ts`, the scraping logic in `lib/scraper.ts`, and the AI-provider request flow in `lib/ai-config.ts`. - Exposure of locally-stored secrets (AI provider keys, session data) via the web UI or API.