# 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.