diff --git a/src/organizations/fixtures/it-contact.json b/src/organizations/fixtures/it-contact.json new file mode 100644 index 000000000..f57597489 --- /dev/null +++ b/src/organizations/fixtures/it-contact.json @@ -0,0 +1,7 @@ +{ + "object": "it_contact", + "id": "it_contact_01HXYZ123456789ABCDEFGHIJ", + "email": "it-contact@example.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/src/organizations/fixtures/list-it-contacts.json b/src/organizations/fixtures/list-it-contacts.json new file mode 100644 index 000000000..ede8fe74c --- /dev/null +++ b/src/organizations/fixtures/list-it-contacts.json @@ -0,0 +1,16 @@ +{ + "object": "list", + "data": [ + { + "object": "it_contact", + "id": "it_contact_01HXYZ123456789ABCDEFGHIJ", + "email": "it-contact@example.com", + "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/organizations/interfaces/index.ts b/src/organizations/interfaces/index.ts index d44e49367..f18875e59 100644 --- a/src/organizations/interfaces/index.ts +++ b/src/organizations/interfaces/index.ts @@ -1,5 +1,7 @@ export * from './create-organization-options.interface'; export * from './domain-data.interface'; +export * from './it-contact-options.interface'; +export * from './it-contact.interface'; export * from './list-organization-feature-flags-options.interface'; export * from './list-organizations-options.interface'; export * from './organization.interface'; diff --git a/src/organizations/interfaces/it-contact-options.interface.ts b/src/organizations/interfaces/it-contact-options.interface.ts new file mode 100644 index 000000000..98fb9df2b --- /dev/null +++ b/src/organizations/interfaces/it-contact-options.interface.ts @@ -0,0 +1,53 @@ +export const ItContactIntent = { + SSO: 'sso', + DirectorySync: 'directory_sync', + LogStreams: 'log_streams', + DomainVerification: 'domain_verification', + BringYourOwnKey: 'bring_your_own_key', +} as const; + +export type ItContactIntent = + (typeof ItContactIntent)[keyof typeof ItContactIntent]; + +export interface ListItContactsOptions { + /** Unique identifier of the Organization. */ + organizationId: string; +} + +export interface CreateItContactOptions { + /** Unique identifier of the Organization. */ + organizationId: string; + /** The email address of the IT Contact. */ + email: string; +} + +export interface SerializedCreateItContactOptions { + email: string; +} + +export interface DeleteItContactOptions { + /** Unique identifier of the Organization. */ + organizationId: string; + /** Unique identifier of the IT Contact. */ + contactId: string; +} + +export interface InviteItContactOptions { + /** Unique identifier of the Organization. */ + organizationId: string; + /** Unique identifier of the IT Contact. */ + contactId: string; + /** The Admin Portal features that the IT Contact can configure. */ + intents: ItContactIntent[]; +} + +export interface SerializedInviteItContactOptions { + intents: ItContactIntent[]; +} + +export interface RevokeItContactOptions { + /** Unique identifier of the Organization. */ + organizationId: string; + /** Unique identifier of the IT Contact. */ + contactId: string; +} diff --git a/src/organizations/interfaces/it-contact.interface.ts b/src/organizations/interfaces/it-contact.interface.ts new file mode 100644 index 000000000..5b9e0f43a --- /dev/null +++ b/src/organizations/interfaces/it-contact.interface.ts @@ -0,0 +1,20 @@ +export interface ItContact { + /** Distinguishes the IT Contact object. */ + object: 'it_contact'; + /** Unique identifier of the IT Contact. */ + id: string; + /** The email address of the IT Contact. */ + email: string; + /** An ISO 8601 timestamp. */ + createdAt: string; + /** An ISO 8601 timestamp. */ + updatedAt: string; +} + +export interface ItContactResponse { + object: 'it_contact'; + id: string; + email: string; + created_at: string; + updated_at: string; +} diff --git a/src/organizations/organizations.spec.ts b/src/organizations/organizations.spec.ts index 1014611ae..cda48deba 100644 --- a/src/organizations/organizations.spec.ts +++ b/src/organizations/organizations.spec.ts @@ -5,17 +5,20 @@ import { fetchSearchParams, fetchHeaders, fetchBody, + fetchMethod, } from '../common/utils/test-utils'; import { WorkOS } from '../workos'; import clearStripeCustomerId from './fixtures/clear-stripe-customer-id.json'; import createOrganizationInvalid from './fixtures/create-organization-invalid.json'; import createOrganization from './fixtures/create-organization.json'; import getOrganization from './fixtures/get-organization.json'; +import itContact from './fixtures/it-contact.json'; +import listItContacts from './fixtures/list-it-contacts.json'; import listOrganizationsFixture from './fixtures/list-organizations.json'; import updateOrganization from './fixtures/update-organization.json'; import setStripeCustomerId from './fixtures/set-stripe-customer-id.json'; import setStripeCustomerIdDisabled from './fixtures/set-stripe-customer-id-disabled.json'; -import { DomainDataState } from './interfaces'; +import { DomainDataState, ItContactIntent } from './interfaces'; const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); @@ -363,4 +366,102 @@ describe('Organizations', () => { }); }); }); + + describe('IT Contacts', () => { + describe('listItContacts', () => { + it('returns the organization’s IT contacts', async () => { + fetchOnce(listItContacts); + + const { data, listMetadata } = + await workos.organizations.listItContacts({ + organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T', + }); + + expect(fetchMethod()).toBe('GET'); + expect(fetchURL()).toContain( + '/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts', + ); + + expect(data).toEqual([ + { + object: 'it_contact', + id: 'it_contact_01HXYZ123456789ABCDEFGHIJ', + email: 'it-contact@example.com', + createdAt: '2026-01-15T12:00:00.000Z', + updatedAt: '2026-01-15T12:00:00.000Z', + }, + ]); + expect(listMetadata).toEqual({ before: null, after: null }); + }); + }); + + describe('createItContact', () => { + it('creates an IT contact', async () => { + fetchOnce(itContact); + + const subject = await workos.organizations.createItContact({ + organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T', + email: 'it-contact@example.com', + }); + + expect(fetchMethod()).toBe('POST'); + expect(fetchURL()).toContain( + '/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts', + ); + expect(fetchBody()).toEqual({ email: 'it-contact@example.com' }); + + expect(subject.id).toBe('it_contact_01HXYZ123456789ABCDEFGHIJ'); + }); + }); + + describe('deleteItContact', () => { + it('deletes an IT contact', async () => { + fetchOnce({}, { status: 204 }); + + await workos.organizations.deleteItContact({ + organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T', + contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ', + }); + + expect(fetchMethod()).toBe('DELETE'); + expect(fetchURL()).toContain( + '/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ', + ); + }); + }); + + describe('inviteItContact', () => { + it('invites an IT contact', async () => { + fetchOnce({}, { status: 204 }); + + await workos.organizations.inviteItContact({ + organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T', + contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ', + intents: [ItContactIntent.SSO, ItContactIntent.DirectorySync], + }); + + expect(fetchMethod()).toBe('POST'); + expect(fetchURL()).toContain( + '/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ/invite', + ); + expect(fetchBody()).toEqual({ intents: ['sso', 'directory_sync'] }); + }); + }); + + describe('revokeItContact', () => { + it('revokes an IT contact’s invitation', async () => { + fetchOnce({}, { status: 204 }); + + await workos.organizations.revokeItContact({ + organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T', + contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ', + }); + + expect(fetchMethod()).toBe('POST'); + expect(fetchURL()).toContain( + '/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ/revoke', + ); + }); + }); + }); }); diff --git a/src/organizations/organizations.ts b/src/organizations/organizations.ts index 4f2053471..dc67c46e3 100644 --- a/src/organizations/organizations.ts +++ b/src/organizations/organizations.ts @@ -1,16 +1,27 @@ import { AutoPaginatable } from '../common/utils/pagination'; import { WorkOS } from '../workos'; +import { List, ListResponse } from '../common/interfaces'; import { + CreateItContactOptions, CreateOrganizationOptions, CreateOrganizationRequestOptions, + DeleteItContactOptions, + InviteItContactOptions, + ItContact, + ItContactResponse, + ListItContactsOptions, ListOrganizationsOptions, Organization, OrganizationResponse, + RevokeItContactOptions, UpdateOrganizationOptions, } from './interfaces'; import { + deserializeItContact, deserializeOrganization, + serializeCreateItContactOptions, serializeCreateOrganizationOptions, + serializeInviteItContactOptions, serializeUpdateOrganizationOptions, } from './serializers'; @@ -151,4 +162,113 @@ export class Organizations { return deserializeOrganization(data); } + + /** + * List IT Contacts + * + * Get the IT Contacts for an Organization. + * @param options - Object containing the Organization ID. + * @returns {Promise>} + * @throws {AuthorizationException} 403 + * @throws {NotFoundException} 404 + */ + async listItContacts( + options: ListItContactsOptions, + ): Promise> { + const { organizationId } = options; + + const { data } = await this.workos.get>( + `/organizations/${organizationId}/it_contacts`, + ); + + return { + object: data.object, + data: data.data.map(deserializeItContact), + listMetadata: { + before: data.list_metadata.before, + after: data.list_metadata.after, + }, + }; + } + + /** + * Create an IT Contact + * + * Add an IT Contact to an Organization. No Admin Portal invitation is sent, + * though the contact is notified if the Organization has a connection + * certificate nearing expiry. + * @param options - Object containing the Organization ID and the email address. + * @returns {Promise} + * @throws {AuthorizationException} 403 + * @throws {NotFoundException} 404 + * @throws {ConflictException} 409 + * @throws {UnprocessableEntityException} 422 + */ + async createItContact(options: CreateItContactOptions): Promise { + const { organizationId, ...payload } = options; + + const { data } = await this.workos.post( + `/organizations/${organizationId}/it_contacts`, + serializeCreateItContactOptions(payload), + ); + + return deserializeItContact(data); + } + + /** + * Delete an IT Contact + * + * Remove an IT Contact from an Organization and revoke the contact's active + * setup links. + * @param options - Object containing the Organization ID and the IT Contact ID. + * @returns {Promise} + * @throws {AuthorizationException} 403 + * @throws {NotFoundException} 404 + */ + async deleteItContact(options: DeleteItContactOptions): Promise { + const { organizationId, contactId } = options; + + await this.workos.delete( + `/organizations/${organizationId}/it_contacts/${contactId}`, + ); + } + + /** + * Invite an IT Contact + * + * Create an Admin Portal setup link and email it to the IT Contact. An + * Organization can have at most one active invitation. + * @param options - Object containing the Organization ID, the IT Contact ID and the intents. + * @returns {Promise} + * @throws {AuthorizationException} 403 + * @throws {NotFoundException} 404 + * @throws {ConflictException} 409 + * @throws {UnprocessableEntityException} 422 + */ + async inviteItContact(options: InviteItContactOptions): Promise { + const { organizationId, contactId, ...payload } = options; + + await this.workos.post( + `/organizations/${organizationId}/it_contacts/${contactId}/invite`, + serializeInviteItContactOptions(payload), + ); + } + + /** + * Revoke an IT Contact's invitation + * + * Revoke the Organization's active Admin Portal invitation. + * @param options - Object containing the Organization ID and the IT Contact ID. + * @returns {Promise} + * @throws {AuthorizationException} 403 + * @throws {NotFoundException} 404 + */ + async revokeItContact(options: RevokeItContactOptions): Promise { + const { organizationId, contactId } = options; + + await this.workos.post( + `/organizations/${organizationId}/it_contacts/${contactId}/revoke`, + {}, + ); + } } diff --git a/src/organizations/serializers/index.ts b/src/organizations/serializers/index.ts index c53e493c4..a7837f4c1 100644 --- a/src/organizations/serializers/index.ts +++ b/src/organizations/serializers/index.ts @@ -1,3 +1,5 @@ export * from './create-organization-options.serializer'; +export * from './it-contact-options.serializer'; +export * from './it-contact.serializer'; export * from './organization.serializer'; export * from './update-organization-options.serializer'; diff --git a/src/organizations/serializers/it-contact-options.serializer.ts b/src/organizations/serializers/it-contact-options.serializer.ts new file mode 100644 index 000000000..bf237c38a --- /dev/null +++ b/src/organizations/serializers/it-contact-options.serializer.ts @@ -0,0 +1,18 @@ +import { + CreateItContactOptions, + InviteItContactOptions, + SerializedCreateItContactOptions, + SerializedInviteItContactOptions, +} from '../interfaces'; + +export const serializeCreateItContactOptions = ( + options: Omit, +): SerializedCreateItContactOptions => ({ + email: options.email, +}); + +export const serializeInviteItContactOptions = ( + options: Omit, +): SerializedInviteItContactOptions => ({ + intents: options.intents, +}); diff --git a/src/organizations/serializers/it-contact.serializer.ts b/src/organizations/serializers/it-contact.serializer.ts new file mode 100644 index 000000000..b7f763b4e --- /dev/null +++ b/src/organizations/serializers/it-contact.serializer.ts @@ -0,0 +1,11 @@ +import { ItContact, ItContactResponse } from '../interfaces'; + +export const deserializeItContact = ( + itContact: ItContactResponse, +): ItContact => ({ + object: itContact.object, + id: itContact.id, + email: itContact.email, + createdAt: itContact.created_at, + updatedAt: itContact.updated_at, +});