Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions lib/Events.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
<?php

namespace WorkOS;

/**
* Class Events
*
* This class facilitates the use of WorkOS Events API.
*
* Security considerations:
* - All input parameters are validated and sanitized
* - Event types are validated against a whitelist
* - Parameter limits are enforced to prevent abuse
* - API authentication is handled by the Client class
*/
class Events
{
/**
* Maximum number of events that can be requested in a single API call
*/
private const MAX_EVENTS_LIMIT = 100;

/**
* List events with optional filtering and pagination.
*
* @param array $params Optional parameters:
* - **events** (string|array): Comma-separated string or array of event types to filter by (required - at least one event type must be specified)
* - **organization_id** (string): Filter events by organization ID
* - **limit** (int): Number of events to return (default: 50, max: 100)
* - **after** (string): Cursor for pagination (object ID)
* - **before** (string): Cursor for pagination (object ID)
* - **order** (string): Sort order - 'asc' or 'desc' (default: 'desc')
*
* @throws Exception\WorkOSException
*
* @return array<string, mixed>
*/
public function listEvents($params = [])
{
$eventsPath = "events";

// Validate and sanitize input parameters
$params = $this->validateAndSanitizeParams($params);

// Handle events parameter - convert array to comma-separated string
if (isset($params['events']) && is_array($params['events'])) {
$params['events'] = implode(',', $params['events']);
}

// Note: The WorkOS Events API requires at least one event type to be specified
// If no events parameter is provided, the API will return a 400 error
// Consider using getValidEventTypes() to get available event types

return Client::request(Client::METHOD_GET, $eventsPath, null, $params, true);
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: The Events API only returns list_metadata.after (no before). To stay consistent with other list methods in this SDK (which all return [$before, $after,
$resources]), consider:

Suggested change
return Client::request(Client::METHOD_GET, $eventsPath, null, $params, true);
$response = Client::request(Client::METHOD_GET, $eventsPath, null, $params, true);
$events = [];
foreach ($response["data"] as $responseData) {
\array_push($events, Resource\Event::constructFromResponse($responseData));
}
$after = $response["list_metadata"]["after"] ?? null;
return [null, $after, $events];

This gives users:

  • Consistent return signature across all list methods
  • Properly typed Resource\Event objects instead of raw arrays
  • Clear indication that before isn't supported (always null)

}



/**
* Get available event types.
*
* @return array<string> Array of valid event types
*/
public function getValidEventTypes()
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit worried about the maintenance burden this creates. Whenever new events are added, they won't be available to use here until this list is updated and you update to that new version of the SDK.

The Node and Ruby SDKs skip this entirely and just pass strings to the API. I think that's valid but but not the
best DX. Instead, what if we keep the constants but remove the validation:

/**
 * Common event types for IDE autocomplete.
 * This list is NOT exhaustive - the API accepts additional values.
 * @see https://workos.com/docs/events
 */
class EventTypes
{
    public const USER_CREATED = 'user.created';
    public const USER_UPDATED = 'user.updated';
    // etc...
}

Then in listEvents(), remove validateAndSanitizeParams() and just pass parameters to the API. Let the API be the source of truth for validation.

This gives PHP developers IDE autocomplete without blocking them from using new events before SDK updates. It also aligns with how Node, Ruby, and Go SDKs handle this.

Methods to remove:

  • validateEventTypes()
  • validateAndSanitizeParams()
  • getEventsByType() (just use listEvents() directly)

What are your thoughts?

{
return [
// Authentication Events
'authentication.email_verification_succeeded',
'authentication.magic_auth_failed',
'authentication.magic_auth_succeeded',
'authentication.mfa_failed',
'authentication.mfa_succeeded',
'authentication.oauth_failed',
'authentication.oauth_succeeded',
'authentication.password_failed',
'authentication.password_succeeded',
'authentication.passkey_failed',
'authentication.passkey_succeeded',
'authentication.sso_failed',
'authentication.sso_succeeded',
'authentication.radar_risk_detected',

// Connection Events
'connection.activated',
'connection.deactivated',
'connection.deleted',
'connection.saml_certificate_renewed',
'connection.saml_certificate_renewal_required',

// DSync Events
'dsync.activated',
'dsync.deleted',
'dsync.group.created',
'dsync.group.deleted',
'dsync.group.updated',
'dsync.group.user_added',
'dsync.group.user_removed',
'dsync.user.created',
'dsync.user.deleted',
'dsync.user.updated',

// Email Verification Events
'email_verification.created',

// Flag Events
'flag.created',
'flag.updated',
'flag.deleted',
'flag.rule_updated',

// Invitation Events
'invitation.accepted',
'invitation.created',
'invitation.revoked',

// Organization Events
'organization.created',
'organization.updated',
'organization.deleted',
'organization_domain.created',
'organization_domain.updated',
'organization_domain.deleted',
'organization_domain.verified',
'organization_domain.verification_failed',
'organization_membership.created',
'organization_membership.deleted',
'organization_membership.updated',

// Password Reset Events
'password_reset.created',
'password_reset.succeeded',

// Role Events
'role.created',
'role.deleted',
'role.updated',

// Session Events
'session.created',
'session.revoked',

// User Events
'user.created',
'user.deleted',
'user.updated',
];
}

/**
* Validate event types against the list of valid types.
*
* @param string|array $eventTypes Event type(s) to validate
*
* @return bool True if all event types are valid
*/
public function validateEventTypes($eventTypes)
{
// Handle null or empty input
if (empty($eventTypes)) {
return false;
}

$validTypes = $this->getValidEventTypes();

if (is_string($eventTypes)) {
$eventTypes = explode(',', $eventTypes);
}

// Ensure we have an array
if (!is_array($eventTypes)) {
return false;
}

foreach ($eventTypes as $eventType) {
$eventType = trim($eventType);

// Skip empty strings after trimming
if (empty($eventType)) {
continue;
}

// Validate against whitelist
if (!in_array($eventType, $validTypes, true)) {
return false;
}
}

return true;
}

/**
* Get events filtered by type.
*
* @param string|array $eventTypes Required event types to filter by (comma-separated string or array)
* @param array $params Optional parameters (same as listEvents)
*
* @throws Exception\BadRequestException if invalid event types are provided
* @throws Exception\WorkOSException
*
* @return array Events filtered by the specified types
*/
public function getEventsByType($eventTypes, $params = [])
{
// Validate event types before proceeding
if (!$this->validateEventTypes($eventTypes)) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400)
);
}

// Add the required events parameter
$params['events'] = $eventTypes;

return $this->listEvents($params);
}

/**
* Validate and sanitize input parameters for API requests.
*
* @param array $params Raw input parameters
*
* @return array Sanitized parameters
*
* @throws Exception\BadRequestException if invalid parameters are provided
*/
private function validateAndSanitizeParams($params)
{
$sanitized = [];

// Validate limit parameter
if (isset($params['limit'])) {
$limit = filter_var($params['limit'], FILTER_VALIDATE_INT);
if ($limit === false || $limit < 1 || $limit > self::MAX_EVENTS_LIMIT) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'Limit must be an integer between 1 and ' . self::MAX_EVENTS_LIMIT]), [], 400)
);
}
$sanitized['limit'] = $limit;
}

// Validate order parameter
if (isset($params['order'])) {
$order = strtolower(trim($params['order']));
if (!in_array($order, ['asc', 'desc'], true)) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'Order must be "asc" or "desc"']), [], 400)
);
}
$sanitized['order'] = $order;
}

// Validate organization_id parameter
if (isset($params['organization_id'])) {
$orgId = trim($params['organization_id']);
if (empty($orgId) || strlen($orgId) > 255) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'Organization ID must be a non-empty string up to 255 characters']), [], 400)
);
}
$sanitized['organization_id'] = $orgId;
}

// Validate after/before cursor parameters
if (isset($params['after'])) {
$after = trim($params['after']);
if (strlen($after) > 255) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'After cursor must be a string up to 255 characters']), [], 400)
);
}
$sanitized['after'] = $after;
}

if (isset($params['before'])) {
$before = trim($params['before']);
if (strlen($before) > 255) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'Before cursor must be a string up to 255 characters']), [], 400)
);
}
$sanitized['before'] = $before;
}

// Validate events parameter
if (isset($params['events'])) {
$events = $params['events'];

// Convert string to array for validation
if (is_string($events)) {
$events = explode(',', $events);
}

if (!is_array($events) || empty($events)) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'Events parameter must be a non-empty array or comma-separated string']), [], 400)
);
}

// Validate each event type
if (!$this->validateEventTypes($events)) {
throw new Exception\BadRequestException(
new Resource\Response(json_encode(['error' => 'One or more event types are invalid']), [], 400)
);
}

$sanitized['events'] = $events;
}

return $sanitized;
}

}
Loading