1// Cloudflare → any notification sent to a generic webhook destination
2// Fires for every Cloudflare notification policy that uses this webhook. The push uses
3// Cloudflare's own text; the alert type sets the label and how loud it is.
4// Docs: https://developers.cloudflare.com/notifications/reference/webhook-payload-schema/
5
6// alert_type → label and priority.
7// 2: under attack or the site is down right now.
8// 1: needs you soon (errors, failing health checks, security findings).
9// 0: worth knowing (certificates, usage, deploys).
10// -1: informational digests and maintenance.
11const TYPES = {
12 advanced_ddos_attack_l4_alert: { emoji: "🛡️", label: "DDoS attack", priority: 2 },
13 advanced_ddos_attack_l7_alert: { emoji: "🛡️", label: "HTTP DDoS attack", priority: 2 },
14 dos_attack_l4: { emoji: "🛡️", label: "DDoS attack", priority: 2 },
15 dos_attack_l7: { emoji: "🛡️", label: "HTTP DDoS attack", priority: 2 },
16 fbm_dosd_attack: { emoji: "🛡️", label: "DDoS attack on your network", priority: 2 },
17 fbm_volumetric_attack: { emoji: "🛡️", label: "Volumetric attack on your network", priority: 2 },
18 bgp_hijack_notification: { emoji: "🚨", label: "BGP hijack detected", priority: 2 },
19 http_alert_origin_error: { emoji: "🔥", label: "Origin error rate high", priority: 2 },
20 real_origin_monitoring: { emoji: "🔥", label: "Origin unreachable", priority: 2 },
21 load_balancing_health_alert: { emoji: "⚠️", label: "Load balancer pool health changed", priority: 1 },
22 health_check_status_notification: { emoji: "⚠️", label: "Health check status changed", priority: 1 },
23 tunnel_health_event: { emoji: "⚠️", label: "Tunnel health changed", priority: 1 },
24 magic_tunnel_health_check_event: { emoji: "⚠️", label: "Magic tunnel health changed", priority: 1 },
25 magic_wan_tunnel_health: { emoji: "⚠️", label: "Magic WAN tunnel health changed", priority: 1 },
26 http_alert_edge_error: { emoji: "⚠️", label: "Edge error rate high", priority: 1 },
27 advanced_http_alert_error: { emoji: "⚠️", label: "HTTP error rate high", priority: 1 },
28 traffic_anomalies_alert: { emoji: "⚠️", label: "Traffic anomaly", priority: 1 },
29 clickhouse_alert_fw_anomaly: { emoji: "⚠️", label: "Security events spike", priority: 1 },
30 clickhouse_alert_fw_ent_anomaly: { emoji: "⚠️", label: "Security events spike", priority: 1 },
31 bot_traffic_basic_alert: { emoji: "🤖", label: "Bot traffic spike", priority: 1 },
32 custom_bot_detection_alert: { emoji: "🤖", label: "Bot detection alert", priority: 1 },
33 scriptmonitor_alert_new_malicious_scripts: { emoji: "🚨", label: "Malicious script detected", priority: 1 },
34 scriptmonitor_alert_new_malicious_hosts: { emoji: "🚨", label: "Malicious script host detected", priority: 1 },
35 scriptmonitor_alert_new_malicious_url: { emoji: "🚨", label: "Malicious URL detected", priority: 1 },
36 abuse_report_alert: { emoji: "⚠️", label: "Abuse report", priority: 1 },
37 block_notification_new_block: { emoji: "⛔", label: "Content blocked", priority: 1 },
38 secondary_dns_all_primaries_failing: { emoji: "🔥", label: "All DNS primaries failing", priority: 1 },
39 secondary_dns_primaries_failing: { emoji: "⚠️", label: "DNS primaries failing", priority: 1 },
40 failing_logpush_job_disabled_alert: { emoji: "⚠️", label: "Logpush job disabled", priority: 1 },
41 workers_observability_alert: { emoji: "⚠️", label: "Workers alert", priority: 1 },
42 synthetic_test_low_availability_alert: { emoji: "⚠️", label: "Low availability", priority: 1 },
43 device_connectivity_anomaly_alert: { emoji: "⚠️", label: "Device connectivity anomaly", priority: 1 },
44 sentinel_alert: { emoji: "⚠️", label: "Sentinel alert", priority: 1 },
45 universal_ssl_event_type: { emoji: "🔒", label: "Universal SSL certificate", priority: 0 },
46 dedicated_ssl_certificate_event_type: { emoji: "🔒", label: "SSL certificate", priority: 0 },
47 custom_ssl_certificate_event_type: { emoji: "🔒", label: "Custom SSL certificate", priority: 0 },
48 access_custom_certificate_expiration_type: { emoji: "🔒", label: "Access certificate expiring", priority: 0 },
49 hostname_aop_custom_certificate_expiration_type: { emoji: "🔒", label: "Origin pull certificate expiring", priority: 0 },
50 zone_aop_custom_certificate_expiration_type: { emoji: "🔒", label: "Origin pull certificate expiring", priority: 0 },
51 mtls_certificate_store_certificate_expiration_type: { emoji: "🔒", label: "mTLS certificate expiring", priority: 0 },
52 expiring_service_token_alert: { emoji: "🔑", label: "Service token expiring", priority: 0 },
53 workers_alert: { emoji: "📈", label: "Workers usage", priority: 0 },
54 billing_usage_alert: { emoji: "💳", label: "Usage alert", priority: 0 },
55 pages_event_alert: { emoji: "📄", label: "Pages deployment", priority: 0 },
56 incident_alert: { emoji: "📢", label: "Cloudflare incident", priority: 0 },
57 synthetic_test_latency_alert: { emoji: "🐢", label: "High latency", priority: 0 },
58 secondary_dns_warning: { emoji: "⚠️", label: "Secondary DNS warning", priority: 0 },
59 secondary_dns_zone_validation_warning: { emoji: "⚠️", label: "Secondary DNS warning", priority: 0 },
60 load_balancing_pool_enablement_alert: { emoji: "⚖️", label: "Load balancer pool toggled", priority: 0 },
61 scriptmonitor_alert_new_hosts: { emoji: "📜", label: "New script host", priority: 0 },
62 scriptmonitor_alert_new_resources: { emoji: "📜", label: "New script", priority: 0 },
63 scriptmonitor_alert_new_code_change_detections: { emoji: "📜", label: "Script changed", priority: 0 },
64 scriptmonitor_alert_new_max_length_resource_url: { emoji: "📜", label: "Long script URL", priority: 0 },
65 fbm_auto_advertisement: { emoji: "🛡️", label: "Prefix auto-advertised", priority: 1 },
66 maintenance_event_notification: { emoji: "🔧", label: "Scheduled maintenance", priority: -1 },
67 cni_maintenance_notification: { emoji: "🔧", label: "Interconnect maintenance", priority: -1 },
68 secondary_dns_zone_successfully_updated: { emoji: "ℹ️", label: "Secondary DNS zone updated", priority: -1 },
69 block_notification_block_removed: { emoji: "ℹ️", label: "Block removed", priority: -1 },
70 web_analytics_metrics_update: { emoji: "📊", label: "Web Analytics update", priority: -1 },
71 radar_notification: { emoji: "📡", label: "Radar notification", priority: -1 },
72 brand_protection_digest: { emoji: "ℹ️", label: "Brand protection digest", priority: -1 },
73}
74
75// Look a key up in one of the tables above, ignoring inherited names like "toString".
76function pick(table, key, fallback) {
77 return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
78}
79
80// Only link to real http(s) URLs.
81function link(url, cta) {
82 return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
83}
84
85// A plain, non-empty string or null.
86function text(value) {
87 return typeof value === "string" && value.trim() ? value.trim() : null
88}
89
90function handleRequest(request) {
91 const body = request.body && typeof request.body === "object" ? request.body : {}
92 const data = body.data && typeof body.data === "object" ? body.data : {}
93 const message = text(body.text)
94 const alertType = text(body.alert_type)
95 const policy = text(body.name) ?? text(body.policy_name)
96
97 // "Save and Test" sends only a text field; confirm the connection quietly.
98 if (message && !alertType && !policy && /test message/i.test(message)) {
99 return {
100 title: "🔔 Cloudflare connected",
101 message: "Your Cloudflare webhook destination is working",
102 topic: "Cloudflare",
103 priority: -1,
104 }
105 }
106
107 // Guard: a Cloudflare notification has text plus an alert type or policy name.
108 if (!message || (!alertType && !policy)) {
109 console.log("Not a Cloudflare notification — skipping")
110 return null
111 }
112
113 const type = pick(TYPES, alertType, null)
114 // An alert that ended, or a health check that turned healthy, is good news.
115 const healthy = /^healthy$/i.test(text(data.new_health_status) ?? text(data.new_status) ?? "")
116 const resolved = body.alert_event === "ALERT_STATE_EVENT_END" || healthy
117 const label = type?.label ?? policy ?? "Cloudflare notification"
118 // Name the zone or host when Cloudflare's text doesn't already.
119 const where = text(data.target_hostname) ?? text(data.zone_name)
120 const title = resolved
121 ? `✅ Resolved: ${label}`
122 : `${type?.emoji ?? "🔔"} ${label}${where && !message.includes(where) ? ` on ${where}` : ""}`
123
124 // Dashboard links: the alert's own link when it has one, otherwise the account.
125 const account = text(body.account_id)
126 const dashboard = text(data.dashboard_link) ?? text(data.rule_link) ??
127 (account && /^[0-9a-f]{32}$/i.test(account) ? `https://dash.cloudflare.com/${account}` : null)
128
129 return {
130 title,
131 message: message.length > 500 ? `${message.slice(0, 497)}…` : message,
132 topic: "Cloudflare",
133 // Resolved alerts are good news; unknown alert types default to Normal.
134 priority: resolved ? -1 : type?.priority ?? 0,
135 buttons: link(dashboard, "Open dashboard"),
136 }
137}