docs: pin the rate-limit source and the webhook recipe (#43)

User-Visible: no
Issue: #43
This commit is contained in:
Codex
2026-09-02 00:05:36 +00:00
committed by claude[bot]
parent d9c7c3df1c
commit 3bbe555ad8
2 changed files with 37 additions and 0 deletions
+10
View File
@@ -443,6 +443,13 @@ if they are unavailable, #43 receives `blocked` without weakening the contract.
- attachment parsed and checked for format/version and top-level allowlist;
- message/contact rendered as escaped plain text only;
- 5 attempts/hour and 20/day per source address plus a global circuit breaker;
- **the source address is the one supplied by the trusted proxy, never one the
client can choose.** The relay reads the *last* element of `X-Forwarded-For`,
because the first element is whatever the caller sent, and the reverse proxy in
front of it is configured to overwrite the header outright rather than append
to it. Both halves are required: without them a caller picks its own rate-limit
bucket by changing one line of the request, and every other limit in this
section becomes decorative;
- source IP is used only through a daily-keyed rate-limit hash with ≤24 h TTL;
raw address is not written to app logs/storage/mail;
- idempotency key retained 24 h and returns the original report id;
@@ -529,6 +536,7 @@ parse them; user message/contact remain verbatim plain text.
| AC10 | Expired/replaced/discarded tokens fail; config changes after preview do not change cached bytes. | Fake-clock backend tests. |
| AC11 | Submit is HTTPS/fixed-host/no-redirect, bounded and idempotent; logs contain no message/contact/body. | Stub aiohttp server + caplog + SSRF/redirect tests. |
| AC12 | Relay enforces schema, size, hash, idempotency and rate limits; HTML remains inert plain text. | Receiver unit/integration suite with a recording fake provider. |
| AC12a | A caller cannot select its own rate-limit bucket: requests carrying different forged `X-Forwarded-For` values land in one source key, and the proxy configuration overwrites the header. | Receiver test plus the deployed proxy fragment under `scripts/support-relay/deploy/`. |
| AC13 | Success shows stable report id; timeout/error preserves form and exposes retry/manual recovery without claiming success. | Browser smoke across success/429/timeout/unknown command. |
| AC14 | Phone/tablet dialog, keyboard/focus and 44 px target satisfy View touch/accessibility contract. | Reviewed desktop + phone + tablet goldens and touch smoke. |
| AC15 | No existing backup/diagnostics/preflight behavior or payload changes. | Existing targeted frontend/backend suites unchanged. |
@@ -581,6 +589,8 @@ Mutation gate must prove at least:
- authorization guard removed → backend test red;
- redirect enabled/arbitrary URL accepted → SSRF test red;
- relay skips hash/rate/idempotency check → receiver tests red;
- relay trusts the client-supplied end of `X-Forwarded-For` (first element
instead of last) → `test_client_cannot_pick_its_own_rate_bucket` red;
- success shown on timeout → browser smoke red.
## 15. Performance and reliability budgets
+27
View File
@@ -151,6 +151,33 @@ sudo systemctl restart hp-support-relay@prod
Ротация вебхука — это ещё и правка `webhook_id` в автоматизации Home Assistant
«House Plan: приёмщик обратной связи → личка»: адрес и есть ключ.
**Минимальная конфигурация той автоматизации** — на случай, если её придётся
пересоздать (она живёт в чужом Home Assistant, не в этом репозитории):
```yaml
alias: "House Plan: приёмщик обратной связи → личка"
mode: queued
max: 10
triggers:
- trigger: webhook
webhook_id: "<тот же идентификатор, что в webhook.url>"
allowed_methods: [POST]
local_only: false # relay вызывает её снаружи
conditions:
- condition: template
value_template: "{{ trigger.json.source == 'houseplan-support-relay' }}"
actions:
- action: telegram_bot.send_message
data:
entity_id: notify.<получатель>
parse_mode: plain_text # текст пишет посторонний человек
disable_web_page_preview: true
message: "{{ trigger.json.text }}"
```
`parse_mode: plain_text` здесь не косметика: без него сообщение пользователя
разбирается как разметка и может подделать вид всего уведомления.
**Прочитать обращение**
```bash