Listeners can now hand an event to a pusher instead of a hard-coded job.
- event_listeners.common_pusher_id links a listener to a Commons pusher
(the column is added by hand, no migration)
- EventPusherJob is the generic callback; EventPusherService applies the
listener rule (is_active, account ownership, time_window, conditions)
and calls PushersService::trigger()
- EventEnvelopeBuilder renders a CloudEvents-shaped payload from the
model's Transformer; ListenerRuleEvaluator implements conditions and
time_window
- Drivers: event_webhook (Standard Webhooks HMAC signing, secret
rotation) and event_chat (slack/mattermost/teams/discord)
- SsrfGuard: DNS re-resolved on every send, private ranges blocked,
connection pinned to the checked IP, no redirects
- Events::fire() skips soft-deleted and inactive listeners and orders
by priority; ListenersService::create wires pusher listeners
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>