Keep neoirc-cli sessions on localhost and test the relay goroutine stop
check / check (push) Failing after 5s

The auth cookie is always Secure, and Go 1.24's cookie jar sends a
Secure cookie only over HTTPS, so neoirc-cli lost its session against a
plain-HTTP server on localhost. Its jar now treats a server on localhost
as HTTPS, as curl does. The README's Transport Security section and
Python example say what a client needs.

A test checks that closing a registered IRC client's connection stops
its relay goroutine. The README Entrypoints section names a make target
only for the scripts that have one, and the Dockerfile says why the
test phase runs with -p 4.

Model: opus-5-5
This commit is contained in:
2026-10-06 14:13:31 +00:00
parent 60c0d4b5e3
commit 58d8f5cc94
5 changed files with 193 additions and 12 deletions
+19 -9
View File
@@ -164,8 +164,9 @@ for multi-client access.
cryptographically random value (64 hex characters) and returns the user ID and
nick in the JSON response body. No auth credential appears in the JSON body.
- The auth cookie is HttpOnly, SameSite=Strict, and Secure: clients send it only
over HTTPS, which the TLS-terminating reverse proxy provides. Browsers handle
cookies automatically. **CLI clients (curl, custom HTTP clients) must
over HTTPS, which the TLS-terminating reverse proxy provides, or to a server
on `localhost` (see [Transport Security](#transport-security)). Browsers
handle cookies automatically. **CLI clients (curl, custom HTTP clients) must
explicitly save and send cookies** — e.g., using curl's `-c`/`-b` flags or an
HTTP cookie jar in their language's HTTP library.
- Sessions start anonymous — no password required. When the session expires or
@@ -2092,9 +2093,14 @@ PGP/DKIM — the mail server sees everything, but signatures prove authenticity.
### Transport Security
- **HTTPS is strongly recommended** for production deployments. The server
itself serves plain HTTP — use a reverse proxy (nginx, Caddy, etc.) for TLS
termination.
- **Clients need HTTPS to keep a session.** The auth cookie is always `Secure`,
and clients send a `Secure` cookie only over HTTPS. The server itself serves
plain HTTP — use a reverse proxy (nginx, Caddy, etc.) for TLS termination.
- **A server on `localhost`**, as in this document's examples, also works over
plain HTTP with curl, `neoirc-cli`, Chrome and Firefox, which treat
`localhost` as secure. Safari does not. Python's `requests` does not either,
so the [Python example](#implementing-long-poll-in-code) sends the cookie
itself.
- **CORS**: The server allows all origins with credentials
(`Access-Control-Allow-Credentials: true`), reflecting the request Origin.
This enables cookie-based auth from cross-origin clients. Restrict origins in
@@ -2428,7 +2434,8 @@ docker run -d \
This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard. Each script below has a `make` target of the same name that calls it,
except `script/install-precommit`, which is `make hooks`.
except `script/install-precommit`, which is `make hooks`, and `script/cibuild`,
`script/precommit` and `script/projectname`, which have none.
- `script/bootstrap`: installs what development needs: make, git, Node and yarn
(for prettier), and Go.
@@ -2625,16 +2632,19 @@ curl -s -b cookies.txt -X POST http://localhost:8080/api/v1/messages \
The key to real-time messaging is the poll loop. Here's the pattern:
```python
# Python example — using requests.Session for automatic cookie handling
# Python example — using requests.Session for cookie handling
import requests, json, time
BASE = "http://localhost:8080/api/v1"
session = requests.Session() # Manages cookies automatically
session = requests.Session()
last_id = 0
# Create session (cookie set automatically via Set-Cookie header)
# Create session (the server sets the auth cookie via Set-Cookie)
resp = session.post(f"{BASE}/session", json={"nick": "pybot"})
print(f"Session: {resp.json()}")
# The auth cookie is Secure, and requests sends it only over HTTPS.
# For a plain-HTTP server on localhost, send it on every request:
session.headers["Cookie"] = f"neoirc_auth={resp.cookies['neoirc_auth']}"
# Join channel
session.post(f"{BASE}/messages",