Skip to content

Add Cloudflare notification webhooks and subscriptions - #467

Closed
Ankcorn wants to merge 6 commits into
cloudflare:mainfrom
Ankcorn:tankcorn/cloudflare-real-time-issues-gatekeeper
Closed

Ankcorn wants to merge 6 commits into
cloudflare:mainfrom
Ankcorn:tankcorn/cloudflare-real-time-issues-gatekeeper

Conversation

@Ankcorn

@Ankcorn Ankcorn commented Sep 9, 2026

Copy link
Copy Markdown
Member

This MR adds notification webhooks and subscriptions to the existing Cloudflare gatekeeper, so Gadgets and agents can react to Cloudflare alerts without polling.

Screenshots

Connect your account. The setup uses the existing Cloudflare connector, with a short connection status and extra guidance under Details.

Notification setup

Receive alerts from Cloudflare. Enabling the first subscription creates a webhook destination in Cloudflare, which you can select on your notification policies.

Webhook destination in Cloudflare

Choose what each subscriber receives. Hooks use the standard Connections controls. This example has two independent subscriptions: one for all alert types and one filtered to a selected type.

Notification subscriptions

See delivery working. This earlier test inbox shows synthetic alerts reaching Gadget callbacks, and lastTestAt records a real Cloudflare ANS destination test. Its queue counters are from the previous implementation and have since been removed: the webhook now waits for subscriber handoff and returns 500 on failure so ANS can retry. Successful handoffs are deduplicated per subscriber. A real policy-triggered incident has not yet been tested.

Notification delivery inbox

Related PRs

Follow-ups

  • Example investigation agent: use Real-Time Issue notifications to investigate with Workers Observability, make a change in a configured GitHub repository, run checks, and open a draft PR.
  • Optional mTLS: add client-certificate verification alongside webhook API-key authentication, with live certificate and ANS delivery testing before review.

Formatting

New files are formatted with pnpm exec vp fmt. Existing files retain their surrounding formatting; whole-file formatting churn was removed to keep the review focused.

@github-actions github-actions Bot added kernel Changes to the Workshop kernel delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@Ankcorn
Ankcorn force-pushed the tankcorn/cloudflare-real-time-issues-gatekeeper branch from 551f84c to c678ffb Compare September 9, 2026 16:40
@Ankcorn

Ankcorn commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 9, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +280 to +282
async disable(hookId: string): Promise<void> {
this.ctx.storage.kv.delete(`hook:${hookId}`);
this.ctx.storage.sql.exec("DELETE FROM notification_receipts WHERE hook_id = ?", hookId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Disabled subscriptions leak server capabilities

Disabling a subscription deletes hook:${hookId} without disposing its stored initiator. Replacement in enable and account cleanup discard the same capability likewise. Repeated subscription changes retain server resources indefinitely.

Prompt for agents
Audit StoredHook capability ownership in packages/gatekeeper-cloudflare/src/notifications.ts. The initiator stored under hook:* must be disposed whenever its record is replaced, disabled, or removed during revokeWithToken. Preserve the current hook and receipt semantics while following the disposal pattern used by the scheduler's StoredCapabilities lifecycle.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/workshop-backend/src/auth/login-flow.ts Outdated
# Conflicts:
#	packages/workshop-backend/src/auth/login-flow.ts
@Ankcorn
Ankcorn marked this pull request as ready for review September 21, 2026 08:37

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Devin Review

Comment on lines +206 to +208
#hooks() {
return [...this.ctx.storage.kv.list<StoredHook>({ prefix: "hook:" })].map(([, hook]) => hook);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Every webhook leaks subscriber capabilities

Each #hooks() call deserializes every stored initiator without disposing it. Status checks, enablement, and every webhook retain server-side RPC resources indefinitely.

Learn more

Durable Object storage returns live RPC capabilities when a StoredHook is read. Each returned capability has caller-owned lifetime, even though another copy remains stored for later delivery. This helper materializes every initiator for status, quota, filtering, and delivery calls, but none of those paths releases the materialized stubs. Replacement, disable, and revocation also delete or overwrite stored capabilities without first loading and disposing them. The scheduler demonstrates the required ownership pattern in disposeCapabilities.

Example: With 100 subscribers, each incoming notification loads 100 initiator stubs. After 1,000 notifications, the receiver has created 100,000 unreleased server-side capabilities, even when every callback succeeds.

Recommended fix: Avoid deserializing hook values when only keys or counts are needed. For delivery, hold one snapshot, await all handoffs, then dispose every snapshot initiator in finally. On replacement, disable, and revocation, retrieve and dispose the removed initiator after the storage mutation, following the scheduler's capability ownership pattern.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1636 to +1644
// Sign-in grants billing access only. Resource grants own persistent bindings and subscriptions;
// preserve them even if expired. Their existing reconnect flow refreshes credentials in place.
if (uniqueName) {
let existing = this.#findConnectedAccountByIdentity(vendorId, uniqueName);
if (existing) {
if (existing.description.grantedResourceUrlPatterns?.length) {
await account.revoke();
return existing.id;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Kernel prerequisite remains bundled

The PR changes repeated-login account preservation alongside notifications. Repository guidance requires kernel concerns to be split, and the description assigns this change to a separate PR.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +44 to +45
/** Parse the generic ANS envelope. No fixed alert-type enumeration or event_id assumption. */
export async function parseNotificationWebhook(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Production alert delivery remains unverified

Tests use synthetic notification envelopes. No policy-triggered incident has validated the production payload or delivery behavior, leaving the core integration assumption unresolved.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@Ankcorn

Ankcorn commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

Superseded by a smaller composable design: a generic webhook gatekeeper owns ingress and subscriptions, while the Cloudflare gatekeeper will only provision the Notifications destination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration kernel Changes to the Workshop kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant