Skip to content
Draft
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,46 @@ can move to using the stable version.
- [Admin Portal Guide](https://workos.com/docs/admin-portal/guide)
- [Magic Link Guide](https://workos.com/docs/magic-link/guide)
- [Domain Verification Guide](https://workos.com/docs/domain-verification/guide)

### 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`. Rule context can omit `actor` for system
changes. Target events preserve both legacy values and rule memberships, including
persisted events emitted before the membership API rollout. Historical `flag.*`
event data and context remain unchanged.

SDK parsing support does not enable event delivery. Coordinate availability with
the feature flag event launch. Webhook endpoint subscription enums must be
regenerated after these names are published in the API specification; rule update
webhook registration is also a launch prerequisite.
179 changes: 179 additions & 0 deletions src/common/interfaces/event.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -718,6 +722,165 @@ export interface ApiKeyRevokedEventResponse extends EventResponseBase {
data: SerializedApiKey;
}

export type FeatureFlagEventData = Omit<FeatureFlag, 'description'> & {
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 FeatureFlagUpdatedContext = FeatureFlagContext & {
previousAttributes?: {
data?: Partial<
Pick<
FeatureFlagEventData,
'name' | 'description' | 'tags' | 'enabled' | 'defaultValue'
>
>;
};
};
export type FeatureFlagUpdatedContextResponse = FeatureFlagContextResponse & {
previous_attributes?: {
data?: Partial<
Pick<
FeatureFlagEventResponseData,
'name' | 'description' | 'tags' | '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: FeatureFlagRuleContext;
}

export interface FeatureFlagsFlagRuleUpdatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_rule.updated';
data: FlagRuleResponse;
context: FeatureFlagRuleContextResponse;
}

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;
Expand Down Expand Up @@ -1079,6 +1242,14 @@ export type Event =
| OrganizationDomainDeletedEvent
| ApiKeyCreatedEvent
| ApiKeyRevokedEvent
| FeatureFlagsFlagCreatedEvent
| FeatureFlagsFlagUpdatedEvent
| FeatureFlagsFlagDeletedEvent
| FeatureFlagsFlagRuleCreatedEvent
| FeatureFlagsFlagRuleUpdatedEvent
| FeatureFlagsFlagRuleDeletedEvent
| FeatureFlagsFlagTargetCreatedEvent
| FeatureFlagsFlagTargetDeletedEvent
| FlagCreatedEvent
| FlagUpdatedEvent
| FlagDeletedEvent
Expand Down Expand Up @@ -1165,6 +1336,14 @@ export type EventResponse =
| OrganizationDomainDeletedEventResponse
| ApiKeyCreatedEventResponse
| ApiKeyRevokedEventResponse
| FeatureFlagsFlagCreatedEventResponse
| FeatureFlagsFlagUpdatedEventResponse
| FeatureFlagsFlagDeletedEventResponse
| FeatureFlagsFlagRuleCreatedEventResponse
| FeatureFlagsFlagRuleUpdatedEventResponse
| FeatureFlagsFlagRuleDeletedEventResponse
| FeatureFlagsFlagTargetCreatedEventResponse
| FeatureFlagsFlagTargetDeletedEventResponse
| FlagCreatedEventResponse
| FlagUpdatedEventResponse
| FlagDeletedEventResponse
Expand Down
47 changes: 47 additions & 0 deletions src/common/serializers/event.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ 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,
deserializeFeatureFlagUpdatedContext,
} from './feature-flag-event.serializer';
import { deserializeFeatureFlag } from '../../feature-flags/serializers/feature-flag.serializer';
import { deserializeGroup } from '../../groups/serializers';
import {
Expand Down Expand Up @@ -272,6 +281,44 @@ 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.updated':
case 'feature_flags.flag_rule.deleted':
return {
...eventBase,
event: event.event,
data: deserializeFlagRule(event.data),
context: deserializeFeatureFlagRuleContext(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':
Expand Down
73 changes: 73 additions & 0 deletions src/common/serializers/feature-flag-event.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
FeatureFlagEventData,
FeatureFlagEventResponseData,
FeatureFlagRuleContext,
FeatureFlagRuleContextResponse,
FeatureFlagUpdatedContext,
FeatureFlagUpdatedContextResponse,
} from '../interfaces';

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:
flag.owner === null
? null
: {
email: flag.owner.email,
firstName: flag.owner.first_name,
lastName: flag.owner.last_name,
},
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 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.enabled === undefined
? {}
: { enabled: data.enabled }),
...(data.default_value === undefined
? {}
: { defaultValue: data.default_value }),
},
}),
},
}),
};
};
Loading
Loading