SPEC v0.20: D-119 APK download rate limit; owner answers to the security review; HANDOFF, prompts

D-119 (landed 96e4cbd, security review L-3): E-6 at most 10 requests per
minute per client IP in its own window, not counted against the 60 of the
other unauthenticated endpoints; SPEC §8.6, §10.6, E-6. M-1 closed by the
owner as an accepted risk (§8.1, §8.7); the D-114 limits confirmed (§11.4).
HANDOFF: main and origin at 96e4cbd, NPM rework (D-120) in flight, owner
steps and questions rewritten, numbering next D-121 and v0.21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-19 19:35:24 +02:00
parent 96e4cbdf4e
commit 481889a9dc
4 changed files with 44 additions and 48 deletions
+31 -39
View File
@@ -3,9 +3,11 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `c582363` (the Android overhaul, landed after the SPEC v0.18 docs commit `48fe119`) plus the SPEC v0.19 docs commit from `agent-docs-v019` once it lands. `origin/main` (Gitea) at `c582363`: everything up to the Android overhaul is pushed. Nothing in flight; tonight's queue is complete.
- `main` at `96e4cbd` (the APK download rate limit, D-119, landed after the SPEC v0.19 docs commit `3ef2c15`) plus the SPEC v0.20 docs commit from `agent-docs-v020` once it lands. `origin/main` (Gitea) at `96e4cbd`: everything up to the rate limit is pushed.
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul.
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119).
- Landed since SPEC v0.19 (recorded in v0.20):
- APK download rate limit (`96e4cbd`, D-119, security review L-3): E-6 `GET /apk/files/{file_name}` at most 10 requests per minute per client IP in its own window, not counted against the 60 of the other unauthenticated endpoints; a 404 counts; 429 `rate_limited` with `Retry-After`; E-5 and E-4 stay on the 60; unit and integration tests.
- Landed since SPEC v0.18 (recorded in v0.19):
- Android overhaul (`e4e6e5d`, `8229ed6`, `515b60f`, `e08aaf0`, `0714242`, `a92c236`, `74b5fbd`, `c582363`; D-118): shared test support in `:core:design`'s test fixtures (`se.hagstedt.kvitt.design.testing`), `kvitt.android.screenshots` wires Roborazzi and the test libraries; `format/Failures.kt`, `format/Limits.kt`, `savedSplitLabel` in `:core:design`; dead code removed (`KvittMotion` only the seesaw spring, `KvittIcon.Description`, unused dimens and shapes); four catalog entries removed; one `CopyCategoryKeywords` task; one `configureAndroidModule`; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Landed since SPEC v0.17 (recorded in v0.18):
@@ -13,71 +15,61 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Server gap pass (`8660b7f`, D-113): `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; `push sent` info line; CLI tests (`invite-partner`, `migrate`, `healthcheck`, SIGTERM drain); `tools/contracts-check.py` 134 checks.
- Infra security (`7fb86f5`, D-117): Caddy log redaction of `/i/<code>` and query strings, CSP and three more response headers, Caddy non-root (uid 10002, read-only, caps dropped), `/certs` 0750, dev stack mounts only the FCM key, `/opt/kvitt/deployed-digests` with a WARNING on change, `kvitt_cicd` with `write:package` only, the plain root key (old variant B) not acceptable.
- M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116): S-16 lost version read-only, S-22 "<version> (schema <n>)" from `/health`, `SettlementRepository` returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, `localeFilters` en/sv, `:data` tests (429, D-040, WorkManager via `work-testing`), the home/account screenshot harness fix (stale `sv_dark` goldens re-recorded), 19 unused string keys removed (`strings.csv` 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4); M-1 and L-3 wait for the owner (§4). H-1 must be fixed before port 8443 is opened.
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed. H-1 must be fixed before port 8443 is opened.
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
- In flight: nothing.
- In flight:
- NPM rework (D-120 reserved, agent running): the owner runs Nginx Proxy Manager on the Home Assistant Pi, and NPM renews the Let's Encrypt certificate for `icehack.asuscomm.com`. Caddy and cert-sync are replaced by an NPM proxy host that forwards to the Kvitt Pi's server on LAN port 8080, so TLS terminates in NPM, the Kvitt Pi's compose stack loses `caddy` and `cert-sync`, and the `infra/pi/` bundle and its owner checklist change. Security review M-2 (the HA SSH key form for cert-sync) becomes moot and is closed. SPEC §10.5 and the other Caddy and cert-sync text are rewritten when D-120 lands (a docs pass records it; next free decision D-121). Open assumptions are §3 item 3.
- This docs pass (`agent-docs-v020`: SPEC v0.20, D-119, HANDOFF, `prompts/README.md`).
- `cargo-audit` in the server gate: the orchestrator is installing `cargo-audit` on the Mac; an agent adds it to the gate after the install.
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
- Remote: `origin` on Gitea, SSH key works; pushed at `c582363`. The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone.conf`, `[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`); the cert-sync key `infra/secrets/ha_ssh_key` exists (all git-ignored). The first push triggered the Gitea Actions workflow; its result is the owner's §3 item 1.
- Remote: `origin` on Gitea, SSH key works; pushed at `96e4cbd`. The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone.conf`, `[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`); the cert-sync key `infra/secrets/ha_ssh_key` exists (all git-ignored). The first push triggered the Gitea Actions workflow; its result is the owner's §3 item 1.
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both green on `main` as far as reported.
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the next install carries M8b to M8f, M12f and the Android overhaul.
- Disk: the MacBook had about 12 GB free during the v0.19 docs pass (2026-10-18); run `tools/clean-builds.sh` when it drops under about 5 GB.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server, with the build whose checks are §3 item 2; the release app `se.hagstedt.kvitt`, installed 2026-10-19, not yet paired (its checks are §3 item 2 as well).
- Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run `tools/clean-builds.sh` when it drops under about 5 GB.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-docs-v019` (orchestrator): SPEC v0.19, D-118, HANDOFF, `prompts/README.md`, design-spec §2.11 and the icon table, `infra/README.md` APK size, the `PLAN-*.md` hash remap.
- Land `agent-docs-v020` (orchestrator): SPEC v0.20, D-119, HANDOFF, `prompts/README.md`.
- Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit).
- AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- Security review L-6: an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1; owner, §4).
- Security review L-10: `forbidden` or `not_found` for the other user's device id (owner decision, §4).
- Tooling: `cargo-audit` in the server gate (agent, once the owner has installed it, §3 item 6).
- Tooling: `cargo-audit` in the server gate (agent, after the orchestrator's install on the Mac, in flight).
- State-class naming: `*Ui`, `*UiState` and `*State` are mixed across features; needs a decision if a convention is wanted (owner; no plan).
- Unverified risk: the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the first runs).
- Install the next debug build and have the owner look (§3 item 2); build a signed release for the smoke taps (§3 item 3).
- Owner runs the M2c-pi bundle on the Kvitt Pi (§3 item 5); record the accepted cert-sync key form and the router-forward answer in `infra/README.md`.
- Plans (SPEC §11.2 v0.19), in dependency order; prompt `prompts/<id>.md`:
- Land the NPM rework (D-120) when its agent reports, then a docs pass records D-120 and rewrites SPEC §10.5 and the Caddy and cert-sync text (orchestrator).
- The owner's answers to the phone checks of §3 item 2 (debug app checks; release app pairing with a device invite the orchestrator provides).
- Owner runs the Pi bundle on the Kvitt Pi: deferred by the owner on 2026-10-19 ("when it makes sense to move over to the Pi"); the bundle and its checklist are rewritten by D-120 first; record the router-forward answer in `infra/README.md` then (the cert-sync key-form question is closed).
- Plans (SPEC §11.2 v0.20), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle, server gap pass, infra overhaul, infra security, the server overhaul and the Android overhaul: landed.
- Next: a server image with the H-1 fix before the router forward; the owner runs the M2c-pi bundle on the Kvitt Pi (D-100); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
- Next: a server image with the H-1 fix before the router forward; the NPM rework (D-120) reshapes the Pi bundle; the owner runs it on the Kvitt Pi when he moves over (D-100, deferred); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
## 3. HUMAN steps pending (relay in chat)
1. Gitea, the repository, Actions: did the run "Build Kvitt images" triggered by the first push succeed (yes/no), and how long did it take?
2. Next install on the phone (yes/no each): screen changes are instant, with no fade or slide; Settings, Om Kvitt shows the server version as "… (schema 3)"; if Inställningar, Synkstatus lists a change under "Ändringar som inte synkades" (likely none), "Öppna" shows "Din version" with "Visa nuvarande version"; plus the earlier checks: the title bar sits below the status bar; the quick-row selected badge is whole; the invite-code field cursor stays after the last character; the expense editor shows Konto, Datum, Betalt av, Delning as four rows that do not move when a value changes; the recurring editor shows Konto, Betalt av, Delning as three such rows; "Household" fits its half of the Analytics filter bar.
3. Release build smoke taps: the app opens; S-1 or Home renders; Settings, Om Kvitt shows the version; add one expense and watch it sync.
4. Owner decisions from the security review (answer each):
1. M-1: encrypt the Drive backups through an rclone `crypt` remote and re-authorise rclone with the narrower `drive.file` scope (a restore then needs the crypt passwords)? yes/no.
2. M-2: which cert-sync key form did the Home Assistant add-on accept at Pi step 3: the restricted root key (variant A) or a dedicated non-root user? (A plain root key is not acceptable.)
3. L-3: a separate per-IP limit for APK downloads (`/apk/files/*`): which number per minute (assumed 10 per minute per IP)?
4. Gitea token: if the workflow's tag push fails with `kvitt_cicd` limited to `write:package`, may `write:repository` be added? yes/no.
5. Confirm the numbers the server overhaul assumed and built: pictures capped at 16 megapixels; `fcm_token` at most 512 characters. yes/no.
5. Pi, after the app is proven: the nine steps of `infra/README.md` "Pi: what the owner does":
1. Provision: Raspberry Pi Imager, Raspberry Pi OS Lite (64-bit), user, SSH key, time zone; static DHCP lease in the router.
2. Docker Engine from Docker's Debian arm64 repository; `docker run --rm hello-world` prints "Hello from Docker!".
3. Home Assistant, Settings, Add-ons, the SSH add-on, Configuration, `authorized_keys`: add the restricted cert-sync key line (variant A); if the add-on refuses it, a dedicated non-root user that can only read the certificate directory; never a plain root key. Question: which form was accepted?
4. On the Mac: `infra/pi/.env` from `.env.example`, every `CHANGE_ME` replaced; secrets staged into `infra/pi/secrets/`.
5. `scp` the bundle to the Pi, remove `infra/pi/secrets` on the Mac; on the Pi `ssh-keyscan` the Home Assistant host into `secrets/ha_known_hosts` (at least 1 line).
6. `./install.sh` on the Pi; expected end "Kvitt is running."; then remove `~/kvitt-pi/secrets`.
7. Router, WAN, Virtual Server / Port Forwarding: TCP 8443 to the Kvitt Pi's 8443 (only after the H-1 fix is deployed); on the phone with Wi-Fi off, `https://icehack.asuscomm.com:8443/health` shows `"status":"ok"` without a certificate warning (yes/no); then the `remote_ip` check of the README's operations table shows a public address (yes/no).
8. `/opt/kvitt/backup-now.sh` ends with "last-success: <stamp>"; `/opt/kvitt/bootstrap.sh` prints the admin invite code (first install only).
9. New versions: `/opt/kvitt/deploy.sh v1.0.N` after the Gitea workflow pushed that tag; it prints the image digests and warns if a recorded tag changed digest.
6. Install cargo-audit on the MacBook (`cargo install cargo-audit`) so it can join the server gate: yes/no.
1. Gitea, the repository, Actions: did the run "Build Kvitt images" triggered by the first push succeed (yes/no), and how long did it take? Parked by the owner ("come back to it"). If the run's tag push failed with `kvitt_cicd` limited to `write:package`: may `write:repository` be added (yes/no)?
2. Phone checks, answers pending (yes/no each):
1. Debug app (`se.hagstedt.kvitt.debug`): screen switches are instant, with no fade or slide; Inställningar, Om Kvitt shows the server version as "… (schema 3)"; the recurring editor shows Konto, Betalt av, Delning as three fixed rows.
2. Release app (`se.hagstedt.kvitt`, installed 2026-10-19): it opens to "Para ihop" without a crash; then pair it with the device invite the orchestrator provides; Home shows "Test"; add one expense and watch it sync.
3. NPM questions (for D-120; the agent proceeds on the assumptions): is `icehack.asuscomm.com` already a proxy host in Nginx Proxy Manager for another service (assumed free)? Does the router forward external 8443 to NPM's 443 or to a dedicated NPM port (assumed 443)?
4. Pi checklist, later: deferred by the owner ("when it makes sense to move over to the Pi"). The steps of `infra/README.md` "Pi: what the owner does" are rewritten by D-120 (no cert-sync key, the router forward goes to NPM) and relayed in chat when the owner moves over.
## 4. Owner questions (asked, unanswered)
- The §3 item 1 workflow result and duration.
- The five security-review decisions of §3 item 4 (M-1 backup encryption, M-2 key form, L-3 APK download rate, `write:repository`, the 16 MP and 512-character assumptions).
- §3 item 1: the workflow result and duration (parked), and `write:repository` if the tag push failed.
- §3 item 2: the debug and release app checks.
- §3 item 3: the two NPM questions (assumed: `icehack.asuscomm.com` free in NPM; external 8443 to NPM 443).
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
- Security review L-10: should a member get `not_found` instead of `forbidden` for a device id of the other user (hides whose it is)? As built and as the spec says: `forbidden`.
- §3 item 6: install `cargo-audit`, yes/no.
- State-class naming (`*Ui`, `*UiState`, `*State` mixed): fix one convention, or leave as built?
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-118 used, except D-008 and D-009 (never used); none reserved. Next free: **D-119**.
- Spec version: v0.19 written; next **v0.20**.
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.19.
- Decisions: D-001 to D-119 used, except D-008 and D-009 (never used); D-120 reserved by the running NPM agent. Next free: **D-121**.
- Spec version: v0.20 written; next **v0.21**.
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.20.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+1
View File
@@ -123,3 +123,4 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-116 | 2026-10-18 | Notifications are posted with `VISIBILITY_PRIVATE` and a public version carrying the generic "Kvitt" / "Open Kvitt to see it" (`notif_generic_title`, `notif_generic_body`), so a secure lock screen shows no description, amount, account or name (new AC-13.9; `NotificationPrivacyTest`). L-6 (a link can prefill any server on S-1) stays as built: S-1 opens its server row expanded when the link's server is not the release default, release builds require `https://`, and a paired phone ignores links (D-084); a confirmation step would need a design state and is not in v1 (OQ-6). Landed by M12f (`2bd6b58`). | Security review L-7 and L-6. | SPEC §4 (AC-2.6, AC-2.8, AC-13.9), §12 (OQ-6) |
| D-117 | 2026-10-18 | Infra security fixes, landed `7fb86f5`. (1) Caddy's access log and error log (`log default`) filter `request>uri` with ``regexp `^(/i/)[^?]+\|([?]).+` `${1}${2}REDACTED` `` (`wrap json`), so invite codes and the `?from=` name never reach a log; `Authorization` stays redacted by default. (2) Deferred response headers `Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"`, `X-Content-Type-Options nosniff`, `X-Frame-Options DENY`, `Referrer-Policy no-referrer`. (3) The `caddy` container runs as `10002:10002` with `group_add` 1000, `read_only`, `cap_drop: [ALL]` plus `cap_add: [NET_BIND_SERVICE]` (the image's binary has that file capability and cannot be exec'd without it), `no-new-privileges`, tmpfs `/config` and `/tmp`; `install.sh` sets `/certs` 0750 1000:1000 and `caddy_data` to 10002 (extends D-105). (4) The development stack mounts `infra/dev/secrets/` (git-ignored), into which `up.sh` copies only `fcm-service-account.json`. (5) `deploy.sh` and `install.sh` print each pulled image's digest and append `<UTC time> <KVITT_TAG> <image reference> <digest>` to `/opt/kvitt/deployed-digests`, with a WARNING when a recorded reference changed digest; the workflow prints the pushed digests. (6) `kvitt_cicd` needs only `write:package` (the tag push uses the checkout credentials). (7) The plain root key (variant B) for cert-sync is no longer acceptable: variant A (restricted) or a dedicated non-root user. (8) One-off check after the router forward that Caddy's `remote_ip` shows public addresses. | Security review at `18c527f`: L-1, L-2, L-5, L-9, M-2, I-5. | SPEC §8.7, §8.9, §10 intro, §10.2, §10.3, §10.5.1, §10.5.2, §10.10; `infra/pi/`, `infra/dev/`, `.gitea/workflows/build-images.yaml`, `infra/README.md` |
| D-118 | 2026-10-18 | Shared test support lives in `:core:design`'s test fixtures (`src/testFixtures`, package `se.hagstedt.kvitt.design.testing`: `Variant`, `TestClock`, `TestEnvironment`, `ScreenSnapshots` with capture modes `Root`/`Screen`/`ScreenWhenIdle`, `MainDispatcherRule`, `withMain`), consumed as `testImplementation(testFixtures(project(":core:design")))`; not a module, so the §6.2 graph is unchanged; the architecture check rejects test fixtures outside test configurations. The convention plugin `kvitt.android.screenshots` wires Roborazzi and the test libraries; feature build scripts carry no screenshot block. Shared failure texts (`format/Failures.kt`: `FAILURE_NETWORK`, `commonFailureText`), limits (`format/Limits.kt`: `BP_TOTAL`, `DISPLAY_NAME_MAX`, `DESCRIPTION_MAX`, `NOTES_MAX`, `FIRST_DATE`, `DAYS_AHEAD_MAX`) and `savedSplitLabel` live in `:core:design`. `KvittMotion` holds only the seesaw spring; `KvittIcon.Description` and unused dimens/shapes removed; catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose`, `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task shared by `:data` and `:core:fakes`. 941 tests, no golden changed, release APK 10.46 MB. Landed by the Android overhaul (`e4e6e5d` to `c582363`). | Eight modules each had a copy of the same screenshot harness, and M12f had to fix the same capture bug in two of them; test fixtures rather than a `:core:testing` module let `:core:design`'s own tests use them, add no module to the §6.2 graph and are treated by lint as test sources. The failure-text map, the limits (`BP_TOTAL` in five copies) and the saved-split label were duplicated across features; the removed tokens, icon and catalog entries were never referenced. | SPEC §6.2 (`:core:design` row, Screenshot tests), §11.1, §11.3; `docs/ux/design-spec.md` §2.11 and the icon table; `android/README.md` |
| D-119 | 2026-10-19 | APK download rate limit. E-6 `GET /apk/files/{file_name}` is limited to 10 requests per minute per client IP in its own fixed window (`APK_DOWNLOAD_LIMIT` in `src/api/rate_limit.rs`), separate from and not counted against the 60 per minute of the other unauthenticated endpoints; the client IP as in D-032 (peer, or the last `X-Forwarded-For` entry from the trusted proxy); refusals are 429 `rate_limited` with `Retry-After`; every request counts, a 404 included; in memory, pruned like the other windows. E-5 `GET /apk/latest` and E-4 `GET /i/{code}` stay on the 60 per minute. Landed `96e4cbd` (unit test and an integration test in `tests/core_api/rate_limit.rs`). Owner: "APK download limit assumed is OK". | Security review L-3: each E-6 answer can be a ~10 MB APK, so 60 per minute per IP let one client pull about 600 MB a minute from the Pi; E-5 and E-4 answers are small and the phone checks E-5 at most every 6 hours (AC-16.2). | SPEC §8.6, §10.6, §14.3.4 (E-6), §11.4 |