feat(canary): event + notify domain — contracts, senders, services
Lays the domain layer for Phase 10 notification dispatch.
- event/contract.go: NotifyInfo (token-shape DTO that breaks the would-be
event→notify→token cycle), Notifier interface, TokenIncrementer interface,
Store interface (Insert+UpdateNotifyStatus+PruneToLimit).
- token.Token.NotifyInfo() helper bridges *token.Token → event.NotifyInfo
for the main.go adapter.
- notify/types.go: Sender + StatusWriter interfaces.
- notify.Service: per-channel sender registry, async fire-and-forget
Notify with bounded sendTimeout, WaitGroup for graceful drain, status
writeback (sent / failed) on the configured StatusWriter.
- telegram.Sender: POST /bot{TOKEN}/sendMessage with MarkdownV2-escaped
body, cenkalti/backoff/v5 retry (3 tries / 30s window), permanent on
4xx, 10s overall + 5s connect timeouts. Note: spec body said
parse_mode=Markdown but escape table is the V2 set; using MarkdownV2
keeps the escape rules consistent.
- webhook.Sender: POST user URL with versioned JSON envelope (§10.4),
optional HMAC-SHA256 signing via X-Canary-Signature, URL validation
at the call site (rejects non-http(s), missing host, userinfo). Same
retry/timeout policy as telegram.
- event.Service: Record(ctx, info, evt) inserts → IncrementTriggerCount
→ Redis SetNX dedup gate (15m TTL, fail-open on Redis error). First
trigger calls notifier; duplicates INCR + UpdateNotifyStatus(deduped).
- event.Service.RunRetentionLoop(ctx, interval, limit) tickered prune.
Adds cenkalti/backoff/v5 + miniredis/v2 to go.mod for backoff API and
unit-test Redis fakes.