chore(canary-phase10): event service + notify dispatcher complete
Phase 10 deliverables (audited PASS):
Domain layer
- event.NotifyInfo / Notifier / TokenIncrementer / Store contracts
in event/contract.go (breaks event→notify→token cycle without
the type-alias relocation pattern from Phase 9)
- token.Token.NotifyInfo() method bridges *Token → event.NotifyInfo
- notify.Sender + StatusWriter interfaces in notify/types.go
Senders
- telegram.Sender: POST /bot{TOKEN}/sendMessage with MarkdownV2
body, cenkalti/backoff/v5 retry (3 tries / 30s window), permanent
on 4xx, 10s overall + 5s connect timeouts. Sanctioned spec deviation:
parse_mode=MarkdownV2 (spec body says Markdown but escape table is
V2); makes the escape rules consistent. Documented in commit body.
- webhook.Sender: POST user URL with versioned JSON envelope
(§10.4 shape verified field-by-field), optional HMAC-SHA256
signing via X-Canary-Signature header, URL validation at the
call site (rejects non-http(s), missing host, userinfo).
Services
- notify.Service: per-channel sender registry, async fire-and-forget
Notify with bounded sendTimeout, sync.WaitGroup for graceful drain
(notifySvc.Wait() called from main.go gracefulShutdown).
- event.Service.Record: Insert → IncrementTriggerCount → Redis SetNX
dedup gate (15m TTL). First trigger calls notifier; duplicates INCR
+ UpdateNotifyStatus(deduped). Fail-open on Redis errors so we don't
silently miss alerts.
- event.Service.RunRetentionLoop: tickered prune to per-token limit.
Wire-up
- cmd/canary/main.go: buildEventStack constructs senders + notify.Service
+ event.Service. eventRecorderAdapter swaps the Phase-9
directEventRecorder for the real event.Service.Record path. Same
adapter wires the mysql listener — one path, one dedup policy.
- fingerprintRecorderAdapter wires event.Repository.AttachFingerprint
with the configured window.
- spawnRetentionLoop runs RunRetentionLoop on the shared wait group.
- Three nested rate limits on POST /api/tokens (read-tier from /api +
createMin 5/min + createHour 20/hr) with distinct Redis namespaces.
- gracefulShutdown drains notifySvc + retention/mysql wg before return.
Closed deferred Phase-9 items
- (1) testcontainers integration test — full create + trigger + dedup
- (2) directEventRecorder → event.Service.Record swap
- (3) nested create-tier rate limit on POST /api/tokens
- (4) gosec G107/G704 path-scoped to telegram/webhook/turnstile;
G101 path-scoped to config (env-var-name false positives)
- (5) FingerprintRecorder wiring (5-min window default)
- (7) drop required tag on TurnstileResp (middleware sole authority)
- (8) consolidate realIP into middleware across 5 generators
(docx, envfile, kubeconfig, pdf, slowredirect)
Audited: superpowers:code-reviewer + general-purpose spec adherence —
PASS. Code-reviewer caught one bug pre-rollup: formatGeo emitted
literal '(' and ')' which are MarkdownV2 reserved chars outside link
delimiters; would crash Telegram with a parse error. Fixed in
41b9d2c9 with regression assertion on the escaped wrapping.
This commit is contained in:
parent
41b9d2c911
commit
2041c557ad