Skip to content

Add webhook listener, replay, and trigger commands#57

Merged
dylanjha merged 24 commits intomainfrom
dj/webhooks
Mar 6, 2026
Merged

Add webhook listener, replay, and trigger commands#57
dylanjha merged 24 commits intomainfrom
dj/webhooks

Conversation

@dylanjha
Copy link
Contributor

@dylanjha dylanjha commented Mar 5, 2026

Summary

  • mux webhooks listen — connects to Mux's SSE event stream and optionally forwards events to a local dev server URL with --forward-to. Displays a signing secret for webhook verification. Reconnects automatically with exponential backoff on connection loss.
  • mux webhooks events list — lists locally stored webhook events captured during listen sessions.
  • mux webhooks events replay <event-id> — replays stored events to a local URL, re-signed with the local secret.
  • mux webhooks trigger <event-type> — sends a synthetic webhook event (with realistic payload) directly to a local URL for testing handlers without creating real resources.
  • mux whoami — displays the current authenticated environment and user info.

Webhook signing secrets are scoped per CLI environment so switching environments requires updating MUX_WEBHOOK_SECRET in the app. Events are deduplicated by ID in local storage.

Note: These commands are for local development only. Production webhooks must be configured in the Mux Dashboard.

Bug fixes

  • Skip Homebrew tap update for pre-release tags (e.g. v1.0.0-beta.1) to avoid overwriting the stable formula

QOL improvement

The term default env was used in code and in user-facing listing of envs. but that's an over-loaded term, because the CLI also names the first env default:

before
Screenshot 2026-03-06 at 9 59 41 AM

  • call the currently selected env current
Screenshot 2026-03-06 at 10 00 07 AM

(for backwards-compat the defaultEnvironment in ~/.config/mux/config.json is still called defaultEnvironment)

Test plan

  • mux webhooks listen --forward-to connects, receives events, forwards with signing
  • mux webhooks listen --json --forward-to forwards events while outputting JSON
  • mux webhooks trigger video.asset.ready --forward-to sends signed synthetic event, passes mux.webhooks.unwrap() verification
  • mux webhooks events list shows captured events without duplicates
  • mux webhooks events replay <id> --forward-to re-forwards with valid signature
  • Connection loss shows dim reconnect message (not red error)
  • Invalid event type in trigger lists all available types
  • All 663 existing tests pass, biome + typecheck clean

🤖 Generated with Claude Code


Note

High Risk
Adds new commands that open long-lived SSE connections, write local event/secret files, and forward signed HTTP requests, increasing surface area for reliability and security issues. Also deletes bin/mux while package.json still points to it, which can break npm installs/publishing if not updated elsewhere.

Overview
Adds webhook developer tooling. Introduces mux webhooks listen (SSE stream with reconnect/backoff), mux webhooks events list/replay (local storage + replay/forward), and mux webhooks trigger (locally generated example events), including per-environment persistent signing secrets and request signing via mux-signature.

Adds mux whoami to call the System API and print authenticated org/environment/token info, and extends lib/mux.ts with getMuxBaseUrl() (supports MUX_BASE_URL) plus getAuthHeaders() for raw fetch calls.

Environment terminology cleanup. Renames config helpers from default to current (getCurrentEnvironment/setCurrentEnvironment) and updates call sites/tests and mux env list output wording.

Release/packaging changes. Skips Homebrew tap updates for pre-release tags, removes internal note/todo files, and deletes the Node shim entrypoint bin/mux (verify npm bin wiring remains valid).

Written by Cursor Bugbot for commit b6f4d6e. This will update automatically on new commits. Configure here.

dylanjha and others added 8 commits March 5, 2026 00:31
docs: add production webhook configuration warning to CLI commands
```
The partial mock broke other test files in CI because Bun's
mock.module replaces the module globally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds meta, progress, track status/primary/name/language_code fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: --json mode silently skips event forwarding in listen
    • I moved the forwarding fetch logic to run before output formatting so events are forwarded regardless of whether --json is enabled.
  • ✅ Fixed: Dedup with fallback ID silently drops subsequent events
    • I replaced the 'unknown' fallback with a generated UUID when an event ID is missing so deduplication no longer collapses unrelated ID-less events.

Create PR

Or push these changes by commenting:

@cursor push 80e606e934
Preview (80e606e934)
diff --git a/src/commands/webhooks/listen.ts b/src/commands/webhooks/listen.ts
--- a/src/commands/webhooks/listen.ts
+++ b/src/commands/webhooks/listen.ts
@@ -130,7 +130,10 @@
             continue;
           }
 
-          const eventId = (parsed.id as string) ?? 'unknown';
+          const eventId =
+            typeof parsed.id === 'string' && parsed.id.length > 0
+              ? parsed.id
+              : crypto.randomUUID();
           const eventType = (parsed.type as string) ?? sseEvent.event;
           const timestamp = new Date().toISOString();
 
@@ -145,32 +148,43 @@
           await appendEvent(storedEvent);
           eventCount++;
 
+          let forwardStatusCode: number | undefined;
+          let forwardErrored = false;
+
+          if (options.forwardTo && signingSecret) {
+            try {
+              const body = JSON.stringify(parsed);
+              const fwdResponse = await fetch(options.forwardTo, {
+                method: 'POST',
+                headers: buildSignedHeaders(body, signingSecret),
+                body,
+              });
+              forwardStatusCode = fwdResponse.status;
+              if (forwardStatusCode >= 200 && forwardStatusCode < 300) {
+                forwardSuccess++;
+              } else {
+                forwardFail++;
+              }
+            } catch {
+              forwardFail++;
+              forwardErrored = true;
+            }
+          }
+
           if (options.json) {
             console.log(JSON.stringify(parsed));
           } else {
             const time = new Date().toLocaleTimeString();
             let line = `[${time}]  ${eventType.padEnd(30)}  ${eventId}`;
 
-            if (options.forwardTo && signingSecret) {
-              try {
-                const body = JSON.stringify(parsed);
-                const fwdResponse = await fetch(options.forwardTo, {
-                  method: 'POST',
-                  headers: buildSignedHeaders(body, signingSecret),
-                  body,
-                });
-                const status = fwdResponse.status;
-                if (status >= 200 && status < 300) {
-                  forwardSuccess++;
-                  line += `  ${colors.green(`[${status}]`)}`;
-                } else {
-                  forwardFail++;
-                  line += `  ${colors.red(`[${status}]`)}`;
-                }
-              } catch {
-                forwardFail++;
-                line += `  ${colors.red('[ERR]')}`;
+            if (forwardStatusCode !== undefined) {
+              if (forwardStatusCode >= 200 && forwardStatusCode < 300) {
+                line += `  ${colors.green(`[${forwardStatusCode}]`)}`;
+              } else {
+                line += `  ${colors.red(`[${forwardStatusCode}]`)}`;
               }
+            } else if (forwardErrored) {
+              line += `  ${colors.red('[ERR]')}`;
             }
 
             console.log(line);
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Add normalize_audio, passthrough, is_live, test, static_renditions,
and expanded meta fields (creator_id, external_id). Set errored
progress state to match docs (-1 progress).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dylanjha and others added 2 commits March 5, 2026 14:25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Handle \r\n, \r, and \n line endings per WHATWG SSE spec
- Remove legacy static_renditions (plural) event types
- Add video.asset.non_standard_input_detected with realistic payload

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@@ -0,0 +1,49 @@
import { Command } from '@cliffy/command';
import { listEvents } from '../../../lib/events-store.ts';
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe in a follow up PR, but we should update our tsconfig so we can just use @ references and clean some of these imports up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep, will follow up 👍🏼

process.exit(1);
}

if (!eventId && !options.all) {
Copy link
Contributor

Choose a reason for hiding this comment

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

All of these checks in here should probably use Cliffy's built in flag conflicts feature

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good shout -- that looked like a good option, but Cliffy's flag conflicts only works on Flag options (not arguments)

eventId is an argument, whereas --all is an option

mux webhooks events replay 5e6135cb-43ec-bd34-e571-5df88227b5c8
mux webhooks events replay --all

So I think the manual check is appropriate here? Or maybe changing the API?

dylanjha added 4 commits March 6, 2026 09:48
- we now refer to the selected env as the 'current' env (prev it was
  'defualt')
- but when creating the first env it gets the name 'default' so that was
  confusing
@dylanjha dylanjha merged commit 5322f2f into main Mar 6, 2026
8 checks passed
@dylanjha dylanjha deleted the dj/webhooks branch March 6, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants