diff --git a/.changeset/directory-group-webhook-events.md b/.changeset/directory-group-webhook-events.md new file mode 100644 index 00000000000..86655113c57 --- /dev/null +++ b/.changeset/directory-group-webhook-events.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add `DirectoryGroupWebhookEvent` and `DirectoryGroupMembershipWebhookEvent` to `WebhookEvent`, so `verifyWebhook()` narrows the `directoryGroup.created`, `directoryGroup.updated`, `directoryGroup.deleted`, `directoryGroupMembership.created` and `directoryGroupMembership.deleted` events that Directory Sync sends. Their payload types are exported as `DirectoryGroupWebhookEventJSON`, `DirectoryGroupMembershipWebhookEventJSON` and `DeletedDirectoryGroupMembershipWebhookEventJSON`. diff --git a/packages/backend/src/__tests__/directory-group-webhooks.test.ts b/packages/backend/src/__tests__/directory-group-webhooks.test.ts new file mode 100644 index 00000000000..33094df47c9 --- /dev/null +++ b/packages/backend/src/__tests__/directory-group-webhooks.test.ts @@ -0,0 +1,28 @@ +import { describe, expectTypeOf, test } from 'vitest'; + +import type { DirectoryGroupMembershipWebhookEvent, DirectoryGroupWebhookEvent, WebhookEventType } from '../index'; +import type { verifyWebhook } from '../webhooks'; + +type VerifiedEvent = Awaited>; + +describe('directory group webhooks', () => { + test('are part of the verified event union', () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf<'directoryGroup.deleted'>().toMatchTypeOf(); + expectTypeOf<'directoryGroupMembership.deleted'>().toMatchTypeOf(); + }); + + test('only the membership deletion carries a cause', () => { + type Deleted = Extract; + type Created = Extract; + expectTypeOf().toEqualTypeOf<'group_deleted' | 'member_removed'>(); + expectTypeOf().not.toHaveProperty('cause'); + expectTypeOf().toEqualTypeOf(); + }); + + test('preserve the common resource ID used by existing webhook handlers', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 90667d1acd2..16c5469743b 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -33,6 +33,8 @@ export const ObjectType = { BlocklistIdentifier: 'blocklist_identifier', Client: 'client', Cookies: 'cookies', + DirectoryGroup: 'directory_group', + DirectoryGroupMembership: 'directory_group_membership', Domain: 'domain', Email: 'email', EnterpriseAccount: 'enterprise_account', @@ -761,6 +763,52 @@ export interface UserDeletedJSON extends DeletedObjectJSON { external_id?: string; } +/** + * Directory group webhook event payload: a group as the identity provider reported it to a directory. + * Used for `directoryGroup.created`, `directoryGroup.updated` and `directoryGroup.deleted` webhook events. + */ +export interface DirectoryGroupWebhookEventJSON { + object: typeof ObjectType.DirectoryGroup; + /** + * The group ID, as assigned by the directory. + */ + id: string; + directory_id: string; + display_name: string; + updated_at: number; +} + +/** + * Directory group membership webhook event payload: one user's membership in a directory group. + * `user_id` is null until the member has been provisioned into Clerk. + * Used for `directoryGroupMembership.created` webhook events. + */ +export interface DirectoryGroupMembershipWebhookEventJSON { + object: typeof ObjectType.DirectoryGroupMembership; + /** + * Never set: the payload has no ID. Declared so `event.data.id` keeps the same type across every `WebhookEvent`. + */ + id?: never; + directory_id: string; + group: { + object: typeof ObjectType.DirectoryGroup; + id: string; + display_name: string; + }; + directory_user_id: string; + user_id: string | null; + updated_at: number; +} + +/** + * Directory group membership deletion webhook event payload that extends `DirectoryGroupMembershipWebhookEventJSON` + * with `cause`: `group_deleted` when the whole group is gone, `member_removed` when the identity provider took this + * user out of it. Used for `directoryGroupMembership.deleted` webhook events. + */ +export interface DeletedDirectoryGroupMembershipWebhookEventJSON extends DirectoryGroupMembershipWebhookEventJSON { + cause: 'group_deleted' | 'member_removed'; +} + export interface PaginatedResponseJSON { data: object[]; total_count?: number; diff --git a/packages/backend/src/api/resources/Webhooks.ts b/packages/backend/src/api/resources/Webhooks.ts index d62651bae7d..54fadd72453 100644 --- a/packages/backend/src/api/resources/Webhooks.ts +++ b/packages/backend/src/api/resources/Webhooks.ts @@ -2,7 +2,10 @@ import type { BillingPaymentAttemptWebhookEventJSON, BillingSubscriptionItemWebhookEventJSON, BillingSubscriptionWebhookEventJSON, + DeletedDirectoryGroupMembershipWebhookEventJSON, DeletedObjectJSON, + DirectoryGroupMembershipWebhookEventJSON, + DirectoryGroupWebhookEventJSON, EmailJSON, OrganizationDomainJSON, OrganizationInvitationAcceptedJSON, @@ -73,6 +76,15 @@ export type EmailDeliveryWebhookEvent = export type SMSWebhookEvent = Webhook<'sms.created', SMSMessageJSON>; +export type DirectoryGroupWebhookEvent = Webhook< + 'directoryGroup.created' | 'directoryGroup.updated' | 'directoryGroup.deleted', + DirectoryGroupWebhookEventJSON +>; + +export type DirectoryGroupMembershipWebhookEvent = + | Webhook<'directoryGroupMembership.created', DirectoryGroupMembershipWebhookEventJSON> + | Webhook<'directoryGroupMembership.deleted', DeletedDirectoryGroupMembershipWebhookEventJSON>; + export type SessionWebhookEvent = Webhook< 'session.created' | 'session.ended' | 'session.removed' | 'session.revoked', SessionWebhookEventJSON @@ -140,6 +152,8 @@ export type WebhookEvent = | EmailWebhookEvent | EmailDeliveryWebhookEvent | SMSWebhookEvent + | DirectoryGroupWebhookEvent + | DirectoryGroupMembershipWebhookEvent | OrganizationWebhookEvent | OrganizationDomainWebhookEvent | OrganizationMembershipWebhookEvent diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9c77851ce76..65b4c1127ff 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -103,6 +103,9 @@ export type { SMSMessageJSON, UserJSON, UserDeletedJSON, + DirectoryGroupWebhookEventJSON, + DirectoryGroupMembershipWebhookEventJSON, + DeletedDirectoryGroupMembershipWebhookEventJSON, VerificationJSON, WaitlistEntryJSON, Web3WalletJSON, @@ -184,6 +187,8 @@ export type { PermissionWebhookEvent, SessionWebhookEvent, SMSWebhookEvent, + DirectoryGroupWebhookEvent, + DirectoryGroupMembershipWebhookEvent, UserWebhookEvent, WaitlistEntryWebhookEvent, WebhookEvent,