Files
channel-exit/README.md
T

71 lines
2.4 KiB
Markdown
Raw Normal View History

2026-08-18 15:41:28 -04:00
# channel-exit
The estate's one way out to a person's inbox or phone.
Services that need to reach a human POST here instead of carrying a SendGrid
key of their own. Credentials live in one place, one service can be turned off
or rate-limited, and a bug in some worker cannot become a bill.
## The contract
```
POST /send
Authorization: Bearer <CHANNEL_EXIT_TOKEN>
Content-Type: application/json
{"channel": "email|sms|whatsapp|push", "to": "...", "subject": "...", "body": "..."}
```
| Reply | When |
| --- | --- |
| `200` | the provider accepted it — `{"ok": true, "provider_status": 202, "id": "..."}` |
| `400` | unknown channel, or `to` is not an email / not E.164, or nothing to send |
| `401` | missing or wrong bearer token |
| `501` | `whatsapp` and `push` — no provider for them in this estate |
| `502` | the provider refused or was unreachable; its status is in the body |
| `503` | that channel's credentials are not configured here |
`GET /healthz` needs no token and reports which channels are wired.
The shape is fixed by its first consumer, `reminders-service`
(`app/alerts.py: gateway_sender`), which treats any 2xx as delivered.
## Two rules
**It listens on loopback and the docker bridge, and nowhere else.** An
authenticated relay reachable from the internet is an open spam relay the
moment the token leaks — and that token is copied into every consumer's
environment. It gets no `0.0.0.0` binding and no cloudflared route, ever.
**It does not log what it was asked to send.** The log records channel, a
redacted address, the provider's status and message id. Never a subject, never
a body, never a credential.
## Consumers
From the host: `http://127.0.0.1:<CHANNEL_EXIT_PORT>/send`
From another container: `http://172.17.0.1:<CHANNEL_EXIT_PORT>/send` — a
consumer's own `127.0.0.1` is that consumer, so a loopback gateway URL fails
every delivery while looking correctly configured.
## Running it
```sh
cp .env.example .env && chmod 600 .env # fill in, then
docker compose up -d --build
```
Every value in `.env` is required except `PROVIDER_TIMEOUT`. `CHANNEL_EXIT_TOKEN`
has no default: the service refuses to start without one rather than coming up
as an unauthenticated relay.
## Tests
```sh
pip install -r requirements-dev.txt && python -m pytest -q
```
The suite replaces `urllib.request.urlopen` with something that raises, so no
test can send a real message however it is written.