SPEC v0.22: D-122 S-22's release server and publish-apk host; D-123 no built-in server host; Kvitt host name
D-122 (4a4e45f,4b1ae0c): AppBuildInfo.releaseServerUrl, S-22 before pairing, publish-apk.sh reads KVITT_PUBLIC_HOST from infra/pi/.env. D-123 (c9abd24,e381e37): KVITT_PUBLIC_URL required, KVITT_TRUSTED_PROXY defaults to none, OpenAPI server https://{host}:8443, E-7/E-12 examples on kvitt.example.invalid. The owner's host name kvitt.suorra.duckdns.org recorded in HANDOFF (SPEC §10.1, §10.5.1 point there). HANDOFF and prompts/README.md status atac17b41. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+4
-2
@@ -124,5 +124,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| 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.~~ (revised by D-120: NPM's `location ^~ /i/` without access or error log; NPM logs no request headers) (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` (revised by D-120: set at NPM's server level with the server's copies hidden). (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).~~ (removed by D-120) (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.~~ (moot since D-120: no Home Assistant SSH key) (8) ~~One-off check after the router forward that Caddy's `remote_ip` shows public addresses.~~ (revised by D-120: the conntrack check of SPEC §10.10) | 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 |
|
||||
| D-120 | 2026-10-19 | TLS terminates in the owner's Nginx Proxy Manager (NPM) on the Home Assistant Pi instead of Caddy and cert-sync, which are removed (`infra/cert-sync/**`, `infra/pi/Caddyfile`, images, volumes `certs` and `caddy_data`, secrets `ha_ssh_key` and `ha_known_hosts`, `.env` keys `HA_PORT`, `HA_USER`, `HA_CERT_DIR`). (1) Kvitt gets its own public host name `KVITT_PUBLIC_HOST` (owner-chosen, recorded in HANDOFF; a DuckDNS name is being set up), because `icehack.asuscomm.com` is NPM's proxy host for Home Assistant and NPM selects by name. `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}:8443`. A path prefix on another name is not supported. (2) The router forwards external TCP 8443 to the Home Assistant Pi's 443 (NPM); nothing is forwarded to the Kvitt Pi. NPM issues the name's Let's Encrypt certificate via HTTP-01 on port 80. (3) The Kvitt Pi runs `server` (publishes 8080 on all LAN interfaces, hardening unchanged) and `backup` (unchanged, D-105). NPM forwards plain HTTP over the LAN (accepted by the owner). (4) `KVITT_TRUSTED_PROXY=${HA_HOST}`, NPM's LAN IPv4 address, checked by `install.sh`; the server is unchanged and takes the last `X-Forwarded-For` entry from that peer. (5) The NPM proxy-host settings and the Custom Nginx Configuration of §10.5.2 carry over D-117's headers and log redaction: headers at server level with the server's copies hidden; its own `location /` without `add_header` (NPM's default adds `X-Served-By`, which makes nginx drop server-level headers); `location ^~ /i/` with `access_log off` and `error_log /dev/null crit` because NPM's access log writes `$request_uri` with the query string; `client_max_body_size 6m`; `proxy_read_timeout 60s`; HSTS on (NPM's 2 years). (6) Health checks: `http://localhost:8080/health` on the Pi, then through NPM (warning only); `install.sh` removes the old stack's leftovers. (7) Gitea login: the secret is interpolated in the `run` line (`printf '%s' "${{ secrets.kvitt_cicd }}" \| docker login …`), the owner's proven form, because the env-based form came through empty on act_runner 0.6.1 with a user-level secret; this reverses L-8's 'never expanded inside a shell line'; act_runner masks the value. Revises D-103 (bundle files), D-104 (images, Caddy pin, cert-sync packages, trigger path), D-106 (cert-sync part), D-117 parts 1, 2, 3, 7, 8, D-023's cert-sync secrets. Verified on the Mac 2026-10-19: nginx -t against NPM v2.16.0 templates; rehearsal server-behind-nginx 21 checks passed (headers once each, HSTS, http→https 301, 413 at 7 MB, rate limit per forwarded client, forged X-Forwarded-For contained, invite code/name/`/i/` absent from nginx logs including on 502). | Owner 2026-10-19: NPM already manages Let's Encrypt; the HA name is taken; 8443 → NPM 443 confirmed. | SPEC §8.5, §8.7, §8.9, §10 intro, §10.1, §10.2, §10.3, §10.4, §10.5, §10.7, §10.10, §11.2, §14.7 |
|
||||
| D-121 | 2026-10-19 | The Android app's public host is a build property. `kvitt.publicHost` is read from the Gradle property (`-Pkvitt.publicHost=…`), else the key `kvitt.publicHost` in `android/local.properties` (owner's, git-ignored), else the placeholder `kvitt.example.invalid` (RFC 2606; never resolves, so a release built without it cannot reach a real host). The build refuses a value that is not a host name. `:app` exposes it as `BuildConfig.PUBLIC_HOST`, as the manifest placeholder `publicHost` (the AC-2.6 `https` intent filter, port 8443, `/i/`, `autoVerify="false"`) and as `RELEASE_SERVER_URL` = `https://<host>:8443` (`link/ReleaseServer.kt`). `:feature:onboarding` receives that URL through the Hilt qualifier `@ReleaseServerUrl` for S-1's prefill and its non-default-server check. Invite-link parsing still accepts a server named by the link (AC-2.6). Tests and goldens use fixed fixture hosts. `infra/publish-apk.sh` passes `-Pkvitt.publicHost` from `KVITT_PUBLIC_HOST` (follow-up agent running). The pinned-root config's domain (D-082) is still the old literal and must follow the property if that fallback is ever prepared. Landed `d090c6c`. | D-120 gave Kvitt its own host name; it was hard-coded in three places. | SPEC AC-2.6, AC-2.7, §8.5, §10.5.3, §10.6, §13.3 S-1, §12 OQ-6; `android/README.md` |
|
||||
| D-120 | 2026-10-19 | TLS terminates in the owner's Nginx Proxy Manager (NPM) on the Home Assistant Pi instead of Caddy and cert-sync, which are removed (`infra/cert-sync/**`, `infra/pi/Caddyfile`, images, volumes `certs` and `caddy_data`, secrets `ha_ssh_key` and `ha_known_hosts`, `.env` keys `HA_PORT`, `HA_USER`, `HA_CERT_DIR`). (1) Kvitt gets its own public host name `KVITT_PUBLIC_HOST` (owner-chosen, recorded in HANDOFF; ~~a DuckDNS name is being set up~~ the owner chose `kvitt.suorra.duckdns.org` on 2026-10-19), because `icehack.asuscomm.com` is NPM's proxy host for Home Assistant and NPM selects by name. `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}:8443`. A path prefix on another name is not supported. (2) The router forwards external TCP 8443 to the Home Assistant Pi's 443 (NPM); nothing is forwarded to the Kvitt Pi. NPM issues the name's Let's Encrypt certificate via HTTP-01 on port 80. (3) The Kvitt Pi runs `server` (publishes 8080 on all LAN interfaces, hardening unchanged) and `backup` (unchanged, D-105). NPM forwards plain HTTP over the LAN (accepted by the owner). (4) `KVITT_TRUSTED_PROXY=${HA_HOST}`, NPM's LAN IPv4 address, checked by `install.sh`; the server is unchanged and takes the last `X-Forwarded-For` entry from that peer (the server's built-in defaults, the OpenAPI server URL and the E-7/E-12 examples that still named the old host: revised by D-123). (5) The NPM proxy-host settings and the Custom Nginx Configuration of §10.5.2 carry over D-117's headers and log redaction: headers at server level with the server's copies hidden; its own `location /` without `add_header` (NPM's default adds `X-Served-By`, which makes nginx drop server-level headers); `location ^~ /i/` with `access_log off` and `error_log /dev/null crit` because NPM's access log writes `$request_uri` with the query string; `client_max_body_size 6m`; `proxy_read_timeout 60s`; HSTS on (NPM's 2 years). (6) Health checks: `http://localhost:8080/health` on the Pi, then through NPM (warning only); `install.sh` removes the old stack's leftovers. (7) Gitea login: the secret is interpolated in the `run` line (`printf '%s' "${{ secrets.kvitt_cicd }}" \| docker login …`), the owner's proven form, because the env-based form came through empty on act_runner 0.6.1 with a user-level secret; this reverses L-8's 'never expanded inside a shell line'; act_runner masks the value. Revises D-103 (bundle files), D-104 (images, Caddy pin, cert-sync packages, trigger path), D-106 (cert-sync part), D-117 parts 1, 2, 3, 7, 8, D-023's cert-sync secrets. Verified on the Mac 2026-10-19: nginx -t against NPM v2.16.0 templates; rehearsal server-behind-nginx 21 checks passed (headers once each, HSTS, http→https 301, 413 at 7 MB, rate limit per forwarded client, forged X-Forwarded-For contained, invite code/name/`/i/` absent from nginx logs including on 502). | Owner 2026-10-19: NPM already manages Let's Encrypt; the HA name is taken; 8443 → NPM 443 confirmed. | SPEC §8.5, §8.7, §8.9, §10 intro, §10.1, §10.2, §10.3, §10.4, §10.5, §10.7, §10.10, §11.2, §14.7 |
|
||||
| D-121 | 2026-10-19 | The Android app's public host is a build property. `kvitt.publicHost` is read from the Gradle property (`-Pkvitt.publicHost=…`), else the key `kvitt.publicHost` in `android/local.properties` (owner's, git-ignored), else the placeholder `kvitt.example.invalid` (RFC 2606; never resolves, so a release built without it cannot reach a real host). The build refuses a value that is not a host name. `:app` exposes it as `BuildConfig.PUBLIC_HOST`, as the manifest placeholder `publicHost` (the AC-2.6 `https` intent filter, port 8443, `/i/`, `autoVerify="false"`) and as `RELEASE_SERVER_URL` = `https://<host>:8443` (`link/ReleaseServer.kt`). `:feature:onboarding` receives that URL through the Hilt qualifier `@ReleaseServerUrl` for S-1's prefill and its non-default-server check. Invite-link parsing still accepts a server named by the link (AC-2.6). Tests and goldens use fixed fixture hosts. `infra/publish-apk.sh` passes `-Pkvitt.publicHost` from `KVITT_PUBLIC_HOST` (~~follow-up agent running~~ landed `4b1ae0c`, D-122; S-22's fallback server also D-122). The pinned-root config's domain (D-082) is still the old literal and must follow the property if that fallback is ever prepared. Landed `d090c6c`. | D-120 gave Kvitt its own host name; it was hard-coded in three places. | SPEC AC-2.6, AC-2.7, §8.5, §10.5.3, §10.6, §13.3 S-1, §12 OQ-6; `android/README.md` |
|
||||
| D-122 | 2026-10-19 | S-22's server before pairing comes from the build (extends D-121). `AppBuildInfo` (defined in `:data` `di/DataModule.kt`, provided by `:app` in `di/AppBuildInfoModule.kt`, consumed by `:data`) gains `releaseServerUrl: String`, filled from `RELEASE_SERVER_URL` = `https://<kvitt.publicHost>:8443`: `data class AppBuildInfo(val versionCode: Int, val versionName: String, val debug: Boolean, val releaseServerUrl: String)`. S-22's server row shows the paired server's base URL; while none is stored it shows `AppBuildInfo.releaseServerUrl` (`AboutRepositoryImpl`'s `releaseServerUrl`, was `defaultServerUrl`); `DataModule.DEFAULT_SERVER_URL` (`https://icehack.asuscomm.com:8443`) is removed, so `:data` names no host. Fakes and tests use `https://kvitt.example.org:8443` (`FakeScenarios`, `SettingsScenarios`, `RepositoriesTest`; 30 S-21/S-22 goldens re-recorded with that fixture host, only the host changed); new test `about_unpaired_showsTheReleaseServerOfTheBuild`. `infra/publish-apk.sh` reads `KVITT_PUBLIC_HOST` from `infra/pi/.env` without sourcing it (refuses a missing file or key, `CHANGE_ME`, a value that is not a host name such as a URL, or the old name `icehack.asuscomm.com`, the checks of `install.sh`), passes `-Pkvitt.publicHost` to `:app:assembleRelease` and prints the release server in its header and summary. Landed `4a4e45f` (Android) and `4b1ae0c` (`publish-apk.sh`, `infra/README.md`). | D-121 left S-22's unpaired fallback on Home Assistant's host and `publish-apk.sh` without the host, so a release could be built for the placeholder. | SPEC §6.2, §10.6, §13.3 S-22, §14.2.4 |
|
||||
| D-123 | 2026-10-19 | The server has no built-in Kvitt host (revises D-120's leftovers). (1) `KVITT_PUBLIC_URL` has no default (`DEFAULT_PUBLIC_URL` removed from `config.rs`): unset or empty, every `kvitt-server` command exits 1 with a configuration error (`configuration: KVITT_PUBLIC_URL is not set …`) before opening the database; the Pi compose sets `https://${KVITT_PUBLIC_HOST}:8443`, the dev stack `http://<Mac LAN address>:<port>` (`infra/dev/up.sh`; compose fallback `http://localhost:8080`). (2) `KVITT_TRUSTED_PROXY` defaults to `none` (`NO_TRUSTED_PROXY`, ignoring case and surrounding white space): no peer trusted and the value never looked up; empty or unresolvable also means no proxy (D-022 unchanged); compose sets `HA_HOST`. (3) The OpenAPI server is `https://{host}:8443` with server variable `host`, default `kvitt.example.invalid` (RFC 6761 reserved), description 'the owner-chosen Kvitt host name, SPEC §10.5', in `contracts/openapi.yaml` and the generated document (`openapi.rs` `PUBLIC_SERVER_URL`, `HOST_VARIABLE_DEFAULT`); operations unchanged (representational for §14.8). (4) Examples E-7 `public_url` and E-12 `link` use `https://kvitt.example.invalid:8443`. `tools/contracts-check.py` expects the `{host}` URL (`e381e37`). Tests: CLI `every_command_refuses_to_start_without_a_public_url` (`serve`, `bootstrap`, `migrate`; no database created), config `public_url_is_required`, rate limiter `the_default_none_trusts_no_peer`, `an_unresolvable_name_trusts_no_peer`, `an_ip_literal_is_trusted`; 276 server tests. `server/README.md` updated. Landed `c9abd24`, `e381e37`. | Since D-120 the old name `icehack.asuscomm.com` is Home Assistant's; a wrong built-in host is worse than none, and `caddy` named a container that no longer exists. | SPEC §10.4, §11.3, §14.3, §14.3.4 (E-7, E-12) |
|
||||
|
||||
Reference in New Issue
Block a user