Skip to content

feat(triggers): dispatcher registration + Slack url_verification + event JSON#2326

Merged
danielkov merged 3 commits intomainfrom
daniel/age-1897-triggers-dispatcher-slack
Apr 21, 2026
Merged

feat(triggers): dispatcher registration + Slack url_verification + event JSON#2326
danielkov merged 3 commits intomainfrom
daniel/age-1897-triggers-dispatcher-slack

Conversation

@danielkov
Copy link
Copy Markdown
Contributor

Summary

  • App.RegisterDispatcher(Dispatcher) — register dispatchers post-construction (no-op on nil). Unblocks services that need the trigger app at wire-up time without a constructor cycle.
  • Slack AuthenticateWebhook short-circuits the url_verification handshake so the challenge can be echoed before a signing secret is necessarily configured.
  • Drop the thread_ts → ts fallback for Slack correlation. Top-level channel/DM messages now correlate on the channel alone, so a user's standalone messages land in one Gram thread instead of spawning one per message.
  • Task.EventJSON []byte (populated from json.Marshal(envelope.Event)), plus BotID / AppID fields on both slackEventRequestBody and slackTriggerEvent (CEL tags on the latter).

All four are pure additions — no removals or renames.

Linear: https://linear.app/speakeasy/issue/AGE-1897/feattriggers-dispatcher-registration-slack-url-verification-event-json

✻ Clauded...

…ent JSON

- Add App.RegisterDispatcher(Dispatcher) for post-construction dispatcher
  wiring so services that need the trigger app at wire-up time can register
  themselves without a constructor cycle. No-op when dispatcher is nil.
- Short-circuit the Slack url_verification handshake in AuthenticateWebhook
  so Slack's challenge can be echoed before a signing secret is necessarily
  configured (HandleWebhook already responds with the challenge).
- Drop the thread_ts -> ts fallback in the Slack definition builder. Top-level
  channel/DM messages now correlate on the channel alone so a user's
  standalone messages merge into one Gram thread instead of spawning one per
  message; replies continue to correlate on thread_ts.
- Add Task.EventJSON []byte populated from json.Marshal(envelope.Event) so
  downstream activities can route on structured event data, plus expose
  bot_id / app_id on slackEventRequestBody and slackTriggerEvent (with CEL
  tags on the latter) so the CEL filter surface can see them.

✻ Clauded...
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment Apr 21, 2026 11:42am

Request Review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@linear
Copy link
Copy Markdown

linear Bot commented Apr 21, 2026

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: 2444058

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
dashboard Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +311 to +317
// Slack's URL verification handshake must echo the challenge before
// any signing secret has necessarily been configured. Allow it
// through auth; HandleWebhook will respond with the challenge.
var probe slackEventRequest
if err := json.Unmarshal(body, &probe); err == nil && probe.Type == "url_verification" && probe.Challenge != "" {
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 url_verification auth bypass: comment premise doesn't hold, deviates from existing pattern

The new AuthenticateWebhook bypass at server/internal/background/triggers/definitions.go:314-317 skips Slack signature verification for url_verification requests, justified by the comment "before any signing secret has necessarily been configured." However, ProcessWebhook calls validateInstance at server/internal/background/triggers/app.go:361 before AuthenticateWebhook at line 374. Since the Slack definition declares SLACK_SIGNING_SECRET as Required: true (definitions.go:289), validateInstance enforces the secret exists in the environment (app.go:570-573). By the time AuthenticateWebhook runs, the secret IS necessarily configured — the comment's premise is incorrect.

This also deviates from the existing pattern in server/internal/thirdparty/slack/impl.go:226 where validateSlackEvent is called before the url_verification check at line 236, meaning signatures are always verified.

The practical impact is very low: HandleWebhook returns early for url_verification with Event: nil (no events created, no triggers dispatched). However, the bypass (1) allows unauthenticated confirmation that a trigger endpoint exists, and (2) masks signing-secret misconfiguration during Slack setup — the URL verification succeeds even if the secret value is wrong, but subsequent event deliveries will fail.

Open in Devin Review

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is incorrect: Slack probes this endpoint before it allows webhook registration, the endpoint needs to respond with 200, otherwise Slack won't allow adding the endpoint

Comment on lines +508 to +510
if dispatcher == nil {
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why is this possible? we shouldn't have this type of logic in littering methods left and right.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +314 to +317
var probe slackEventRequest
if err := json.Unmarshal(body, &probe); err == nil && probe.Type == "url_verification" && probe.Challenge != "" {
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Slack webhook HMAC signature verification bypassed for url_verification requests even when signing secret is available

The new short-circuit in AuthenticateWebhook at definitions.go:315-316 skips HMAC signature verification for any request where type == "url_verification" and challenge != "". The comment claims this is needed "before any signing secret has necessarily been configured", but that scenario is impossible in practice: ProcessWebhook calls validateInstance at server/internal/background/triggers/app.go:361 before reaching AuthenticateWebhook, and validateInstance already enforces that SLACK_SIGNING_SECRET exists (it's marked Required: true in the Slack definition's EnvRequirements at definitions.go:289). If the secret is missing, validateInstance returns an error and AuthenticateWebhook is never called.

Therefore, this bypass only takes effect when the signing secret is available — meaning it skips a legitimate HMAC check. An attacker can send {"type":"url_verification","challenge":"probe"} to any Slack trigger webhook endpoint without a valid Slack signature and receive a 200 OK response, allowing them to probe for valid trigger instance UUIDs without authentication. While no events are dispatched (HandleWebhook returns early with just the challenge echo), this is an unnecessary weakening of the authentication layer.

Prompt for agents
The url_verification bypass in AuthenticateWebhook skips HMAC signature verification even when the signing secret is available. The comment says this is for when the secret hasn't been configured yet, but validateInstance (called earlier in ProcessWebhook at app.go:361) already enforces that SLACK_SIGNING_SECRET exists before AuthenticateWebhook is ever reached.

Two possible fixes:

1. Remove the bypass entirely from AuthenticateWebhook and always verify the HMAC signature, even for url_verification requests. Slack sends a valid signature with the url_verification request, so this should work.

2. If there's a legitimate need to handle url_verification before the environment is fully configured, the short-circuit should be moved to HandleWebhook in the HTTP handler layer (server/internal/triggers/impl.go HandleWebhook method) BEFORE authentication is attempted, or the bypass should only activate when the signing secret is actually empty (i.e. check signingSecret == "" first, then allow url_verification through).

Option 1 is the simplest and most secure approach.
Open in Devin Review

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

@danielkov danielkov merged commit f0cf087 into main Apr 21, 2026
28 checks passed
@danielkov danielkov deleted the daniel/age-1897-triggers-dispatcher-slack branch April 21, 2026 11:49
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants