Files
mopac-pmo/archive/REPORT-20260829-1700-discourse-cli.md
T

77 lines
4.0 KiB
Markdown

# REPORT — mopac-discourse-go: Redmine 507 surface complete
Task: TASK-20260829-1700 — Discourse Go client (Redmine #507)
Delivered: 2026-08-29 17:00 turn
Repo: https://git.knownelement.com/UKRRS/mopac-discourse-go (existing,
AGPLv3 since first commit)
Clone: ~/projects/meta/MOPAC/discourse-go (origin main, 9 commits, pushed)
Status: **Redmine 507 scope complete, green, pushed. Feeds the 495
briefing delivery + fleet verification.**
No Redmine writes in this turn (PMO owns the ticket, per directive).
## Starting point
The repo already carried a clean v0 (categories list/create, topics
create/list/latest/get, posts create/update/get, whoami, raw JSON
passthrough, typed error classes, fake-server tests, container smoke) —
committed and pushed earlier today. This turn built on it rather than
resetting: v0 was green, standard-shaped and already on origin main.
## What landed this turn (8 commits, TDD red/green each)
| Commit | Content |
|---|---|
| `f51ca59` | style: gofmt tag alignment the v0 files missed |
| `14cdaa0` | feat: search — `GET /search.json` grouped hits (posts/topics/users/categories), term operators pass through |
| `6891f3f` | feat: private-message send — `POST /posts.json` with comma-joined `target_usernames`/`target_group_names`/`target_emails` |
| `5bbb307` | feat: webhook + SSO helpers — constant-time HMAC verification, `WebhookHandler`, `VerifySSO`/`BuildSSOResponse`/`SSOLogin`, env-only secret loaders |
| `5b95ac3` | feat: `DISCOURSE_KEY` env alias (explicit `DISCOURSE_API_KEY` wins) |
| `f133027` | feat(cli): `search`, `messages send`, `webhook verify`, `sso verify` |
| `445df7a` | test: smoke coverage for all of the above (real CLI vs containerized fake) |
| `bb96849` | docs: README + env.example to standard |
## Surface (Redmine 507 checklist)
- **category/topic/post list+create** — v0, unchanged, green.
- **search** — `Client.Search(term, page)` + `discourse-go search TERM
[-page N]`; url-encodes the term (no query smuggling), decodes the
four hit groups; typed errors preserved.
- **private-message send** — `Client.SendMessage(title, raw, targets)` +
`discourse-go messages send -title T -to u1,u2 [-group] [-email]`;
targets comma-joined on the wire exactly as Discourse expects;
undeliverable recipients surface as `ErrForbidden`.
- **webhook-helper** — `VerifyWebhook` (sha256-HMAC hex, `sha256=`
prefix tolerated, constant-time), `ParseWebhook`/`WebhookHandler`
(403 before dispatch), `webhook verify SIG` CLI (body on stdin, exit
0/2). Secret: `DISCOURSE_WEBHOOK_SECRET`.
- **SSO secret handling** — `VerifySSO` (HMAC over the base64 string),
`BuildSSOResponse`, `SSORedirectURL`, one-call `SSOLogin`, plus
`sso verify SSO SIG` CLI that prints the decoded payload. Secret:
`DISCOURSE_SSO_SECRET`.
- **env config** — `DISCOURSE_URL` / `DISCOURSE_API_KEY` (alias
`DISCOURSE_KEY`) / `DISCOURSE_API_USERNAME`; secrets env-only, never
flags, never logged (redaction sweep in the smoke greps all output).
## Verification
- `./dev.sh check` (build + vet + test inside the digest-pinned
`golang:1.26-bookworm` Docker builder): **green** — 24 library tests
across search/messages/webhook/SSO/env-alias plus the v0 suite.
- `./dev.sh smoke` (real CLI binary vs fake Discourse in a container on
127.0.0.1:8610): **smoke: OK** — new flows cover search hits, PM send
with recipient assertion, webhook good-sig=0/bad-sig=2, SSO
decode + tamper rejection; no live forum contacted.
- gofmt clean; working tree clean; `origin/main` at `bb96849`.
## Notes for the fleet
- `WebhookHandler`/`SSOLogin` are importable by the fleet's receiver
service — no shell-out, no third-party modules, stdlib crypto only.
- Fake server records PM deliveries (`/__pms.json` on the smoke fake)
so downstream delivery assertions can be scripted.
- Live-verification status unchanged from v0: reads OK against
community.turnsys.com with the current key; category create waits on
the admin-scoped key (Charles's 1900 list); everything new this turn
is exercised against the fake only, per no-live-writes discipline.