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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ npm run linkcheck
2. Sign in at <https://dashboard.mintlify.com> with your GitHub account.
3. Click **Add deployment** → select this repo → confirm `docs.json` as the config root.
4. Mintlify auto-deploys to `<your-subdomain>.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

Expand Down
2 changes: 1 addition & 1 deletion about-these-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 46 additions & 7 deletions configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<cwd>/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. |

<Warning>
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.
</Warning>

### 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

Expand Down Expand Up @@ -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/<code>.json` and append
the code to `SUPPORTED_LOCALES`.

`locales/en.json` is the source of truth; every other `locales/<lang>.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/<lang>.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
Expand Down
4 changes: 2 additions & 2 deletions cookbook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Step>
Expand Down
4 changes: 2 additions & 2 deletions develop/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Warning>
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.
Expand All @@ -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.<key>` and `descriptions.<key>` 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

Expand Down
8 changes: 4 additions & 4 deletions develop/build-from-source.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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/
```

Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions develop/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion develop/feature-flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion develop/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 1 addition & 1 deletion develop/tauri.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading