diff --git a/src/common/interfaces/event.interface.ts b/src/common/interfaces/event.interface.ts index 6a1d12b1f..53ae0f37c 100644 --- a/src/common/interfaces/event.interface.ts +++ b/src/common/interfaces/event.interface.ts @@ -57,6 +57,7 @@ import { FeatureFlagResponse, } from '../../feature-flags/interfaces'; import { Group, GroupResponse } from '../../groups/interfaces'; +import { ConnectedAccountState } from '../../pipes/interfaces'; import { VaultDataCreatedEventData, VaultDataCreatedEventResponseData, @@ -817,6 +818,100 @@ export interface GroupMemberRemovedEventResponse extends EventResponseBase { data: GroupMemberEventResponseData; } +export type PipesConnectedAccountState = ConnectedAccountState | 'disconnected'; + +export interface PipesConnectedAccount { + object: 'connected_account'; + id: string; + dataIntegrationId: string; + providerSlug: string; + userId: string | null; + organizationId: string | null; + scopes: string[]; + state: PipesConnectedAccountState; + createdAt: string; + updatedAt: string; +} + +export interface PipesConnectedAccountResponse { + object: 'connected_account'; + id: string; + data_integration_id: string; + provider_slug: string; + user_id: string | null; + organization_id: string | null; + scopes: string[]; + state: PipesConnectedAccountState; + created_at: string; + updated_at: string; +} + +export interface PipesConnectionFailed { + object: 'connection_failed'; + dataIntegrationId: string; + providerSlug: string; + userId: string | null; + organizationId: string | null; + errorCode: string; + errorReason: string | null; + providerError: string | null; + providerErrorDescription: string | null; + createdAt: string; +} + +export interface PipesConnectionFailedResponse { + object: 'connection_failed'; + data_integration_id: string; + provider_slug: string; + user_id: string | null; + organization_id: string | null; + error_code: string; + error_reason: string | null; + provider_error: string | null; + provider_error_description: string | null; + created_at: string; +} + +export interface PipesConnectedAccountConnectedEvent extends EventBase { + event: 'pipes.connected_account.connected'; + data: PipesConnectedAccount; +} + +export interface PipesConnectedAccountConnectedEventResponse extends EventResponseBase { + event: 'pipes.connected_account.connected'; + data: PipesConnectedAccountResponse; +} + +export interface PipesConnectedAccountConnectionFailedEvent extends EventBase { + event: 'pipes.connected_account.connection_failed'; + data: PipesConnectionFailed; +} + +export interface PipesConnectedAccountConnectionFailedEventResponse extends EventResponseBase { + event: 'pipes.connected_account.connection_failed'; + data: PipesConnectionFailedResponse; +} + +export interface PipesConnectedAccountDisconnectedEvent extends EventBase { + event: 'pipes.connected_account.disconnected'; + data: PipesConnectedAccount; +} + +export interface PipesConnectedAccountDisconnectedEventResponse extends EventResponseBase { + event: 'pipes.connected_account.disconnected'; + data: PipesConnectedAccountResponse; +} + +export interface PipesConnectedAccountReauthorizationNeededEvent extends EventBase { + event: 'pipes.connected_account.reauthorization_needed'; + data: PipesConnectedAccount; +} + +export interface PipesConnectedAccountReauthorizationNeededEventResponse extends EventResponseBase { + event: 'pipes.connected_account.reauthorization_needed'; + data: PipesConnectedAccountResponse; +} + export interface VaultDataCreatedEvent extends EventBase { event: 'vault.data.created'; data: VaultDataCreatedEventData; @@ -993,6 +1088,10 @@ export type Event = | GroupDeletedEvent | GroupMemberAddedEvent | GroupMemberRemovedEvent + | PipesConnectedAccountConnectedEvent + | PipesConnectedAccountConnectionFailedEvent + | PipesConnectedAccountDisconnectedEvent + | PipesConnectedAccountReauthorizationNeededEvent | VaultDataCreatedEvent | VaultDataUpdatedEvent | VaultDataReadEvent @@ -1075,6 +1174,10 @@ export type EventResponse = | GroupDeletedEventResponse | GroupMemberAddedEventResponse | GroupMemberRemovedEventResponse + | PipesConnectedAccountConnectedEventResponse + | PipesConnectedAccountConnectionFailedEventResponse + | PipesConnectedAccountDisconnectedEventResponse + | PipesConnectedAccountReauthorizationNeededEventResponse | VaultDataCreatedEventResponse | VaultDataUpdatedEventResponse | VaultDataReadEventResponse diff --git a/src/common/serializers/event.serializer.ts b/src/common/serializers/event.serializer.ts index 11c26940b..7df974cc3 100644 --- a/src/common/serializers/event.serializer.ts +++ b/src/common/serializers/event.serializer.ts @@ -20,7 +20,15 @@ import { deserializeOrganizationDomain } from '../../organization-domains/serial import { deserializeOrganizationMembership } from '../../user-management/serializers/organization-membership.serializer'; import { deserializeRoleEvent } from '../../user-management/serializers/role.serializer'; import { deserializeSession } from '../../user-management/serializers/session.serializer'; -import { Event, EventBase, EventResponse } from '../interfaces'; +import { + Event, + EventBase, + EventResponse, + PipesConnectedAccount, + PipesConnectedAccountResponse, + PipesConnectionFailed, + PipesConnectionFailedResponse, +} from '../interfaces'; import { deserializeAuthenticationRadarRiskDetectedEvent } from '../../user-management/serializers/authentication-radar-risk-event-serializer'; import { deserializeApiKey } from '../../api-keys/serializers/api-key.serializer'; import { deserializeOrganizationRoleEvent } from '../../authorization/serializers/organization-role.serializer'; @@ -41,6 +49,36 @@ import { } from '../../vault/serializers/vault-event.serializer'; import { deserializeOrganizationDomainVerificationFailed } from '../../organization-domains/serializers/organization-domain-verification-failed.serializer'; +const deserializePipesConnectedAccount = ( + connectedAccount: PipesConnectedAccountResponse, +): PipesConnectedAccount => ({ + object: connectedAccount.object, + id: connectedAccount.id, + dataIntegrationId: connectedAccount.data_integration_id, + providerSlug: connectedAccount.provider_slug, + userId: connectedAccount.user_id, + organizationId: connectedAccount.organization_id, + scopes: connectedAccount.scopes, + state: connectedAccount.state, + createdAt: connectedAccount.created_at, + updatedAt: connectedAccount.updated_at, +}); + +const deserializePipesConnectionFailed = ( + connectionFailed: PipesConnectionFailedResponse, +): PipesConnectionFailed => ({ + object: connectionFailed.object, + dataIntegrationId: connectionFailed.data_integration_id, + providerSlug: connectionFailed.provider_slug, + userId: connectionFailed.user_id, + organizationId: connectionFailed.organization_id, + errorCode: connectionFailed.error_code, + errorReason: connectionFailed.error_reason, + providerError: connectionFailed.provider_error, + providerErrorDescription: connectionFailed.provider_error_description, + createdAt: connectionFailed.created_at, +}); + export const deserializeEvent = (event: EventResponse): Event => { const eventBase: EventBase = { id: event.id, @@ -261,6 +299,20 @@ export const deserializeEvent = (event: EventResponse): Event => { organizationMembershipId: event.data.organization_membership_id, }, }; + case 'pipes.connected_account.connected': + case 'pipes.connected_account.disconnected': + case 'pipes.connected_account.reauthorization_needed': + return { + ...eventBase, + event: event.event, + data: deserializePipesConnectedAccount(event.data), + }; + case 'pipes.connected_account.connection_failed': + return { + ...eventBase, + event: event.event, + data: deserializePipesConnectionFailed(event.data), + }; case 'vault.data.created': return { ...eventBase, diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index 2c9a16685..bc56cc6f4 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -18,6 +18,12 @@ import { OrganizationDomainCreatedEventResponse, OrganizationDomainVerificationFailedEvent, OrganizationDomainVerificationFailedEventResponse, + PipesConnectedAccountConnectedEvent, + PipesConnectedAccountConnectedEventResponse, + PipesConnectedAccountConnectionFailedEvent, + PipesConnectedAccountConnectionFailedEventResponse, + PipesConnectedAccountDisconnectedEvent, + PipesConnectedAccountDisconnectedEventResponse, VaultDataCreatedEvent, VaultDataCreatedEventResponse, VaultDataUpdatedEvent, @@ -169,6 +175,123 @@ describe('Event', () => { }); }); + describe('Pipes connected account events', () => { + it('accepts event filters and deserializes payloads', async () => { + const connectedResponse: PipesConnectedAccountConnectedEventResponse = { + id: 'event_connected', + created_at: '2026-08-25T12:00:00.000Z', + context: { client_id: 'client_123' }, + event: 'pipes.connected_account.connected', + data: { + object: 'connected_account', + id: 'data_installation_123', + data_integration_id: 'data_integration_123', + provider_slug: 'github', + user_id: 'user_123', + organization_id: null, + scopes: ['repo', 'user:email'], + state: 'connected', + created_at: '2026-08-25T11:00:00.000Z', + updated_at: '2026-08-25T12:00:00.000Z', + }, + }; + const disconnectedResponse: PipesConnectedAccountDisconnectedEventResponse = + { + ...connectedResponse, + id: 'event_disconnected', + event: 'pipes.connected_account.disconnected', + data: { ...connectedResponse.data, state: 'disconnected' }, + }; + const connectionFailedResponse: PipesConnectedAccountConnectionFailedEventResponse = + { + id: 'event_connection_failed', + created_at: '2026-08-25T13:00:00.000Z', + event: 'pipes.connected_account.connection_failed', + data: { + object: 'connection_failed', + data_integration_id: 'data_integration_123', + provider_slug: 'github', + user_id: 'user_123', + organization_id: null, + error_code: 'authorization_code_exchange_error', + error_reason: 'The authorization code has expired.', + provider_error: 'access_denied', + provider_error_description: + 'The user denied the authorization request.', + created_at: '2026-08-25T13:00:00.000Z', + }, + }; + const connected: PipesConnectedAccountConnectedEvent = { + id: 'event_connected', + createdAt: '2026-08-25T12:00:00.000Z', + context: { client_id: 'client_123' }, + event: 'pipes.connected_account.connected', + data: { + object: 'connected_account', + id: 'data_installation_123', + dataIntegrationId: 'data_integration_123', + providerSlug: 'github', + userId: 'user_123', + organizationId: null, + scopes: ['repo', 'user:email'], + state: 'connected', + createdAt: '2026-08-25T11:00:00.000Z', + updatedAt: '2026-08-25T12:00:00.000Z', + }, + }; + const disconnected: PipesConnectedAccountDisconnectedEvent = { + ...connected, + id: 'event_disconnected', + event: 'pipes.connected_account.disconnected', + data: { ...connected.data, state: 'disconnected' }, + }; + const connectionFailed: PipesConnectedAccountConnectionFailedEvent = { + id: 'event_connection_failed', + createdAt: '2026-08-25T13:00:00.000Z', + context: undefined, + event: 'pipes.connected_account.connection_failed', + data: { + object: 'connection_failed', + dataIntegrationId: 'data_integration_123', + providerSlug: 'github', + userId: 'user_123', + organizationId: null, + errorCode: 'authorization_code_exchange_error', + errorReason: 'The authorization code has expired.', + providerError: 'access_denied', + providerErrorDescription: + 'The user denied the authorization request.', + createdAt: '2026-08-25T13:00:00.000Z', + }, + }; + + fetchOnce({ + object: 'list', + data: [ + connectedResponse, + disconnectedResponse, + connectionFailedResponse, + ], + list_metadata: {}, + }); + + const list = await workos.events.listEvents({ + events: [ + 'pipes.connected_account.connected', + 'pipes.connected_account.connection_failed', + 'pipes.connected_account.disconnected', + 'pipes.connected_account.reauthorization_needed', + ], + }); + + expect(list).toEqual({ + object: 'list', + data: [connected, disconnected, connectionFailed], + listMetadata: {}, + }); + }); + }); + describe('feature flag events', () => { it('deserializes flag.created events', async () => { const flagCreatedResponse: FlagCreatedEventResponse = { diff --git a/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts b/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts index 138539523..7bedc0eeb 100644 --- a/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts +++ b/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts @@ -77,6 +77,8 @@ export const CreateWebhookEndpointEvents = { PermissionDeleted: 'permission.deleted', PermissionUpdated: 'permission.updated', PipesConnectedAccountConnected: 'pipes.connected_account.connected', + PipesConnectedAccountConnectionFailed: + 'pipes.connected_account.connection_failed', PipesConnectedAccountDisconnected: 'pipes.connected_account.disconnected', PipesConnectedAccountReauthorizationNeeded: 'pipes.connected_account.reauthorization_needed', diff --git a/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts b/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts index 5f99b45b6..7b435681f 100644 --- a/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts +++ b/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts @@ -77,6 +77,8 @@ export const UpdateWebhookEndpointEvents = { PermissionDeleted: 'permission.deleted', PermissionUpdated: 'permission.updated', PipesConnectedAccountConnected: 'pipes.connected_account.connected', + PipesConnectedAccountConnectionFailed: + 'pipes.connected_account.connection_failed', PipesConnectedAccountDisconnected: 'pipes.connected_account.disconnected', PipesConnectedAccountReauthorizationNeeded: 'pipes.connected_account.reauthorization_needed',