events: verify, normalize, append-only store with provider-id dedup
Signature verification (gitea hex HMAC-SHA256 over the raw body via constant-time hmac.Equal; redmine/discourse constant-time shared-secret headers) with one generic ErrUnverified so rejects give attackers no oracle. Tolerant normalization of the known Redmine/Discourse/Gitea payload variants into one Event record (canonical subject ids, actor, title, repo, sha256 payload digest) plus the DESIGN action mapping (dispatch_turn / respond_turn / pipeline_step / ignore). Store: JSONL under state dir, 0600, dedup keyed on provider event id (delivery header, payload-digest fallback), index rebuilt at startup so replays across restarts still dedup; torn tail lines skipped, not fatal.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// Package events is the `harness events` webhook receiver (DESIGN "Events
|
||||
// are V1 scope"): Redmine, Discourse and Gitea webhooks land here, are
|
||||
// verified against per-source secrets, normalized into one internal Event
|
||||
// record, persisted append-only (JSONL, dedup by provider event id), and
|
||||
// mapped to a conductor action. Secret material is never logged and never
|
||||
// persisted; only payload digests are.
|
||||
package events
|
||||
|
||||
import "time"
|
||||
|
||||
// Provider sources.
|
||||
const (
|
||||
SourceRedmine = "redmine"
|
||||
SourceDiscourse = "discourse"
|
||||
SourceGitea = "gitea"
|
||||
)
|
||||
|
||||
// Actions an event can map to (DESIGN trigger -> action semantics).
|
||||
const (
|
||||
ActionDispatchTurn = "dispatch_turn" // redmine issue update/note -> run a turn for that stack
|
||||
ActionRespondTurn = "respond_turn" // discourse post reply -> context update + response turn
|
||||
ActionPipelineStep = "pipeline_step" // gitea PR approved/merged -> next pipeline step
|
||||
ActionIgnore = "ignore" // recognized but not actionable
|
||||
)
|
||||
|
||||
// Event is the normalized internal record every provider webhook becomes.
|
||||
// One shape, three sources: source + kind say what happened, actor who did
|
||||
// it, SubjectID/Subject what it happened to (issue / topic / PR).
|
||||
type Event struct {
|
||||
Source string `json:"source"`
|
||||
Kind string `json:"kind"`
|
||||
// Action is the mapped conductor action (dispatch_turn, ...).
|
||||
Action string `json:"action"`
|
||||
// Actor is the provider-side user who triggered the event.
|
||||
Actor string `json:"actor"`
|
||||
// SubjectID canonically identifies the subject: redmine:issue:42,
|
||||
// discourse:topic:7, gitea:pr:ukrrs/MOPAC#5.
|
||||
SubjectID string `json:"subject_id"`
|
||||
// Subject is the human-readable title (best effort).
|
||||
Subject string `json:"subject,omitempty"`
|
||||
// Repo is the gitea repository full_name when applicable.
|
||||
Repo string `json:"repo,omitempty"`
|
||||
// ProviderID is the provider's own event id (delivery/event header,
|
||||
// falling back to the payload digest) and the dedup identity.
|
||||
ProviderID string `json:"provider_id"`
|
||||
PayloadDigest string `json:"payload_digest"`
|
||||
ReceivedAt time.Time `json:"received_at"`
|
||||
}
|
||||
|
||||
// DedupKey is the append-store identity: source + provider event id.
|
||||
func (e Event) DedupKey() string {
|
||||
return e.Source + ":" + e.ProviderID
|
||||
}
|
||||
|
||||
// Sources lists the receiver's provider sources.
|
||||
func Sources() []string {
|
||||
return []string{SourceRedmine, SourceDiscourse, SourceGitea}
|
||||
}
|
||||
Reference in New Issue
Block a user