diff --git a/README.md b/README.md index 46bfeb60c..ffb275054 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,44 @@ rules; existing integrations do not need to change. `valueType` while the environment still uses the legacy target contract. Narrow using `'ruleId' in target` before accessing the contract-specific fields. +## Feature flag events + +The Events API and `webhooks.constructEvent` support the namespaced feature flag +lifecycle events: + +- `feature_flags.flag.created`, `feature_flags.flag.updated`, `feature_flags.flag.deleted` +- `feature_flags.flag_rule.created`, `feature_flags.flag_rule.updated`, `feature_flags.flag_rule.deleted` +- `feature_flags.flag_target.created`, `feature_flags.flag_target.deleted` + +Use these names in `events.listEvents({ events: [...] })`. The returned `Event` +union narrows by `event`; event data and context use camelCase fields. + +```ts +const { data: events } = await workos.events.listEvents({ + events: [ + 'feature_flags.flag_target.created', + 'feature_flags.flag_rule.updated', + ], +}); + +for (const event of events) { + if (event.event === 'feature_flags.flag_target.created') { + if ('ruleId' in event.data) { + console.log(event.data.ruleId, event.data.targetId); + } else { + console.log(event.data.valueType, event.data.value); + } + } +} +``` + +Flag events include `environmentId`, nullable `description`, and a nullable +`owner` with `email`, `firstName`, and `lastName`. Updated flag context includes +optional `previousAttributes.data`, and updated rule context includes the rule's +previous `value` there. Rule context can omit `actor` for system changes. Target +event data is a `FlagTarget` with `ruleId`, or a `LegacyFlagTarget` with `value` +and `valueType`. The `flag.*` events keep their existing data shape. + ## SDK Versioning For our SDKs WorkOS follows a Semantic Versioning ([SemVer](https://semver.org/)) process where all releases will have a version X.Y.Z (like 1.0.0) pattern wherein Z would be a bug fix (e.g., 1.0.1), Y would be a minor release (1.1.0) and X would be a major release (2.0.0). We permit any breaking changes to only be released in major versions and strongly recommend reading changelogs before making any major version upgrades. diff --git a/src/common/interfaces/event.interface.ts b/src/common/interfaces/event.interface.ts index 53ae0f37c..292f892b8 100644 --- a/src/common/interfaces/event.interface.ts +++ b/src/common/interfaces/event.interface.ts @@ -55,6 +55,10 @@ import { ApiKey, SerializedApiKey } from '../../api-keys/interfaces'; import { FeatureFlag, FeatureFlagResponse, + FlagRule, + FlagRuleResponse, + FlagTargetResource, + FlagTargetResourceResponse, } from '../../feature-flags/interfaces'; import { Group, GroupResponse } from '../../groups/interfaces'; import { ConnectedAccountState } from '../../pipes/interfaces'; @@ -718,6 +722,172 @@ export interface ApiKeyRevokedEventResponse extends EventResponseBase { data: SerializedApiKey; } +export type FeatureFlagEventData = Omit & { + environmentId: string; + description: string | null; + owner: { + email: string; + firstName: string | null; + lastName: string | null; + } | null; +}; + +export type FeatureFlagEventResponseData = Omit< + FeatureFlagResponse, + 'description' +> & { + environment_id: string; + description: string | null; + owner: { + email: string; + first_name: string | null; + last_name: string | null; + } | null; +}; + +export type FeatureFlagActor = { + id: string; + source: 'api' | 'dashboard' | 'admin_portal' | 'system'; + name: string | null; +}; + +export type FeatureFlagContext = { clientId: string; actor: FeatureFlagActor }; +export type FeatureFlagContextResponse = { + client_id: string; + actor: FeatureFlagActor; +}; +export type FeatureFlagRuleContext = { + clientId: string; + actor?: FeatureFlagActor; +}; +export type FeatureFlagRuleContextResponse = { + client_id: string; + actor?: FeatureFlagActor; +}; +export type FeatureFlagRuleUpdatedContext = FeatureFlagRuleContext & { + previousAttributes?: { data?: Partial> }; +}; +export type FeatureFlagRuleUpdatedContextResponse = + FeatureFlagRuleContextResponse & { + previous_attributes?: { data?: Partial> }; + }; +export type FeatureFlagUpdatedContext = FeatureFlagContext & { + previousAttributes?: { + data?: Partial< + Pick< + FeatureFlagEventData, + 'name' | 'description' | 'tags' | 'owner' | 'enabled' | 'defaultValue' + > + >; + }; +}; +export type FeatureFlagUpdatedContextResponse = FeatureFlagContextResponse & { + previous_attributes?: { + data?: Partial< + Pick< + FeatureFlagEventResponseData, + 'name' | 'description' | 'tags' | 'owner' | 'enabled' | 'default_value' + > + >; + }; +}; + +export interface FeatureFlagsFlagCreatedEvent extends EventBase { + event: 'feature_flags.flag.created'; + data: FeatureFlagEventData; + context: FeatureFlagContext; +} + +export interface FeatureFlagsFlagCreatedEventResponse extends EventResponseBase { + event: 'feature_flags.flag.created'; + data: FeatureFlagEventResponseData; + context: FeatureFlagContextResponse; +} + +export interface FeatureFlagsFlagUpdatedEvent extends EventBase { + event: 'feature_flags.flag.updated'; + data: FeatureFlagEventData; + context: FeatureFlagUpdatedContext; +} + +export interface FeatureFlagsFlagUpdatedEventResponse extends EventResponseBase { + event: 'feature_flags.flag.updated'; + data: FeatureFlagEventResponseData; + context: FeatureFlagUpdatedContextResponse; +} + +export interface FeatureFlagsFlagDeletedEvent extends EventBase { + event: 'feature_flags.flag.deleted'; + data: FeatureFlagEventData; + context: FeatureFlagContext; +} + +export interface FeatureFlagsFlagDeletedEventResponse extends EventResponseBase { + event: 'feature_flags.flag.deleted'; + data: FeatureFlagEventResponseData; + context: FeatureFlagContextResponse; +} + +export interface FeatureFlagsFlagRuleCreatedEvent extends EventBase { + event: 'feature_flags.flag_rule.created'; + data: FlagRule; + context: FeatureFlagRuleContext; +} + +export interface FeatureFlagsFlagRuleCreatedEventResponse extends EventResponseBase { + event: 'feature_flags.flag_rule.created'; + data: FlagRuleResponse; + context: FeatureFlagRuleContextResponse; +} + +export interface FeatureFlagsFlagRuleUpdatedEvent extends EventBase { + event: 'feature_flags.flag_rule.updated'; + data: FlagRule; + context: FeatureFlagRuleUpdatedContext; +} + +export interface FeatureFlagsFlagRuleUpdatedEventResponse extends EventResponseBase { + event: 'feature_flags.flag_rule.updated'; + data: FlagRuleResponse; + context: FeatureFlagRuleUpdatedContextResponse; +} + +export interface FeatureFlagsFlagRuleDeletedEvent extends EventBase { + event: 'feature_flags.flag_rule.deleted'; + data: FlagRule; + context: FeatureFlagRuleContext; +} + +export interface FeatureFlagsFlagRuleDeletedEventResponse extends EventResponseBase { + event: 'feature_flags.flag_rule.deleted'; + data: FlagRuleResponse; + context: FeatureFlagRuleContextResponse; +} + +export interface FeatureFlagsFlagTargetCreatedEvent extends EventBase { + event: 'feature_flags.flag_target.created'; + data: FlagTargetResource; + context: FeatureFlagContext; +} + +export interface FeatureFlagsFlagTargetCreatedEventResponse extends EventResponseBase { + event: 'feature_flags.flag_target.created'; + data: FlagTargetResourceResponse; + context: FeatureFlagContextResponse; +} + +export interface FeatureFlagsFlagTargetDeletedEvent extends EventBase { + event: 'feature_flags.flag_target.deleted'; + data: FlagTargetResource; + context: FeatureFlagContext; +} + +export interface FeatureFlagsFlagTargetDeletedEventResponse extends EventResponseBase { + event: 'feature_flags.flag_target.deleted'; + data: FlagTargetResourceResponse; + context: FeatureFlagContextResponse; +} + export interface FlagCreatedEvent extends EventBase { event: 'flag.created'; data: FeatureFlag; @@ -1079,6 +1249,14 @@ export type Event = | OrganizationDomainDeletedEvent | ApiKeyCreatedEvent | ApiKeyRevokedEvent + | FeatureFlagsFlagCreatedEvent + | FeatureFlagsFlagUpdatedEvent + | FeatureFlagsFlagDeletedEvent + | FeatureFlagsFlagRuleCreatedEvent + | FeatureFlagsFlagRuleUpdatedEvent + | FeatureFlagsFlagRuleDeletedEvent + | FeatureFlagsFlagTargetCreatedEvent + | FeatureFlagsFlagTargetDeletedEvent | FlagCreatedEvent | FlagUpdatedEvent | FlagDeletedEvent @@ -1165,6 +1343,14 @@ export type EventResponse = | OrganizationDomainDeletedEventResponse | ApiKeyCreatedEventResponse | ApiKeyRevokedEventResponse + | FeatureFlagsFlagCreatedEventResponse + | FeatureFlagsFlagUpdatedEventResponse + | FeatureFlagsFlagDeletedEventResponse + | FeatureFlagsFlagRuleCreatedEventResponse + | FeatureFlagsFlagRuleUpdatedEventResponse + | FeatureFlagsFlagRuleDeletedEventResponse + | FeatureFlagsFlagTargetCreatedEventResponse + | FeatureFlagsFlagTargetDeletedEventResponse | FlagCreatedEventResponse | FlagUpdatedEventResponse | FlagDeletedEventResponse diff --git a/src/common/serializers/event.serializer.ts b/src/common/serializers/event.serializer.ts index 7df974cc3..00991d31b 100644 --- a/src/common/serializers/event.serializer.ts +++ b/src/common/serializers/event.serializer.ts @@ -33,6 +33,16 @@ import { deserializeAuthenticationRadarRiskDetectedEvent } from '../../user-mana import { deserializeApiKey } from '../../api-keys/serializers/api-key.serializer'; import { deserializeOrganizationRoleEvent } from '../../authorization/serializers/organization-role.serializer'; import { deserializePermission } from '../../authorization/serializers/permission.serializer'; +import { + deserializeFlagRule, + deserializeFlagTargetResource, +} from '../../feature-flags/serializers'; +import { + deserializeFeatureFlagEventData, + deserializeFeatureFlagRuleContext, + deserializeFeatureFlagRuleUpdatedContext, + deserializeFeatureFlagUpdatedContext, +} from './feature-flag-event.serializer'; import { deserializeFeatureFlag } from '../../feature-flags/serializers/feature-flag.serializer'; import { deserializeGroup } from '../../groups/serializers'; import { @@ -272,6 +282,50 @@ export const deserializeEvent = (event: EventResponse): Event => { event: event.event, data: deserializeApiKey(event.data), }; + case 'feature_flags.flag.created': + case 'feature_flags.flag.deleted': + return { + ...eventBase, + event: event.event, + data: deserializeFeatureFlagEventData(event.data), + context: { + clientId: event.context.client_id, + actor: event.context.actor, + }, + }; + case 'feature_flags.flag.updated': + return { + ...eventBase, + event: event.event, + data: deserializeFeatureFlagEventData(event.data), + context: deserializeFeatureFlagUpdatedContext(event.context), + }; + case 'feature_flags.flag_rule.created': + case 'feature_flags.flag_rule.deleted': + return { + ...eventBase, + event: event.event, + data: deserializeFlagRule(event.data), + context: deserializeFeatureFlagRuleContext(event.context), + }; + case 'feature_flags.flag_rule.updated': + return { + ...eventBase, + event: event.event, + data: deserializeFlagRule(event.data), + context: deserializeFeatureFlagRuleUpdatedContext(event.context), + }; + case 'feature_flags.flag_target.created': + case 'feature_flags.flag_target.deleted': + return { + ...eventBase, + event: event.event, + data: deserializeFlagTargetResource(event.data), + context: { + clientId: event.context.client_id, + actor: event.context.actor, + }, + }; case 'flag.created': case 'flag.updated': case 'flag.deleted': diff --git a/src/common/serializers/feature-flag-event.serializer.ts b/src/common/serializers/feature-flag-event.serializer.ts new file mode 100644 index 000000000..778c17b6e --- /dev/null +++ b/src/common/serializers/feature-flag-event.serializer.ts @@ -0,0 +1,103 @@ +import { + FeatureFlagEventData, + FeatureFlagEventResponseData, + FeatureFlagRuleContext, + FeatureFlagRuleContextResponse, + FeatureFlagRuleUpdatedContext, + FeatureFlagRuleUpdatedContextResponse, + FeatureFlagUpdatedContext, + FeatureFlagUpdatedContextResponse, +} from '../interfaces'; + +const deserializeFeatureFlagOwner = ( + owner: FeatureFlagEventResponseData['owner'], +): FeatureFlagEventData['owner'] => + owner === null + ? null + : { + email: owner.email, + firstName: owner.first_name, + lastName: owner.last_name, + }; + +export const deserializeFeatureFlagEventData = ( + flag: FeatureFlagEventResponseData, +): FeatureFlagEventData => ({ + object: flag.object, + id: flag.id, + environmentId: flag.environment_id, + slug: flag.slug, + name: flag.name, + description: flag.description, + owner: deserializeFeatureFlagOwner(flag.owner), + tags: flag.tags, + enabled: flag.enabled, + defaultValue: flag.default_value, + createdAt: flag.created_at, + updatedAt: flag.updated_at, +}); + +export const deserializeFeatureFlagRuleContext = ( + context: FeatureFlagRuleContextResponse, +): FeatureFlagRuleContext => ({ + clientId: context.client_id, + ...(context.actor === undefined ? {} : { actor: context.actor }), +}); + +export const deserializeFeatureFlagRuleUpdatedContext = ( + context: FeatureFlagRuleUpdatedContextResponse, +): FeatureFlagRuleUpdatedContext => { + const previous = context.previous_attributes; + const data = previous?.data; + return { + ...deserializeFeatureFlagRuleContext(context), + ...(previous === undefined + ? {} + : { + previousAttributes: { + ...(data === undefined + ? {} + : { + data: data.value === undefined ? {} : { value: data.value }, + }), + }, + }), + }; +}; + +export const deserializeFeatureFlagUpdatedContext = ( + context: FeatureFlagUpdatedContextResponse, +): FeatureFlagUpdatedContext => { + const previous = context.previous_attributes; + const data = previous?.data; + return { + clientId: context.client_id, + actor: context.actor, + ...(previous === undefined + ? {} + : { + previousAttributes: { + ...(data === undefined + ? {} + : { + data: { + ...(data.name === undefined ? {} : { name: data.name }), + ...(data.description === undefined + ? {} + : { description: data.description }), + ...(data.tags === undefined ? {} : { tags: data.tags }), + ...(data.owner === undefined + ? {} + : { owner: deserializeFeatureFlagOwner(data.owner) }), + ...(data.enabled === undefined + ? {} + : { enabled: data.enabled }), + ...(data.default_value === undefined + ? {} + : { defaultValue: data.default_value }), + }, + }), + }, + }), + }; +}; diff --git a/src/events/feature-flag-events.spec.ts b/src/events/feature-flag-events.spec.ts new file mode 100644 index 000000000..54ca64e8a --- /dev/null +++ b/src/events/feature-flag-events.spec.ts @@ -0,0 +1,409 @@ +import crypto from 'crypto'; +import fetch from 'jest-fetch-mock'; +import { WorkOS } from '../workos'; +import { + EventResponse, + EventName, + FeatureFlagActor, + FeatureFlagEventResponseData, +} from '../common/interfaces'; +import { FlagRuleResponse } from '../feature-flags/interfaces'; +import { fetchOnce, fetchSearchParams } from '../common/utils/test-utils'; + +const actor: FeatureFlagActor = { + id: 'user_actor', + source: 'dashboard', + name: null, +}; +const timestamps = { + created_at: '2026-10-07T12:00:00.000Z', + updated_at: '2026-10-07T12:01:00.000Z', +}; +const flag: FeatureFlagEventResponseData = { + object: 'feature_flag', + id: 'flag_test', + environment_id: 'environment_test', + slug: 'new-checkout', + name: 'New checkout', + description: null, + owner: { email: 'owner@example.com', first_name: null, last_name: 'Owner' }, + tags: ['beta'], + enabled: true, + default_value: false, + ...timestamps, +}; +const expectedFlag = { + object: 'feature_flag', + id: 'flag_test', + environmentId: 'environment_test', + slug: 'new-checkout', + name: 'New checkout', + description: null, + owner: { email: 'owner@example.com', firstName: null, lastName: 'Owner' }, + tags: ['beta'], + enabled: true, + defaultValue: false, + createdAt: '2026-10-07T12:00:00.000Z', + updatedAt: '2026-10-07T12:01:00.000Z', +}; +const rule: FlagRuleResponse = { + object: 'flag_rule', + id: 'flag_rule_test', + flag_id: 'flag_test', + flag_slug: 'new-checkout', + environment_id: 'environment_test', + target_type: null, + position: 0, + value_type: 'boolean', + value: false, + ...timestamps, +}; +const expectedRule = { + object: 'flag_rule', + id: 'flag_rule_test', + flagId: 'flag_test', + flagSlug: 'new-checkout', + environmentId: 'environment_test', + targetType: null, + position: 0, + valueType: 'boolean', + value: false, + createdAt: '2026-10-07T12:00:00.000Z', + updatedAt: '2026-10-07T12:01:00.000Z', +}; +const target = { + object: 'flag_target', + id: 'flag_target_test', + flag_id: 'flag_test', + flag_slug: 'new-checkout', + environment_id: 'environment_test', + target_type: 'organization', + target_id: 'org_test', + ...timestamps, +}; +const expectedTarget = { + object: 'flag_target', + id: 'flag_target_test', + flagId: 'flag_test', + flagSlug: 'new-checkout', + environmentId: 'environment_test', + targetType: 'organization', + targetId: 'org_test', + createdAt: '2026-10-07T12:00:00.000Z', + updatedAt: '2026-10-07T12:01:00.000Z', +}; + +// API schema fixtures assert the public event boundary, including fields that +// differ from REST flags and the historical value-bearing target contract. +interface EventCase { + name: string; + response: EventResponse; + expected: object; +} +const eventBase = { id: 'event_test', created_at: timestamps.created_at }; +const responseContext = { client_id: 'client_test', actor }; +const expectedContext = { clientId: 'client_test', actor }; +const ruleEvents: Array< + | 'feature_flags.flag_rule.created' + | 'feature_flags.flag_rule.updated' + | 'feature_flags.flag_rule.deleted' +> = [ + 'feature_flags.flag_rule.created', + 'feature_flags.flag_rule.updated', + 'feature_flags.flag_rule.deleted', +]; +const targetEvents: Array< + 'feature_flags.flag_target.created' | 'feature_flags.flag_target.deleted' +> = ['feature_flags.flag_target.created', 'feature_flags.flag_target.deleted']; +const cases: EventCase[] = [ + { + name: 'feature_flags.flag.created', + response: { + ...eventBase, + event: 'feature_flags.flag.created', + data: flag, + context: responseContext, + }, + expected: { data: expectedFlag, context: expectedContext }, + }, + { + name: 'feature_flags.flag.deleted with no owner', + response: { + ...eventBase, + event: 'feature_flags.flag.deleted', + data: { ...flag, owner: null }, + context: responseContext, + }, + expected: { + data: { ...expectedFlag, owner: null }, + context: expectedContext, + }, + }, + { + name: 'feature_flags.flag.updated with previous attributes', + response: { + ...eventBase, + event: 'feature_flags.flag.updated', + data: flag, + context: { + ...responseContext, + previous_attributes: { + data: { + name: 'Old checkout', + description: null, + tags: [], + enabled: false, + default_value: false, + }, + }, + }, + }, + expected: { + data: expectedFlag, + context: { + ...expectedContext, + previousAttributes: { + data: { + name: 'Old checkout', + description: null, + tags: [], + enabled: false, + defaultValue: false, + }, + }, + }, + }, + }, + { + name: 'feature_flags.flag.updated with a previous owner', + response: { + ...eventBase, + event: 'feature_flags.flag.updated', + data: flag, + context: { + ...responseContext, + previous_attributes: { + data: { + owner: { + email: 'previous@example.com', + first_name: null, + last_name: 'Owner', + }, + }, + }, + }, + }, + expected: { + data: expectedFlag, + context: { + ...expectedContext, + previousAttributes: { + data: { + owner: { + email: 'previous@example.com', + firstName: null, + lastName: 'Owner', + }, + }, + }, + }, + }, + }, + { + name: 'feature_flags.flag.updated with no previous owner', + response: { + ...eventBase, + event: 'feature_flags.flag.updated', + data: flag, + context: { + ...responseContext, + previous_attributes: { data: { owner: null } }, + }, + }, + expected: { + data: expectedFlag, + context: { + ...expectedContext, + previousAttributes: { data: { owner: null } }, + }, + }, + }, + ...ruleEvents.map((event): EventCase => { + const withActor = event !== 'feature_flags.flag_rule.created'; + return { + name: event, + response: { + ...eventBase, + event, + data: rule, + context: { client_id: 'client_test', ...(withActor ? { actor } : {}) }, + }, + expected: { + data: expectedRule, + context: { clientId: 'client_test', ...(withActor ? { actor } : {}) }, + }, + }; + }), + ...targetEvents.flatMap((event): EventCase[] => [ + { + name: `${event} membership`, + response: { + ...eventBase, + event, + data: { ...target, object: 'flag_target', rule_id: 'flag_rule_test' }, + context: responseContext, + }, + expected: { + data: { ...expectedTarget, ruleId: 'flag_rule_test' }, + context: expectedContext, + }, + }, + { + name: `${event} legacy`, + response: { + ...eventBase, + event, + data: { + ...target, + object: 'flag_target', + value_type: 'boolean', + value: false, + }, + context: responseContext, + }, + expected: { + data: { ...expectedTarget, valueType: 'boolean', value: false }, + context: expectedContext, + }, + }, + ]), + ...[undefined, {}, { data: {} }].map((previous): EventCase => ({ + name: `feature_flags.flag.updated sparse previous ${JSON.stringify(previous)}`, + response: { + ...eventBase, + event: 'feature_flags.flag.updated', + data: flag, + context: { + ...responseContext, + ...(previous === undefined ? {} : { previous_attributes: previous }), + }, + }, + expected: { + data: expectedFlag, + context: { + ...expectedContext, + ...(previous === undefined ? {} : { previousAttributes: previous }), + }, + }, + })), + ...[{ data: { value: true } }, {}, { data: {} }].map( + (previous): EventCase => ({ + name: `feature_flags.flag_rule.updated previous ${JSON.stringify(previous)}`, + response: { + ...eventBase, + event: 'feature_flags.flag_rule.updated', + data: rule, + context: { ...responseContext, previous_attributes: previous }, + }, + expected: { + data: expectedRule, + context: { ...expectedContext, previousAttributes: previous }, + }, + }), + ), +]; + +const workos = new WorkOS('sk_test_feature_flag_events'); +const eventNames: EventName[] = cases.map(({ response }) => response.event); + +describe('Namespaced feature flag events', () => { + beforeEach(() => fetch.resetMocks()); + + it.each(cases)( + 'listEvents deserializes $name', + async ({ response, expected }) => { + fetchOnce({ + object: 'list', + data: [response], + list_metadata: { before: null, after: null }, + }); + const result = await workos.events.listEvents({ events: eventNames }); + expect(fetchSearchParams().events).toContain(response.event); + expect(result.data).toEqual([ + { + id: 'event_test', + event: response.event, + createdAt: timestamps.created_at, + ...expected, + }, + ]); + }, + ); + + it.each(cases)( + 'constructEvent verifies and deserializes $name', + async ({ response, expected }) => { + const payload = JSON.stringify(response); + const timestamp = Date.now(); + const secret = 'test_webhook_secret'; + const signature = crypto + .createHmac('sha256', secret) + .update(`${timestamp}.${payload}`) + .digest('hex'); + const event = await workos.webhooks.constructEvent({ + payload, + sigHeader: `t=${timestamp}, v1=${signature}`, + secret, + }); + expect(event).toEqual({ + id: 'event_test', + event: response.event, + createdAt: timestamps.created_at, + ...expected, + }); + }, + ); + + const historicalNames: Array< + 'flag.created' | 'flag.updated' | 'flag.deleted' | 'flag.rule_updated' + > = ['flag.created', 'flag.updated', 'flag.deleted', 'flag.rule_updated']; + it.each(historicalNames)( + 'preserves historical %s data and context', + async (name) => { + const context = { + client_id: 'client_test', + actor, + access_type: 'some', + configured_targets: { organizations: [], users: [] }, + }; + fetchOnce({ + object: 'list', + data: [ + { + id: 'event_test', + event: name, + data: { ...flag, description: 'Historical flag' }, + created_at: timestamps.created_at, + context, + }, + ], + list_metadata: { before: null, after: null }, + }); + const result = await workos.events.listEvents({ events: [name] }); + const { + environmentId: _environmentId, + owner: _owner, + ...historicalFlag + } = expectedFlag; + expect(result.data).toEqual([ + { + id: 'event_test', + event: name, + createdAt: timestamps.created_at, + data: { ...historicalFlag, description: 'Historical flag' }, + context, + }, + ]); + }, + ); +});