diff --git a/src/user-management/fixtures/list-waitlist-entries.json b/src/user-management/fixtures/list-waitlist-entries.json new file mode 100644 index 000000000..71d9cc4ec --- /dev/null +++ b/src/user-management/fixtures/list-waitlist-entries.json @@ -0,0 +1,22 @@ +{ + "object": "list", + "data": [ + { + "object": "waitlist_entry", + "id": "wl_user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "approved_at": null, + "additional_fields": { + "company": "Example Corp" + }, + "waitlist_id": "waitlist_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/src/user-management/fixtures/list-waitlists.json b/src/user-management/fixtures/list-waitlists.json new file mode 100644 index 000000000..2d7dca411 --- /dev/null +++ b/src/user-management/fixtures/list-waitlists.json @@ -0,0 +1,15 @@ +{ + "object": "list", + "data": [ + { + "object": "waitlist", + "id": "waitlist_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/src/user-management/fixtures/waitlist-entry.json b/src/user-management/fixtures/waitlist-entry.json new file mode 100644 index 000000000..2935e79dd --- /dev/null +++ b/src/user-management/fixtures/waitlist-entry.json @@ -0,0 +1,13 @@ +{ + "object": "waitlist_entry", + "id": "wl_user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "approved_at": null, + "additional_fields": { + "company": "Example Corp" + }, + "waitlist_id": "waitlist_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/src/user-management/fixtures/waitlist.json b/src/user-management/fixtures/waitlist.json new file mode 100644 index 000000000..1eb2c771e --- /dev/null +++ b/src/user-management/fixtures/waitlist.json @@ -0,0 +1,6 @@ +{ + "object": "waitlist", + "id": "waitlist_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/src/user-management/interfaces/create-waitlist-entry-options.interface.ts b/src/user-management/interfaces/create-waitlist-entry-options.interface.ts new file mode 100644 index 000000000..5025462c9 --- /dev/null +++ b/src/user-management/interfaces/create-waitlist-entry-options.interface.ts @@ -0,0 +1,14 @@ +export interface CreateWaitlistEntryOptions { + /** The email address of the user joining the waitlist. */ + email: string; + /** Additional key/value pairs collected with the waitlist entry. Supports up to 50 string pairs, with keys up to 40 characters and values up to 600 characters. */ + additionalFields?: Record; + /** Whether to send the waitlist confirmation email to the user. Defaults to `false`. No email is sent when the waitlist confirmation email is disabled in the environment, even if `sendConfirmationEmail` is `true`. */ + sendConfirmationEmail?: boolean; +} + +export interface SerializedCreateWaitlistEntryOptions { + email: string; + additional_fields?: Record; + send_confirmation_email?: boolean; +} diff --git a/src/user-management/interfaces/index.ts b/src/user-management/interfaces/index.ts index 5bfa63473..387975a6e 100644 --- a/src/user-management/interfaces/index.ts +++ b/src/user-management/interfaces/index.ts @@ -21,6 +21,7 @@ export * from './create-organization-membership-options.interface'; export * from './create-password-reset-options.interface'; export * from './create-user-api-key-options.interface'; export * from './create-user-options.interface'; +export * from './create-waitlist-entry-options.interface'; export * from './email-verification.interface'; export * from './enroll-auth-factor.interface'; export * from './identity.interface'; @@ -34,6 +35,7 @@ export * from './list-sessions-options.interface'; export * from './list-user-feature-flags-options.interface'; export * from './list-user-api-keys-options.interface'; export * from './list-users-options.interface'; +export * from './list-waitlist-entries-options.interface'; export * from './locale.interface'; export * from './logout-url-options.interface'; export * from './magic-auth.interface'; @@ -55,3 +57,5 @@ export * from './user.interface'; export * from './user-api-key.interface'; export * from './user-api-key-with-value.interface'; export * from './verify-email-options.interface'; +export * from './waitlist.interface'; +export * from './waitlist-entry.interface'; diff --git a/src/user-management/interfaces/list-waitlist-entries-options.interface.ts b/src/user-management/interfaces/list-waitlist-entries-options.interface.ts new file mode 100644 index 000000000..ab801ac14 --- /dev/null +++ b/src/user-management/interfaces/list-waitlist-entries-options.interface.ts @@ -0,0 +1,14 @@ +import { PaginationOptions } from '../../common/interfaces'; +import { WaitlistEntryState } from './waitlist-entry.interface'; + +export interface ListWaitlistEntriesOptions extends PaginationOptions { + /** Filter entries by state. */ + state?: WaitlistEntryState; + /** Filter entries by email address. */ + email?: string; +} + +export interface SerializedListWaitlistEntriesOptions extends PaginationOptions { + state?: WaitlistEntryState; + email?: string; +} diff --git a/src/user-management/interfaces/waitlist-entry.interface.ts b/src/user-management/interfaces/waitlist-entry.interface.ts new file mode 100644 index 000000000..da5e8cc5a --- /dev/null +++ b/src/user-management/interfaces/waitlist-entry.interface.ts @@ -0,0 +1,34 @@ +export type WaitlistEntryState = 'pending' | 'approved' | 'denied'; + +export interface WaitlistEntry { + /** Distinguishes the Waitlist Entry object. */ + object: 'waitlist_entry'; + /** The unique ID of the waitlist entry. */ + id: string; + /** The email address of the user on the waitlist. */ + email: string; + /** The state of the waitlist entry. */ + state: WaitlistEntryState; + /** The timestamp when the entry was approved, or null if not yet approved. */ + approvedAt: string | null; + /** Additional fields submitted when the user joined the waitlist. Values are user-provided — treat them as untrusted input when rendering or exporting. */ + additionalFields?: Record; + /** The unique ID of the waitlist the entry belongs to. */ + waitlistId: string | null; + /** An ISO 8601 timestamp. */ + createdAt: string; + /** An ISO 8601 timestamp. */ + updatedAt: string; +} + +export interface WaitlistEntryResponse { + object: 'waitlist_entry'; + id: string; + email: string; + state: WaitlistEntryState; + approved_at: string | null; + additional_fields?: Record; + waitlist_id?: string | null; + created_at: string; + updated_at: string; +} diff --git a/src/user-management/interfaces/waitlist.interface.ts b/src/user-management/interfaces/waitlist.interface.ts new file mode 100644 index 000000000..6c3e03368 --- /dev/null +++ b/src/user-management/interfaces/waitlist.interface.ts @@ -0,0 +1,17 @@ +export interface Waitlist { + /** Distinguishes the Waitlist object. */ + object: 'waitlist'; + /** The unique ID of the Waitlist. */ + id: string; + /** An ISO 8601 timestamp. */ + createdAt: string; + /** An ISO 8601 timestamp. */ + updatedAt: string; +} + +export interface WaitlistResponse { + object: 'waitlist'; + id: string; + created_at: string; + updated_at: string; +} diff --git a/src/user-management/serializers/create-waitlist-entry-options.serializer.ts b/src/user-management/serializers/create-waitlist-entry-options.serializer.ts new file mode 100644 index 000000000..c68899c64 --- /dev/null +++ b/src/user-management/serializers/create-waitlist-entry-options.serializer.ts @@ -0,0 +1,12 @@ +import { + CreateWaitlistEntryOptions, + SerializedCreateWaitlistEntryOptions, +} from '../interfaces/create-waitlist-entry-options.interface'; + +export const serializeCreateWaitlistEntryOptions = ( + options: CreateWaitlistEntryOptions, +): SerializedCreateWaitlistEntryOptions => ({ + email: options.email, + additional_fields: options.additionalFields, + send_confirmation_email: options.sendConfirmationEmail, +}); diff --git a/src/user-management/serializers/index.ts b/src/user-management/serializers/index.ts index 102e1b025..c59788ccf 100644 --- a/src/user-management/serializers/index.ts +++ b/src/user-management/serializers/index.ts @@ -29,3 +29,5 @@ export * from './update-user-password-options.serializer'; export * from './user.serializer'; export * from './user-api-key.serializer'; export * from './user-api-key-with-value.serializer'; +export * from './waitlist.serializer'; +export * from './waitlist-entry.serializer'; diff --git a/src/user-management/serializers/list-waitlist-entries-options.serializer.ts b/src/user-management/serializers/list-waitlist-entries-options.serializer.ts new file mode 100644 index 000000000..69ebd8ca8 --- /dev/null +++ b/src/user-management/serializers/list-waitlist-entries-options.serializer.ts @@ -0,0 +1,15 @@ +import { + ListWaitlistEntriesOptions, + SerializedListWaitlistEntriesOptions, +} from '../interfaces/list-waitlist-entries-options.interface'; + +export const serializeListWaitlistEntriesOptions = ( + options: ListWaitlistEntriesOptions, +): SerializedListWaitlistEntriesOptions => ({ + state: options.state, + email: options.email, + limit: options.limit, + before: options.before, + after: options.after, + order: options.order, +}); diff --git a/src/user-management/serializers/waitlist-entry.serializer.ts b/src/user-management/serializers/waitlist-entry.serializer.ts new file mode 100644 index 000000000..b4f21af1f --- /dev/null +++ b/src/user-management/serializers/waitlist-entry.serializer.ts @@ -0,0 +1,18 @@ +import { + WaitlistEntry, + WaitlistEntryResponse, +} from '../interfaces/waitlist-entry.interface'; + +export const deserializeWaitlistEntry = ( + waitlistEntry: WaitlistEntryResponse, +): WaitlistEntry => ({ + object: waitlistEntry.object, + id: waitlistEntry.id, + email: waitlistEntry.email, + state: waitlistEntry.state, + approvedAt: waitlistEntry.approved_at, + additionalFields: waitlistEntry.additional_fields, + waitlistId: waitlistEntry.waitlist_id ?? null, + createdAt: waitlistEntry.created_at, + updatedAt: waitlistEntry.updated_at, +}); diff --git a/src/user-management/serializers/waitlist.serializer.ts b/src/user-management/serializers/waitlist.serializer.ts new file mode 100644 index 000000000..db5f244d5 --- /dev/null +++ b/src/user-management/serializers/waitlist.serializer.ts @@ -0,0 +1,8 @@ +import { Waitlist, WaitlistResponse } from '../interfaces/waitlist.interface'; + +export const deserializeWaitlist = (waitlist: WaitlistResponse): Waitlist => ({ + object: waitlist.object, + id: waitlist.id, + createdAt: waitlist.created_at, + updatedAt: waitlist.updated_at, +}); diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index e71cf89bb..726954064 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -22,6 +22,10 @@ import passwordResetFixture from './fixtures/password_reset.json'; import userFixture from './fixtures/user.json'; import createUserApiKeyFixture from './fixtures/create-user-api-key.json'; import identityFixture from './fixtures/identity.json'; +import waitlistFixture from './fixtures/waitlist.json'; +import listWaitlistsFixture from './fixtures/list-waitlists.json'; +import waitlistEntryFixture from './fixtures/waitlist-entry.json'; +import listWaitlistEntriesFixture from './fixtures/list-waitlist-entries.json'; import * as jose from 'jose'; import { sealData } from '../common/crypto/seal'; @@ -36,6 +40,8 @@ const organizationMembershipId = 'om_01H5JQDV7R7ATEYZDEG0W5PRYS'; const emailVerificationId = 'email_verification_01H5JQDV7R7ATEYZDEG0W5PRYS'; const invitationId = 'invitation_01H5JQDV7R7ATEYZDEG0W5PRYS'; const invitationToken = 'Z1uX3RbwcIl5fIGJJJCXXisdI'; +const waitlistId = 'waitlist_01E4ZCR3C56J083X43JQXF3JK5'; +const waitlistEntryId = 'wl_user_01E4ZCR3C56J083X43JQXF3JK5'; const magicAuthId = 'magic_auth_01H5JQDV7R7ATEYZDEG0W5PRYS'; const passwordResetId = 'password_reset_01H5JQDV7R7ATEYZDEG0W5PRYS'; @@ -2788,6 +2794,213 @@ describe('UserManagement', () => { }); }); + describe('listWaitlists', () => { + it('lists waitlists', async () => { + fetchOnce(listWaitlistsFixture); + const waitlists = await workos.userManagement.listWaitlists(); + + expect(fetchURL()).toContain('/user_management/waitlists'); + expect(waitlists).toMatchObject({ + object: 'list', + data: [ + { + object: 'waitlist', + id: waitlistId, + }, + ], + listMetadata: { + before: null, + after: null, + }, + }); + }); + }); + + describe('getWaitlist', () => { + it('sends a Get Waitlist request', async () => { + fetchOnce(waitlistFixture); + const waitlist = await workos.userManagement.getWaitlist(waitlistId); + + expect(fetchURL()).toContain(`/user_management/waitlists/${waitlistId}`); + expect(waitlist).toMatchObject({ + object: 'waitlist', + id: waitlistId, + }); + }); + + it('accepts the default waitlist alias', async () => { + fetchOnce(waitlistFixture); + await workos.userManagement.getWaitlist('default'); + + expect(fetchURL()).toContain('/user_management/waitlists/default'); + }); + }); + + describe('listWaitlistEntries', () => { + it('lists waitlist entries', async () => { + fetchOnce(listWaitlistEntriesFixture); + const waitlistEntries = + await workos.userManagement.listWaitlistEntries(waitlistId); + + expect(fetchURL()).toContain( + `/user_management/waitlists/${waitlistId}/entries`, + ); + expect(waitlistEntries).toMatchObject({ + object: 'list', + data: [ + { + object: 'waitlist_entry', + id: waitlistEntryId, + email: 'marcelina.davis@example.com', + state: 'pending', + approvedAt: null, + additionalFields: { + company: 'Example Corp', + }, + waitlistId, + }, + ], + listMetadata: { + before: null, + after: null, + }, + }); + }); + + it('sends the correct params when filtering', async () => { + fetchOnce(listWaitlistEntriesFixture); + await workos.userManagement.listWaitlistEntries(waitlistId, { + state: 'pending', + email: 'marcelina.davis@example.com', + limit: 10, + }); + + expect(fetchSearchParams()).toEqual({ + state: 'pending', + email: 'marcelina.davis@example.com', + limit: '10', + order: 'desc', + }); + }); + }); + + describe('createWaitlistEntry', () => { + it('sends a Create Waitlist Entry request', async () => { + fetchOnce(waitlistEntryFixture, { status: 201 }); + const waitlistEntry = await workos.userManagement.createWaitlistEntry( + waitlistId, + { + email: 'marcelina.davis@example.com', + }, + ); + + expect(fetchURL()).toContain( + `/user_management/waitlists/${waitlistId}/entries`, + ); + expect(fetchBody()).toEqual({ + email: 'marcelina.davis@example.com', + }); + expect(waitlistEntry).toMatchObject({ + object: 'waitlist_entry', + id: waitlistEntryId, + email: 'marcelina.davis@example.com', + }); + }); + + it('sends the correct params when provided', async () => { + fetchOnce(waitlistEntryFixture, { status: 201 }); + await workos.userManagement.createWaitlistEntry('default', { + email: 'marcelina.davis@example.com', + additionalFields: { + company: 'Example Corp', + }, + sendConfirmationEmail: true, + }); + + expect(fetchURL()).toContain( + '/user_management/waitlists/default/entries', + ); + expect(fetchBody()).toEqual({ + email: 'marcelina.davis@example.com', + additional_fields: { + company: 'Example Corp', + }, + send_confirmation_email: true, + }); + }); + + it('throws when the email belongs to an existing user', async () => { + fetchOnce( + { + message: 'An account with that email already exists.', + code: 'user_already_exists', + }, + { status: 422 }, + ); + + await expect( + workos.userManagement.createWaitlistEntry(waitlistId, { + email: 'marcelina.davis@example.com', + }), + ).rejects.toThrow(); + }); + }); + + describe('approveWaitlistEntry', () => { + it('sends an Approve Waitlist Entry request', async () => { + fetchOnce({ + ...waitlistEntryFixture, + state: 'approved', + approved_at: '2026-01-16T12:00:00.000Z', + }); + const waitlistEntry = + await workos.userManagement.approveWaitlistEntry(waitlistEntryId); + + expect(fetchURL()).toContain( + `/user_management/waitlist_entries/${waitlistEntryId}/approve`, + ); + expect(waitlistEntry).toMatchObject({ + object: 'waitlist_entry', + id: waitlistEntryId, + state: 'approved', + approvedAt: '2026-01-16T12:00:00.000Z', + }); + }); + }); + + describe('denyWaitlistEntry', () => { + it('sends a Deny Waitlist Entry request', async () => { + fetchOnce({ + ...waitlistEntryFixture, + state: 'denied', + }); + const waitlistEntry = + await workos.userManagement.denyWaitlistEntry(waitlistEntryId); + + expect(fetchURL()).toContain( + `/user_management/waitlist_entries/${waitlistEntryId}/deny`, + ); + expect(waitlistEntry).toMatchObject({ + object: 'waitlist_entry', + id: waitlistEntryId, + state: 'denied', + }); + }); + }); + + describe('deleteWaitlistEntry', () => { + it('sends a Delete Waitlist Entry request', async () => { + fetchOnce({}, { status: 204 }); + const response = + await workos.userManagement.deleteWaitlistEntry(waitlistEntryId); + + expect(fetchURL()).toContain( + `/user_management/waitlist_entries/${waitlistEntryId}`, + ); + expect(response).toBeUndefined(); + }); + }); + describe('revokeSession', () => { it('sends a Revoke Session request', async () => { const sessionId = 'session_12345'; diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index fd02fe8ea..6bd954b4c 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -1,4 +1,6 @@ import { sealData, unsealData } from '../common/crypto/seal'; +import { List, ListResponse } from '../common/interfaces'; +import { deserializeList } from '../common/serializers'; import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; import { AutoPaginatable } from '../common/utils/pagination'; import { getEnv } from '../common/utils/env'; @@ -60,6 +62,14 @@ import { VerifyEmailOptions, SerializedUserApiKey, SerializedUserApiKeyWithValue, + CreateWaitlistEntryOptions, + ListWaitlistEntriesOptions, + SerializedCreateWaitlistEntryOptions, + SerializedListWaitlistEntriesOptions, + Waitlist, + WaitlistEntry, + WaitlistEntryResponse, + WaitlistResponse, } from './interfaces'; import { AuthenticateWithEmailVerificationOptions, @@ -176,6 +186,10 @@ import { serializeResendInvitationOptions } from './serializers/resend-invitatio import { deserializeOrganizationMembership } from './serializers/organization-membership.serializer'; import { serializeSendInvitationOptions } from './serializers/send-invitation-options.serializer'; import { serializeUpdateOrganizationMembershipOptions } from './serializers/update-organization-membership-options.serializer'; +import { deserializeWaitlist } from './serializers/waitlist.serializer'; +import { deserializeWaitlistEntry } from './serializers/waitlist-entry.serializer'; +import { serializeCreateWaitlistEntryOptions } from './serializers/create-waitlist-entry-options.serializer'; +import { serializeListWaitlistEntriesOptions } from './serializers/list-waitlist-entries-options.serializer'; import { CookieSession } from './session'; import { getJose } from '../utils/jose'; import { Group, GroupResponse } from '../groups/interfaces'; @@ -1471,6 +1485,151 @@ export class UserManagement { return deserializeInvitation(data); } + /** + * List waitlists + * + * Get a list of the waitlists in the environment. All waitlists are + * returned in a single response — this endpoint is not paginated. + * @returns {Promise>} + */ + async listWaitlists(): Promise> { + const { data } = await this.workos.get>( + '/user_management/waitlists', + ); + + return deserializeList(data, deserializeWaitlist); + } + + /** + * Get a waitlist + * + * Get the details of an existing waitlist. The literal id `default` + * is accepted and resolves to the environment's default waitlist. + * @returns {Promise} + * @throws {NotFoundException} 404 + */ + async getWaitlist(waitlistId: string): Promise { + const { data } = await this.workos.get( + `/user_management/waitlists/${waitlistId}`, + ); + + return deserializeWaitlist(data); + } + + /** + * List waitlist entries + * + * Get a list of the entries on a waitlist matching the criteria specified. + * The literal id `default` is accepted and resolves to the environment's + * default waitlist. + * @param waitlistId - The unique ID of the waitlist. + * @param options - Pagination and filter options. + * @returns {Promise>} + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async listWaitlistEntries( + waitlistId: string, + options?: ListWaitlistEntriesOptions, + ): Promise< + AutoPaginatable + > { + return new AutoPaginatable( + await fetchAndDeserialize( + this.workos, + `/user_management/waitlists/${waitlistId}/entries`, + deserializeWaitlistEntry, + options ? serializeListWaitlistEntriesOptions(options) : undefined, + ), + (params) => + fetchAndDeserialize( + this.workos, + `/user_management/waitlists/${waitlistId}/entries`, + deserializeWaitlistEntry, + params, + ), + options ? serializeListWaitlistEntriesOptions(options) : undefined, + ); + } + + /** + * Create a waitlist entry + * + * Add an email address to a waitlist. Adding an email address that is + * already on the waitlist returns the existing entry unchanged. The + * literal id `default` is accepted and resolves to the environment's + * default waitlist. + * @param waitlistId - The unique ID of the waitlist. + * @param payload - Object containing email and optional fields. + * @returns {Promise} + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async createWaitlistEntry( + waitlistId: string, + payload: CreateWaitlistEntryOptions, + ): Promise { + const { data } = await this.workos.post< + WaitlistEntryResponse, + SerializedCreateWaitlistEntryOptions + >( + `/user_management/waitlists/${waitlistId}/entries`, + serializeCreateWaitlistEntryOptions(payload), + ); + + return deserializeWaitlistEntry(data); + } + + /** + * Approve a waitlist entry + * + * Approve a waitlist entry, create an invitation for its email address, + * and send the invitation email. + * @returns {Promise} + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async approveWaitlistEntry(waitlistEntryId: string): Promise { + const { data } = await this.workos.post( + `/user_management/waitlist_entries/${waitlistEntryId}/approve`, + null, + ); + + return deserializeWaitlistEntry(data); + } + + /** + * Deny a waitlist entry + * + * Deny a pending waitlist entry. + * @returns {Promise} + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async denyWaitlistEntry(waitlistEntryId: string): Promise { + const { data } = await this.workos.post( + `/user_management/waitlist_entries/${waitlistEntryId}/deny`, + null, + ); + + return deserializeWaitlistEntry(data); + } + + /** + * Delete a waitlist entry + * + * Remove the entry from the waitlist. Its email address can join again + * unless a user with that email now exists in the environment. Deleting + * the entry does not revoke an invitation created by approving it. + * @returns {Promise} + * @throws {NotFoundException} 404 + */ + async deleteWaitlistEntry(waitlistEntryId: string): Promise { + await this.workos.delete( + `/user_management/waitlist_entries/${waitlistEntryId}`, + ); + } + /** * Revoke Session *