From 0c4e5a2c81535be72eb511b9200149919a7a97df Mon Sep 17 00:00:00 2001 From: Rajat Bajaj Date: Thu, 26 Mar 2026 16:07:12 +0530 Subject: [PATCH 1/4] feat: Support configuration for event streams --- docs/auth0_event-streams_create.md | 1 + docs/auth0_event-streams_update.md | 3 ++- internal/cli/actions.go | 1 + internal/cli/actions_embed.go | 3 +++ internal/cli/data/action-template-event-stream.js | 9 +++++++++ internal/cli/event_streams.go | 4 +++- 6 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 internal/cli/data/action-template-event-stream.js diff --git a/docs/auth0_event-streams_create.md b/docs/auth0_event-streams_create.md index b777bada8..556c9a456 100644 --- a/docs/auth0_event-streams_create.md +++ b/docs/auth0_event-streams_create.md @@ -22,6 +22,7 @@ auth0 event-streams create [flags] auth0 event-streams create auth0 event-streams create --name my-event-stream --type eventbridge --subscriptions "user.created,user.updated" --configuration '{"aws_account_id":"325235643634","aws_region":"us-east-2"}' auth0 event-streams create --name my-event-stream --type webhook --subscriptions "user.created,user.deleted" --configuration '{"webhook_endpoint":"https://mywebhook.net","webhook_authorization":{"method":"bearer","token":"123456789"}}' + auth0 event-streams create --name my-event-stream --type action --subscriptions "user.created" --configuration '{"action_id":"b053e4ca-8b14-4233-b615-bd3bdb353977"}' auth0 event-streams create -n my-event-stream -t webhook -s "user.created,user.deleted" -c '{"webhook_endpoint":"https://mywebhook.net","webhook_authorization":{"method":"bearer","token":"123456789"}}' ``` diff --git a/docs/auth0_event-streams_update.md b/docs/auth0_event-streams_update.md index f3413a4ec..4c37a72ac 100644 --- a/docs/auth0_event-streams_update.md +++ b/docs/auth0_event-streams_update.md @@ -24,7 +24,8 @@ auth0 event-streams update [flags] auth0 event-streams update --name my-event-stream --status enabled auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created,user.updated" auth0 event-streams update --name my-event-stream --status disabled --subscriptions "user.deleted" --configuration '{"aws_account_id":"325235643634","aws_region":"us-east-2"}' - auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created" --configuration '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"0909090909"}} + auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created" --configuration '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"0909090909"}}' + auth0 event-streams create --name my-event-stream --subscriptions "user.updated" --configuration '{"action_id":"b053e4ca-8b14-4233-b615-bd3bdb353977"}' auth0 event-streams update -n my-event-stream --status enabled -s "user.created" -c '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"987654321"}} ``` diff --git a/internal/cli/actions.go b/internal/cli/actions.go index 179df3c4c..2b922dac6 100644 --- a/internal/cli/actions.go +++ b/internal/cli/actions.go @@ -76,6 +76,7 @@ var ( "send-phone-message": actionTemplateSendPhoneMessage, "custom-email-provider": actionTemplateCustomEmailProvider, "custom-phone-provider": actionTemplateCustomPhoneProvider, + "event-stream": actionTemplateEventStream, } ) diff --git a/internal/cli/actions_embed.go b/internal/cli/actions_embed.go index bcce0280e..48d0528f5 100644 --- a/internal/cli/actions_embed.go +++ b/internal/cli/actions_embed.go @@ -31,4 +31,7 @@ var ( //go:embed data/action-template-empty.js actionTemplateEmpty string + + //go:embed data/action-template-event-stream.js + actionTemplateEventStream string ) diff --git a/internal/cli/data/action-template-event-stream.js b/internal/cli/data/action-template-event-stream.js new file mode 100644 index 000000000..4f6fe12af --- /dev/null +++ b/internal/cli/data/action-template-event-stream.js @@ -0,0 +1,9 @@ +/** + * Handler that will be called during the execution of a EventStream flow. + * + * @param {Event} event - Details about the user and the context in which they are logging in. + * @param {EventStreamAPI} api - Methods and utilities to help + */ +exports.onExecuteEventStream = async (event, api) => { + +}; diff --git a/internal/cli/event_streams.go b/internal/cli/event_streams.go index f740a2f66..aed6a23f9 100644 --- a/internal/cli/event_streams.go +++ b/internal/cli/event_streams.go @@ -189,6 +189,7 @@ func createEventStreamCmd(cli *cli) *cobra.Command { Example: ` auth0 event-streams create auth0 event-streams create --name my-event-stream --type eventbridge --subscriptions "user.created,user.updated" --configuration '{"aws_account_id":"325235643634","aws_region":"us-east-2"}' auth0 event-streams create --name my-event-stream --type webhook --subscriptions "user.created,user.deleted" --configuration '{"webhook_endpoint":"https://mywebhook.net","webhook_authorization":{"method":"bearer","token":"123456789"}}' + auth0 event-streams create --name my-event-stream --type action --subscriptions "user.created" --configuration '{"action_id":"b053e4ca-8b14-4233-b615-bd3bdb353977"}' auth0 event-streams create -n my-event-stream -t webhook -s "user.created,user.deleted" -c '{"webhook_endpoint":"https://mywebhook.net","webhook_authorization":{"method":"bearer","token":"123456789"}}'`, RunE: func(cmd *cobra.Command, args []string) error { if err := eventStreamName.Ask(cmd, &inputs.Name, nil); err != nil { @@ -275,7 +276,8 @@ func updateEventStreamCmd(cli *cli) *cobra.Command { auth0 event-streams update --name my-event-stream --status enabled auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created,user.updated" auth0 event-streams update --name my-event-stream --status disabled --subscriptions "user.deleted" --configuration '{"aws_account_id":"325235643634","aws_region":"us-east-2"}' - auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created" --configuration '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"0909090909"}} + auth0 event-streams update --name my-event-stream --status enabled --subscriptions "user.created" --configuration '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"0909090909"}}' + auth0 event-streams create --name my-event-stream --subscriptions "user.updated" --configuration '{"action_id":"b053e4ca-8b14-4233-b615-bd3bdb353977"}' auth0 event-streams update -n my-event-stream --status enabled -s "user.created" -c '{"webhook_endpoint":"https://my-new-webhook.net","webhook_authorization":{"method":"bearer","token":"987654321"}}`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { From 54891cc974fb81bb8c08fef3f991608689d905c7 Mon Sep 17 00:00:00 2001 From: Rajat Bajaj Date: Thu, 26 Mar 2026 16:16:17 +0530 Subject: [PATCH 2/4] docs --- internal/cli/actions.go | 2 +- internal/cli/event_streams.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/cli/actions.go b/internal/cli/actions.go index 2b922dac6..57c501e34 100644 --- a/internal/cli/actions.go +++ b/internal/cli/actions.go @@ -202,7 +202,7 @@ func createActionCmd(cli *cli) *cobra.Command { Example: ` auth0 actions create auth0 actions create --name myaction auth0 actions create --name myaction --trigger post-login - auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js) --runtime node18 + auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --runtime node18 auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value" auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value" diff --git a/internal/cli/event_streams.go b/internal/cli/event_streams.go index aed6a23f9..72c0166fa 100644 --- a/internal/cli/event_streams.go +++ b/internal/cli/event_streams.go @@ -185,7 +185,8 @@ func createEventStreamCmd(cli *cli) *cobra.Command { Short: "Create a new event stream", Long: "Create a new event stream.\n\n" + "To create interactively, use `auth0 event-streams create` with no flags.\n\n" + - "To create non-interactively, supply the event stream name, type, subscriptions and configuration through the flags.", + "To create non-interactively, supply the event stream name, type, subscriptions and configuration through the flags. \n" + + "Only deployed actions can be used for action configuration", Example: ` auth0 event-streams create auth0 event-streams create --name my-event-stream --type eventbridge --subscriptions "user.created,user.updated" --configuration '{"aws_account_id":"325235643634","aws_region":"us-east-2"}' auth0 event-streams create --name my-event-stream --type webhook --subscriptions "user.created,user.deleted" --configuration '{"webhook_endpoint":"https://mywebhook.net","webhook_authorization":{"method":"bearer","token":"123456789"}}' @@ -270,7 +271,8 @@ func updateEventStreamCmd(cli *cli) *cobra.Command { "To update non-interactively, supply the event id, name, status, subscriptions and " + "configuration through the flags. An event stream type CANNOT be updated hence the configuration " + "should match the schema based on the type of event stream. Configuration for `eventbridge` streams " + - "cannot be updated.", + "cannot be updated." + + "Only deployed actions can be used for action configuration", Example: ` auth0 event-streams update auth0 event-streams update --name my-event-stream auth0 event-streams update --name my-event-stream --status enabled From 7f9e2c935e397cea2c54c063cbe503f977b76c4f Mon Sep 17 00:00:00 2001 From: Rajat Bajaj Date: Mon, 11 May 2026 23:50:17 +0530 Subject: [PATCH 3/4] feat: Support streaming of live subscribed events --- docs/auth0_actions_create.md | 2 +- docs/auth0_event-streams.md | 1 + docs/auth0_event-streams_create.md | 4 +- docs/auth0_event-streams_delete.md | 1 + docs/auth0_event-streams_list.md | 1 + docs/auth0_event-streams_redeliver-many.md | 1 + docs/auth0_event-streams_redeliver.md | 1 + docs/auth0_event-streams_show.md | 1 + docs/auth0_event-streams_stats.md | 1 + docs/auth0_event-streams_subscribe.md | 75 ++++ docs/auth0_event-streams_trigger.md | 1 + docs/auth0_event-streams_update.md | 3 +- go.mod | 2 +- go.sum | 4 +- internal/auth/auth.go | 2 +- internal/auth0/auth0.go | 2 + internal/auth0/events.go | 24 ++ internal/cli/event_streams.go | 1 + internal/cli/event_streams_subscribe.go | 397 +++++++++++++++++++++ 19 files changed, 517 insertions(+), 7 deletions(-) create mode 100644 docs/auth0_event-streams_subscribe.md create mode 100644 internal/auth0/events.go create mode 100644 internal/cli/event_streams_subscribe.go diff --git a/docs/auth0_actions_create.md b/docs/auth0_actions_create.md index c57568aea..478d85d30 100644 --- a/docs/auth0_actions_create.md +++ b/docs/auth0_actions_create.md @@ -22,7 +22,7 @@ auth0 actions create [flags] auth0 actions create auth0 actions create --name myaction auth0 actions create --name myaction --trigger post-login - auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js) --runtime node18 + auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --runtime node18 auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value" auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value" diff --git a/docs/auth0_event-streams.md b/docs/auth0_event-streams.md index 5d4aa56df..8ef0d8c6f 100644 --- a/docs/auth0_event-streams.md +++ b/docs/auth0_event-streams.md @@ -17,6 +17,7 @@ Events are a way for Auth0 customers to synchronize, correlate or orchestrate ch - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_create.md b/docs/auth0_event-streams_create.md index 556c9a456..3c2b79c6f 100644 --- a/docs/auth0_event-streams_create.md +++ b/docs/auth0_event-streams_create.md @@ -9,7 +9,8 @@ Create a new event stream. To create interactively, use `auth0 event-streams create` with no flags. -To create non-interactively, supply the event stream name, type, subscriptions and configuration through the flags. +To create non-interactively, supply the event stream name, type, subscriptions and configuration through the flags. +Only deployed actions can be used for action configuration ## Usage ``` @@ -61,6 +62,7 @@ auth0 event-streams create [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_delete.md b/docs/auth0_event-streams_delete.md index ffa99d35b..be747e8e4 100644 --- a/docs/auth0_event-streams_delete.md +++ b/docs/auth0_event-streams_delete.md @@ -55,6 +55,7 @@ auth0 event-streams delete [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_list.md b/docs/auth0_event-streams_list.md index b28e70822..be407dc1c 100644 --- a/docs/auth0_event-streams_list.md +++ b/docs/auth0_event-streams_list.md @@ -52,6 +52,7 @@ auth0 event-streams list [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_redeliver-many.md b/docs/auth0_event-streams_redeliver-many.md index c936f60d2..66c083923 100644 --- a/docs/auth0_event-streams_redeliver-many.md +++ b/docs/auth0_event-streams_redeliver-many.md @@ -53,6 +53,7 @@ auth0 event-streams redeliver-many [stream-id] [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_redeliver.md b/docs/auth0_event-streams_redeliver.md index 6d63bf677..6810b7203 100644 --- a/docs/auth0_event-streams_redeliver.md +++ b/docs/auth0_event-streams_redeliver.md @@ -44,6 +44,7 @@ auth0 event-streams redeliver [stream-id] [comma-separated-delivery-ids] [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_show.md b/docs/auth0_event-streams_show.md index 89708ffbc..3d900e413 100644 --- a/docs/auth0_event-streams_show.md +++ b/docs/auth0_event-streams_show.md @@ -50,6 +50,7 @@ auth0 event-streams show [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_stats.md b/docs/auth0_event-streams_stats.md index 0daf07924..1a6808baf 100644 --- a/docs/auth0_event-streams_stats.md +++ b/docs/auth0_event-streams_stats.md @@ -51,6 +51,7 @@ auth0 event-streams stats [stream-id] [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_subscribe.md b/docs/auth0_event-streams_subscribe.md new file mode 100644 index 000000000..169441139 --- /dev/null +++ b/docs/auth0_event-streams_subscribe.md @@ -0,0 +1,75 @@ +--- +layout: default +parent: auth0 event-streams +has_toc: false +--- +# auth0 event-streams subscribe + +Subscribe to events emitted by your tenant via Server-Sent Events (SSE). + +By default, every received event is rendered as a single, color-coded summary line: + TIME TYPE SOURCE EVENT-ID + +Use --verbose to also print the full JSON payload after each summary, or --json / --json-compact to emit raw JSON suitable for piping into `jq`. + +Heartbeat (`offset-only`) messages are suppressed by default and surfaced via a periodic faint indicator and a final cursor on disconnect; pass --show-heartbeats to render each one. Press Ctrl+C to disconnect; a per-type summary and the latest cursor will be printed so you can resume with --from. + +## Usage +``` +auth0 event-streams subscribe [flags] +``` + +## Examples + +``` + auth0 event-streams subscribe + auth0 event-streams subscribe --event-type user.created + auth0 event-streams subscribe --event-type user.created --event-type user.updated + auth0 event-streams subscribe --from-timestamp 2026-05-01T00:00:00Z + auth0 event-streams subscribe --from + auth0 event-streams subscribe -v + auth0 event-streams subscribe --show-heartbeats + auth0 event-streams subscribe --output-file events.jsonl + auth0 event-streams subscribe --json | jq . +``` + + +## Flags + +``` + --event-type strings Event type(s) to listen for. Specify multiple times for multiple types (e.g. --event-type user.created --event-type user.updated). If not provided, all event types are streamed. + --from offset Opaque cursor token representing the position in the stream. If not provided, the stream starts from the latest events. Use the offset printed when the connection ends to resume from where you left off. + --from-timestamp string RFC-3339 timestamp indicating where to start streaming events from. Use this on the initial query when no cursor (--from) is available; prefer --from on subsequent runs as it is more accurate. + --json Output each event as JSON (one indented object per event). + --json-compact Output each event as compact, single-line JSON (newline-delimited). + --output-file string Append every received event as a JSON line to this file (raw payload). Independent of the stdout format. + --show-heartbeats offset-only Show every offset-only heartbeat as its own line. By default heartbeats are silently tracked and only the latest cursor is reported on disconnect. + -v, --verbose Print the full JSON payload after each event summary line. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 event-streams create](auth0_event-streams_create.md) - Create a new event stream +- [auth0 event-streams delete](auth0_event-streams_delete.md) - Delete an event stream +- [auth0 event-streams deliveries](auth0_event-streams_deliveries.md) - Manage event stream deliveries +- [auth0 event-streams list](auth0_event-streams_list.md) - List your event streams +- [auth0 event-streams redeliver](auth0_event-streams_redeliver.md) - Retry one or more event deliveries for a given stream +- [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters +- [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream +- [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) +- [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream +- [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream + + diff --git a/docs/auth0_event-streams_trigger.md b/docs/auth0_event-streams_trigger.md index c35038fc5..7df3381e3 100644 --- a/docs/auth0_event-streams_trigger.md +++ b/docs/auth0_event-streams_trigger.md @@ -51,6 +51,7 @@ auth0 event-streams trigger [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/docs/auth0_event-streams_update.md b/docs/auth0_event-streams_update.md index 4c37a72ac..4a13c67b4 100644 --- a/docs/auth0_event-streams_update.md +++ b/docs/auth0_event-streams_update.md @@ -9,7 +9,7 @@ Update an event stream. To update interactively, use `auth0 event-streams update` with no arguments. -To update non-interactively, supply the event id, name, status, subscriptions and configuration through the flags. An event stream type CANNOT be updated hence the configuration should match the schema based on the type of event stream. Configuration for `eventbridge` streams cannot be updated. +To update non-interactively, supply the event id, name, status, subscriptions and configuration through the flags. An event stream type CANNOT be updated hence the configuration should match the schema based on the type of event stream. Configuration for `eventbridge` streams cannot be updated.Only deployed actions can be used for action configuration ## Usage ``` @@ -64,6 +64,7 @@ auth0 event-streams update [flags] - [auth0 event-streams redeliver-many](auth0_event-streams_redeliver-many.md) - Bulk retry failed event deliveries using filters - [auth0 event-streams show](auth0_event-streams_show.md) - Show an event stream - [auth0 event-streams stats](auth0_event-streams_stats.md) - View delivery stats for an event stream +- [auth0 event-streams subscribe](auth0_event-streams_subscribe.md) - Subscribe to live events via Server-Sent Events (SSE) - [auth0 event-streams trigger](auth0_event-streams_trigger.md) - Trigger a test event for an event stream - [auth0 event-streams update](auth0_event-streams_update.md) - Update an event stream diff --git a/go.mod b/go.mod index f445941c8..5bed6224a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 github.com/auth0/go-auth0 v1.36.0 - github.com/auth0/go-auth0/v2 v2.7.0 + github.com/auth0/go-auth0/v2 v2.10.0 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v1.0.0 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index 561534c84..0b74c9444 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/auth0/go-auth0 v1.36.0 h1:B3gxl26i4HLKoUmpEF4KrtsEA0Tx0Quxci+9f8/Lvfg= github.com/auth0/go-auth0 v1.36.0/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= -github.com/auth0/go-auth0/v2 v2.7.0 h1:uwY9yWGbtuU+M5z8GtK+smgUleu7+gfCA8hIpbxxd+Y= -github.com/auth0/go-auth0/v2 v2.7.0/go.mod h1:Q/Y3VZVoI3sw87VyTPhx2TQL6Sq4Q/iCP67rW2gcn+M= +github.com/auth0/go-auth0/v2 v2.10.0 h1:fPiIE/QusagbRTBC0mTdmF2rfx7Flt+4ei0gmvTKPTw= +github.com/auth0/go-auth0/v2 v2.10.0/go.mod h1:Q/Y3VZVoI3sw87VyTPhx2TQL6Sq4Q/iCP67rW2gcn+M= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 2f4c3a393..eaf8cfa2b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -146,7 +146,7 @@ var RequiredScopes = []string{ "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", "read:organization_connections", "read:prompts", "update:prompts", "read:attack_protection", "update:attack_protection", - "read:event_streams", "create:event_streams", "update:event_streams", "delete:event_streams", + "read:event_streams", "create:event_streams", "update:event_streams", "delete:event_streams", "read:events", "read:network_acls", "create:network_acls", "update:network_acls", "delete:network_acls", "read:token_exchange_profiles", "create:token_exchange_profiles", "update:token_exchange_profiles", "delete:token_exchange_profiles", "read:organization_invitations", "create:organization_invitations", "delete:organization_invitations", diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 4f1f7eba8..8f048f060 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -79,11 +79,13 @@ func NewAPI(m *management.Management) *API { type APIV2 struct { AttackProtectionBotDetection AttackProtectionBotDetectionAPIV2 + Events EventsAPIV2 } func NewAPIV2(m *managementv2.Management) *APIV2 { return &APIV2{ AttackProtectionBotDetection: m.AttackProtection.BotDetection, + Events: m.Events, } } diff --git a/internal/auth0/events.go b/internal/auth0/events.go new file mode 100644 index 000000000..bdf9404ca --- /dev/null +++ b/internal/auth0/events.go @@ -0,0 +1,24 @@ +package auth0 + +import ( + "context" + + managementv2 "github.com/auth0/go-auth0/v2/management" + "github.com/auth0/go-auth0/v2/management/core" + "github.com/auth0/go-auth0/v2/management/option" +) + +// EventsAPIV2 is the V2 SDK interface for the /events endpoint +// (Server-Sent Event subscription stream). +type EventsAPIV2 interface { + // Subscribe to events via Server-Sent Events (SSE). + // + // Required scope: `read:events` + // + // See: https://auth0.com/docs/api/management/v2/events/get-events + Subscribe( + ctx context.Context, + request *managementv2.SubscribeEventsRequestParameters, + opts ...option.RequestOption, + ) (*core.Stream[managementv2.EventStreamSubscribeEventsResponseContent], error) +} diff --git a/internal/cli/event_streams.go b/internal/cli/event_streams.go index 72c0166fa..7b032a9cc 100644 --- a/internal/cli/event_streams.go +++ b/internal/cli/event_streams.go @@ -92,6 +92,7 @@ func eventStreamsCmd(cli *cli) *cobra.Command { cmd.AddCommand(redeliverEventStreamCmd(cli)) cmd.AddCommand(redeliverManyEventStreamCmd(cli)) cmd.AddCommand(statsEventStreamCmd(cli)) + cmd.AddCommand(subscribeEventStreamCmd(cli)) return cmd } diff --git a/internal/cli/event_streams_subscribe.go b/internal/cli/event_streams_subscribe.go new file mode 100644 index 000000000..bad150439 --- /dev/null +++ b/internal/cli/event_streams_subscribe.go @@ -0,0 +1,397 @@ +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/signal" + "sort" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + managementv2 "github.com/auth0/go-auth0/v2/management" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +var ( + eventSubscribeFrom = Flag{ + Name: "From", + LongForm: "from", + Help: "Opaque cursor token representing the position in the stream. " + + "If not provided, the stream starts from the latest events. Use the " + + "`offset` printed when the connection ends to resume from where you left off.", + } + + eventSubscribeFromTimestamp = Flag{ + Name: "From Timestamp", + LongForm: "from-timestamp", + Help: "RFC-3339 timestamp indicating where to start streaming events from. " + + "Use this on the initial query when no cursor (--from) is available; " + + "prefer --from on subsequent runs as it is more accurate.", + } + + eventSubscribeEventType = Flag{ + Name: "Event Type", + LongForm: "event-type", + Help: "Event type(s) to listen for. Specify multiple times for multiple types " + + "(e.g. --event-type user.created --event-type user.updated). " + + "If not provided, all event types are streamed.", + } + + eventSubscribeVerbose = Flag{ + Name: "Verbose", + LongForm: "verbose", + ShortForm: "v", + Help: "Print the full JSON payload after each event summary line.", + } + + eventSubscribeShowHeartbeats = Flag{ + Name: "Show Heartbeats", + LongForm: "show-heartbeats", + Help: "Show every `offset-only` heartbeat as its own line. " + + "By default heartbeats are silently tracked and only the latest cursor " + + "is reported on disconnect.", + } + + eventSubscribeOutputFile = Flag{ + Name: "Output File", + LongForm: "output-file", + Help: "Append every received event as a JSON line to this file (raw payload). " + + "Independent of the stdout format.", + } +) + +type subscribeInputs struct { + From string + FromTimestamp string + EventTypes []string + Verbose bool + ShowHeartbeats bool + OutputFile string +} + +func subscribeEventStreamCmd(cli *cli) *cobra.Command { + var inputs subscribeInputs + + cmd := &cobra.Command{ + Use: "subscribe", + Args: cobra.NoArgs, + Short: "Subscribe to live events via Server-Sent Events (SSE)", + Long: "Subscribe to events emitted by your tenant via Server-Sent Events (SSE).\n\n" + + "By default, every received event is rendered as a single, color-coded summary line:\n" + + " TIME TYPE SOURCE EVENT-ID\n\n" + + "Use --verbose to also print the full JSON payload after each summary, " + + "or --json / --json-compact to emit raw JSON suitable for piping into `jq`.\n\n" + + "Heartbeat (`offset-only`) messages are suppressed by default and surfaced via " + + "a periodic faint indicator and a final cursor on disconnect; pass --show-heartbeats " + + "to render each one. Press Ctrl+C to disconnect; a per-type summary and the " + + "latest cursor will be printed so you can resume with --from.", + Example: ` auth0 event-streams subscribe + auth0 event-streams subscribe --event-type user.created + auth0 event-streams subscribe --event-type user.created --event-type user.updated + auth0 event-streams subscribe --from-timestamp 2026-05-01T00:00:00Z + auth0 event-streams subscribe --from + auth0 event-streams subscribe -v + auth0 event-streams subscribe --show-heartbeats + auth0 event-streams subscribe --output-file events.jsonl + auth0 event-streams subscribe --json | jq .`, + RunE: func(cmd *cobra.Command, args []string) error { + req := &managementv2.SubscribeEventsRequestParameters{} + + if inputs.From != "" { + req.From = &inputs.From + } + if inputs.FromTimestamp != "" { + req.FromTimestamp = &inputs.FromTimestamp + } + if len(inputs.EventTypes) > 0 { + eventTypes := make([]*managementv2.EventStreamSubscribeEventsEventTypeEnum, 0, len(inputs.EventTypes)) + for _, t := range inputs.EventTypes { + t = strings.TrimSpace(t) + if t == "" { + continue + } + enum, err := managementv2.NewEventStreamSubscribeEventsEventTypeEnumFromString(t) + if err != nil { + return fmt.Errorf("invalid --event-type value %q: %w", t, err) + } + eventTypes = append(eventTypes, enum.Ptr()) + } + req.EventType = eventTypes + } + + var outFile *os.File + if inputs.OutputFile != "" { + f, err := os.OpenFile(inputs.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("failed to open --output-file %q: %w", inputs.OutputFile, err) + } + defer func() { _ = f.Close() }() + outFile = f + } + + stream, err := cli.apiv2.Events.Subscribe(cmd.Context(), req) + if err != nil { + return fmt.Errorf("failed to subscribe to events: %w", err) + } + defer func() { _ = stream.Close() }() + + useJSON := cli.json || cli.jsonCompact + if !useJSON { + cli.renderer.Infof(ansi.Faint("Subscribed to event stream. Press Ctrl+C to disconnect.")) + } + + counts := map[string]int{} + var ( + totalEvents uint64 + heartbeats uint64 + lastOffset string + lastHeartbeatAt time.Time + countsMu sync.Mutex + summaryOnce sync.Once + streamClosed atomic.Bool + ) + + flushSummary := func() { + summaryOnce.Do(func() { + if useJSON { + return + } + countsMu.Lock() + defer countsMu.Unlock() + + cli.renderer.Newline() + cli.renderer.Infof(ansi.Bold("Disconnected. Summary:")) + cli.renderer.Infof(" Events received: %d", atomic.LoadUint64(&totalEvents)) + cli.renderer.Infof(" Heartbeats: %d", atomic.LoadUint64(&heartbeats)) + if len(counts) > 0 { + types := make([]string, 0, len(counts)) + for t := range counts { + types = append(types, t) + } + sort.Strings(types) + for _, t := range types { + cli.renderer.Infof(" %s %s %d", ansi.Faint("·"), t, counts[t]) + } + } + if lastOffset != "" { + cli.renderer.Newline() + cli.renderer.Infof("Resume with: %s", ansi.Cyan(fmt.Sprintf("auth0 event-streams subscribe --from %s", lastOffset))) + } + }) + } + + // The root command installs a SIGINT handler that calls os.Exit(0) + // from a goroutine, which would skip our deferred summary. Reset + // it first so only our handler runs, then print the summary and + // exit cleanly ourselves. + signal.Reset(os.Interrupt, syscall.SIGTERM) + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(sigCh) + go func() { + if _, ok := <-sigCh; !ok { + return + } + streamClosed.Store(true) + _ = stream.Close() + flushSummary() + os.Exit(0) + }() + + for { + event, err := stream.Recv() + if err != nil { + // Treat as graceful shutdown if: + // - EOF (server closed) + // - context cancelled + // - we closed the stream ourselves (Ctrl+C) + // - http2 body closed error (result of stream.Close()) + isGraceful := errors.Is(err, io.EOF) || + errors.Is(err, cmd.Context().Err()) || + cmd.Context().Err() != nil || + streamClosed.Load() || + strings.Contains(err.Error(), "response body closed") + if isGraceful { + flushSummary() + return nil + } + return fmt.Errorf("error receiving event: %w", err) + } + + summary := summarizeEvent(&event) + + if summary.offset != "" { + lastOffset = summary.offset + } + + // Always persist raw payload to file if requested. + if outFile != nil { + raw, mErr := json.Marshal(&event) + if mErr == nil { + _, _ = outFile.Write(raw) + _, _ = outFile.WriteString("\n") + } + } + + if useJSON { + if cli.jsonCompact { + cli.renderer.JSONCompactResult(&event) + } else { + cli.renderer.JSONResult(&event) + } + continue + } + + if summary.isHeartbeat { + atomic.AddUint64(&heartbeats, 1) + if inputs.ShowHeartbeats { + renderHeartbeatLine(cli, summary) + } else if time.Since(lastHeartbeatAt) > 30*time.Second { + // Periodic faint pulse so users know the connection is alive. + lastHeartbeatAt = time.Now() + renderHeartbeatPulse(cli, summary) + } + continue + } + + atomic.AddUint64(&totalEvents, 1) + countsMu.Lock() + counts[summary.eventType]++ + countsMu.Unlock() + + renderEventSummary(cli, summary) + + if inputs.Verbose { + payload, mErr := json.MarshalIndent(&event, "", " ") + if mErr != nil { + cli.renderer.Warnf("failed to marshal event payload: %v", mErr) + } else { + cli.renderer.Output(ansi.ColorizeJSON(string(payload))) + } + } + } + }, + } + + eventSubscribeFrom.RegisterString(cmd, &inputs.From, "") + eventSubscribeFromTimestamp.RegisterString(cmd, &inputs.FromTimestamp, "") + eventSubscribeEventType.RegisterStringSlice(cmd, &inputs.EventTypes, nil) + eventSubscribeVerbose.RegisterBool(cmd, &inputs.Verbose, false) + eventSubscribeShowHeartbeats.RegisterBool(cmd, &inputs.ShowHeartbeats, false) + eventSubscribeOutputFile.RegisterString(cmd, &inputs.OutputFile, "") + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output each event as JSON (one indented object per event).") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output each event as compact, single-line JSON (newline-delimited).") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact") + + return cmd +} + +// eventSummary is a generic, payload-agnostic projection of an SSE message +// extracted by re-marshalling the SDK union type and pulling the standard +// CloudEvents envelope fields. +type eventSummary struct { + eventType string + isHeartbeat bool + offset string + id string + source string + time time.Time +} + +func summarizeEvent(ev *managementv2.EventStreamSubscribeEventsResponseContent) eventSummary { + s := eventSummary{eventType: ev.GetType()} + if s.eventType == "offset-only" { + s.isHeartbeat = true + if oo := ev.GetOffsetOnly(); oo != nil { + s.offset = oo.GetOffset() + } + return s + } + + // All concrete event payloads share the same shape: + // { "type": "...", "offset": "...", "event": { "id", "time", "source", ... } } + // Re-marshal once and extract the envelope generically so we don't have + // to switch on every event variant. + raw, err := json.Marshal(ev) + if err != nil { + return s + } + var envelope struct { + Offset string `json:"offset"` + Event struct { + ID string `json:"id"` + Source string `json:"source"` + Time time.Time `json:"time"` + } `json:"event"` + } + if err := json.Unmarshal(raw, &envelope); err != nil { + return s + } + s.offset = envelope.Offset + s.id = envelope.Event.ID + s.source = envelope.Event.Source + s.time = envelope.Event.Time + return s +} + +func renderEventSummary(cli *cli, s eventSummary) { + ts := s.time + if ts.IsZero() { + ts = time.Now() + } + line := fmt.Sprintf( + "%s %s %s %s", + ansi.Faint(ts.Local().Format("15:04:05")), + ansi.Bold(colorForEventType(s.eventType)), + s.source, + ansi.Faint(s.id), + ) + cli.renderer.Output(line) +} + +func renderHeartbeatLine(cli *cli, s eventSummary) { + cli.renderer.Output(fmt.Sprintf( + "%s %s %s", + ansi.Faint(time.Now().Local().Format("15:04:05")), + ansi.Faint("heartbeat"), + ansi.Faint(shortOffset(s.offset)), + )) +} + +func renderHeartbeatPulse(cli *cli, s eventSummary) { + cli.renderer.Output(ansi.Faint(fmt.Sprintf( + "· still listening (cursor %s)", + shortOffset(s.offset), + ))) +} + +func shortOffset(o string) string { + if len(o) <= 12 { + return o + } + return o[:8] + "…" + o[len(o)-4:] +} + +func colorForEventType(t string) string { + switch { + case strings.HasSuffix(t, ".created") || strings.HasSuffix(t, ".added") || strings.HasSuffix(t, ".assigned"): + return ansi.Green(t) + case strings.HasSuffix(t, ".updated"): + return ansi.Cyan(t) + case strings.HasSuffix(t, ".deleted") || strings.HasSuffix(t, ".removed"): + return ansi.Red(t) + case t == "error": + return ansi.BrightRed(t) + default: + return t + } +} From c7b8333402e9dbdcc44ed93377e04898c016eeeb Mon Sep 17 00:00:00 2001 From: Rajat Bajaj Date: Thu, 14 May 2026 11:17:04 +0530 Subject: [PATCH 4/4] Lint --- internal/cli/event_streams_subscribe.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/cli/event_streams_subscribe.go b/internal/cli/event_streams_subscribe.go index bad150439..fcf605d6e 100644 --- a/internal/cli/event_streams_subscribe.go +++ b/internal/cli/event_streams_subscribe.go @@ -209,11 +209,11 @@ func subscribeEventStreamCmd(cli *cli) *cobra.Command { for { event, err := stream.Recv() if err != nil { - // Treat as graceful shutdown if: - // - EOF (server closed) - // - context cancelled - // - we closed the stream ourselves (Ctrl+C) - // - http2 body closed error (result of stream.Close()) + /* Treat as graceful shutdown if + - EOF (server closed) + - context cancelled + - we closed the stream ourselves (Ctrl+C) + - http2 body closed error (result of stream.Close()) */ isGraceful := errors.Is(err, io.EOF) || errors.Is(err, cmd.Context().Err()) || cmd.Context().Err() != nil ||