From 5452ee48511e30a2dfa545254c3ca0388ddd068a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Thu, 22 Oct 2026 22:17:00 -0300 Subject: [PATCH] feat(backend): Add directory group webhook event types The directoryGroup.* and directoryGroupMembership.* events Directory Sync sends are now in the public webhook reference, but verifyWebhook() could not narrow them because WebhookEvent had no members for them. The membership payload declares an optional id it never sets, so event.data.id keeps its type across the union. --- .changeset/directory-group-webhook-events.md | 5 ++ .../directory-group-webhooks.test.ts | 28 +++++++++++ packages/backend/src/api/resources/JSON.ts | 48 +++++++++++++++++++ .../backend/src/api/resources/Webhooks.ts | 14 ++++++ packages/backend/src/index.ts | 5 ++ 5 files changed, 100 insertions(+) create mode 100644 .changeset/directory-group-webhook-events.md create mode 100644 packages/backend/src/__tests__/directory-group-webhooks.test.ts 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,