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

4.0 KiB

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.
  • searchClient.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 sendClient.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-helperVerifyWebhook (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 handlingVerifySSO (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 configDISCOURSE_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.