diff --git a/.changeset/backend-delete-invitation.md b/.changeset/backend-delete-invitation.md new file mode 100644 index 00000000000..d2cf179c16b --- /dev/null +++ b/.changeset/backend-delete-invitation.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add `clerkClient.invitations.deleteInvitation(invitationId)`, which permanently deletes an instance invitation and the stored copies of its invitation email. Unlike `revokeInvitation`, this removes the invitation record itself, so it can be used to honor a data erasure request from someone who was invited but never signed up. diff --git a/packages/backend/src/api/__tests__/InvitationApi.test.ts b/packages/backend/src/api/__tests__/InvitationApi.test.ts new file mode 100644 index 00000000000..f91582082bd --- /dev/null +++ b/packages/backend/src/api/__tests__/InvitationApi.test.ts @@ -0,0 +1,35 @@ +import { http, HttpResponse } from 'msw'; +import { describe, expect, it } from 'vitest'; + +import { server, validateHeaders } from '../../mock-server'; +import { createBackendApiClient } from '../factory'; + +describe('InvitationAPI', () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + secretKey: 'deadbeef', + }); + + describe('deleteInvitation', () => { + const invitationId = 'inv_123'; + + it('deletes an invitation by ID', async () => { + server.use( + http.delete( + `https://api.clerk.test/v1/invitations/${invitationId}`, + validateHeaders(() => HttpResponse.json({ object: 'invitation', id: invitationId, deleted: true })), + ), + ); + + const response = await apiClient.invitations.deleteInvitation(invitationId); + + expect(response.object).toBe('invitation'); + expect(response.id).toBe(invitationId); + expect(response.deleted).toBe(true); + }); + + it('throws an error when the invitation ID is missing', async () => { + await expect(apiClient.invitations.deleteInvitation('')).rejects.toThrow('A valid resource ID is required.'); + }); + }); +}); diff --git a/packages/backend/src/api/endpoints/InvitationApi.ts b/packages/backend/src/api/endpoints/InvitationApi.ts index 45b4e2c9976..13ba0310a12 100644 --- a/packages/backend/src/api/endpoints/InvitationApi.ts +++ b/packages/backend/src/api/endpoints/InvitationApi.ts @@ -1,6 +1,7 @@ import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; +import type { DeletedObject } from '../resources/DeletedObject'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; import type { InvitationStatus } from '../resources/Enums'; import type { Invitation } from '../resources/Invitation'; @@ -132,4 +133,21 @@ export class InvitationAPI extends AbstractAPI { path: joinPaths(basePath, invitationId, 'revoke'), }); } + + /** + * Permanently deletes the given invitation and the copies of the invitation email Clerk stored for its recipient. + * + * Unlike revoking, deleting removes the invitation record itself, which helps honor a data erasure request from someone who was invited but never signed up. Other records that contain the same email address, such as users or organization invitations, are not affected. + * + * Invitations of any status can be deleted. + * @param invitationId - The ID of the invitation to delete. + * @returns The [`DeletedObject`](https://clerk.com/docs/reference/backend/types/deleted-object) object. + */ + public async deleteInvitation(invitationId: string): Promise { + this.requireId(invitationId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, invitationId), + }); + } }