feat(events): outbound webhook subscription bus, HMAC + SSRF-guarded (plan 44) by duyet · Pull Request #2235 · chmonitor/chmonitor · GitHub
Skip to content

feat(events): outbound webhook subscription bus, HMAC + SSRF-guarded (plan 44) - #2235

Merged
duyet merged 6 commits into
mainfrom
advisor/44-webhook-event-bus-outbound
Jul 3, 2026
Merged

feat(events): outbound webhook subscription bus, HMAC + SSRF-guarded (plan 44)#2235
duyet merged 6 commits into
mainfrom
advisor/44-webhook-event-bus-outbound

Conversation

@duyet

@duyet duyet commented Jul 3, 2026

Copy link
Copy Markdown
Member

What (plan 44 — outbound webhook event bus)

emitEvent(userId, evt) fans out to a user's enabled subscriptions; deliver() SSRF-guards the destination (validateHostUrl), HMAC-SHA256-signs the raw body, retries ≤3× with bounded 0/2/8s backoff (5xx/429 only; 4xx + SSRF-block dead-letter immediately), and records every outcome. Never throws (producers call it unawaited — no waitUntil plumbing).

  • New lib/events/* + 0010_webhook_subscriptions.sql; 4 user-scoped routes (list/create, patch/delete, test-ping, deliveries); "Webhooks" settings tab; webhookSubscriptions feature flag.
  • Honest scope: only connection.created/connection.deleted are wired — alert.*/insight.* have no per-user owner model in this codebase, so they're intentionally not offered (plan authorizes "only list event types actually emitted").
  • bun test src/lib/events → 18 pass (fan-out with independently-verified HMAC, bounded retry incl. 429/4xx/network edges, SSRF rejection = zero fetch, owner-guarded UPDATE/DELETE vs real SQLite). Full suite 4849 pass. bun run check / depcruise clean.

🔶 Held for review — reasons

  1. New outbound egress path (SSRF/HMAC) — same trust posture as the existing health/webhook.ts proxy (mirrors, not bypasses). Warrants a security read of: the subscription-store.ts ownership guards, and the plaintext webhook secret at rest in D1 (matches the plan's literal schema; lower blast radius than CH creds, but flag it).
  2. Migration 0010 collision0010_webhook_subscriptions.sql joins plans 27/22 and held chore(deps): update docker/metadata-action digest to 6d6eaf3 #56 all at 0010; renumber at merge.

Note: plans/README.md not edited (central reconciliation at merge).

Co-Authored-By: duyetbot bot@duyet.net

Generalize outbound webhooks from alerts-only to a configurable,
user-scoped bus: subscribe a URL to connection.created/connection.deleted
and receive HMAC-signed, retried, SSRF-guarded deliveries with a
dead-letter log.

- lib/events/{event-types,outbound-bus,subscription-store}.ts: emitEvent()
  fans out to a user's enabled subscriptions matching the event type;
  deliver() SSRF-guards via validateHostUrl (not createHostValidationFetch,
  which throws on Workers for non-IP hosts), HMAC-SHA256 signs the raw
  body, retries 3x with 0/2s/8s backoff (5xx/429 only), and records
  delivered/failed/dead outcomes. Never throws; producers call it
  fire-and-forget since this app has no waitUntil plumbed to routes.
- D1 migration 0010: webhook_subscriptions + webhook_deliveries, owner-
  guarded UPDATE/DELETE (same IDOR class plan 04 fixed), proven against
  real SQLite in subscription-store.sql.test.ts.
- routes/api/v1/webhooks/subscriptions(/$id, /$id/test, /$id/deliveries):
  user-scoped CRUD + send-test + dead-letter view, gated behind Clerk + D1
  like user-connections (fails closed on self-hosted/OSS — no behavior
  change).
- Emit hooks wired at user-connections create/delete (the one producer
  with a real per-user owner). alert.*/insight.* are intentionally not
  wired: the health-alert cron and insights generator run against
  env-configured hosts with no per-user owner anywhere in this codebase,
  so subscribing to them would need a bigger ownership model this plan
  doesn't ask for.
- New "Webhooks" tab in HealthSettingsDialog: list/add/delete/toggle,
  reveal-once secret, send test, recent deliveries.

Co-Authored-By: duyetbot <bot@duyet.net>
@github-actions github-actions Bot added the app: dashboard Changes to the apps/dashboard TanStack Start app label Jul 3, 2026
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

@duyet
duyet enabled auto-merge (squash) July 3, 2026 13:18
duyet and others added 3 commits July 3, 2026 20:22
…ent-bus-outbound

# Conflicts:
#	apps/dashboard/src/components/health/health-settings-dialog.tsx
- webhook-subscriptions-panel.tsx: onToggled callback took an unused
  boolean param, which didn't match refetch's RefetchOptions signature
- subscription-store.ts: cast eventTypes to readonly string[] before
  .includes(eventType: string) instead of narrowing the param type
- subscriptions/$id.ts: declare eventTypes as possibly undefined so TS
  doesn't flag it as used-before-assigned when body.eventTypes is absent

Co-Authored-By: duyetbot <bot@duyet.net>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

☁️ Cloudflare Preview Deployment

App Preview URL
Dashboard https://preview.dash.chmonitor.dev
MCP https://preview.dash.chmonitor.dev/api/mcp
Landing https://preview.chmonitor.dev
Docs https://preview.docs.chmonitor.dev
Property Value
Commit b39a64b
Deployed at 2026-07-03T16:21:45.063Z

Previews are automatically updated on every push to this PR.
Unchanged apps keep their previous preview (path-filtered deploys).

…iled #2242)

Resolves conflicts in user-connections.ts / user-connections/$id.ts where
this branch's outbound webhook bus (emitEvent) and main's audit log
(logEvent/logSessionEvent) both hook the connection create/delete path —
kept both, audit log first then webhook emit. Adds emitEvent mocks to
user-connections.test.ts and user-connections/$id.test.ts (main's
audit-log tests didn't know about the webhook bus, so the route now
transitively pulled in @chm/platform's cloudflare:workers binding
under bun test).

Co-Authored-By: duyetbot <bot@duyet.net>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

☁️ Cloudflare Preview Deployment

App Preview URL
Dashboard https://preview.dash.chmonitor.dev
MCP https://preview.dash.chmonitor.dev/api/mcp
Landing https://preview.chmonitor.dev
Docs https://preview.docs.chmonitor.dev
Property Value
Commit 989c290
Deployed at 2026-07-03T16:32:36.147Z

Previews are automatically updated on every push to this PR.
Unchanged apps keep their previous preview (path-filtered deploys).

@duyet
duyet merged commit 6eaaccc into main Jul 3, 2026
18 of 19 checks passed
@duyet
duyet deleted the advisor/44-webhook-event-bus-outbound branch July 3, 2026 16:32
@duyet
duyet restored the advisor/44-webhook-event-bus-outbound branch July 6, 2026 11:42
@duyet
duyet deleted the advisor/44-webhook-event-bus-outbound branch July 16, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: dashboard Changes to the apps/dashboard TanStack Start app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants