feat(cli): search, messages send, webhook verify, sso verify

The CLI now covers the whole Redmine 507 surface: `search TERM
[-page N]` (operators pass through quoted), `messages send -title
-to user1,user2 [-group/-email] [-file|-raw]`, plus the two secret
helpers `webhook verify SIG` (delivery body on stdin, exit 0 only
on a valid HMAC) and `sso verify SSO SIG` (prints the decoded
Discourse Connect payload as JSON). The helpers run before client
construction so they work with only their secret env set, no
instance credentials needed. Usage text lists the new env vars.

Part of Redmine 507 (Discourse Go client).
This commit is contained in:
2026-08-29 16:50:31 -05:00
parent 5b95ac3c7e
commit f13302778b
3 changed files with 144 additions and 1 deletions
+70
View File
@@ -0,0 +1,70 @@
// Secret-helper commands: they never touch an instance, so they run
// before client construction and read only their own env secrets.
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"git.knownelement.com/ukrrs/mopac-discourse-go"
)
// runWebhook implements `webhook verify SIG`: the raw delivery body on
// stdin, the X-Discourse-Event-Signature value as the argument, the
// secret from DISCOURSE_WEBHOOK_SECRET. Exit 0 = signature valid.
func runWebhook(args []string) int {
if len(args) != 2 || args[0] != "verify" {
fmt.Fprintf(os.Stderr, "webhook needs: verify SIG (delivery body on stdin)\n\n%s", usage)
return 1
}
secret, err := discourse.WebhookSecretFromEnv()
if err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
return 1
}
body, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: read stdin: %v\n", err)
return 1
}
if err := discourse.VerifyWebhook(secret, body, args[1]); err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
return 2
}
fmt.Printf("verified %d bytes\n", len(body))
return 0
}
// runSSO implements `sso verify SSO SIG`: checks a Discourse Connect
// login redirect's sso/sig pair against DISCOURSE_SSO_SECRET and prints
// the decoded payload as JSON. Exit 0 = signature valid.
func runSSO(args []string) int {
if len(args) != 3 || args[0] != "verify" {
fmt.Fprintf(os.Stderr, "sso needs: verify SSO SIG (sso+sig from the login URL)\n\n%s", usage)
return 1
}
secret, err := discourse.SSOSecretFromEnv()
if err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
return 1
}
vals, err := discourse.VerifySSO(secret, args[1], args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: %v\n", err)
return 2
}
obj := map[string]string{}
for k, v := range vals {
obj[k] = strings.Join(v, ",")
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(obj); err != nil {
fmt.Fprintf(os.Stderr, "discourse-go: encode payload: %v\n", err)
return 1
}
return 0
}