diff --git a/src/user-management/fixtures/create-user-api-key.json b/src/user-management/fixtures/create-user-api-key.json new file mode 100644 index 000000000..1fbb67ea4 --- /dev/null +++ b/src/user-management/fixtures/create-user-api-key.json @@ -0,0 +1,17 @@ +{ + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "user", + "id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "expires_at": "2030-01-01T00:00:00.000Z", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "value": "test_api_key_value" +} diff --git a/src/user-management/fixtures/list-user-api-keys.json b/src/user-management/fixtures/list-user-api-keys.json new file mode 100644 index 000000000..aa2fb2b07 --- /dev/null +++ b/src/user-management/fixtures/list-user-api-keys.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "user", + "id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "expires_at": null, + "permissions": ["posts:read", "posts:write"], + "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/interfaces/create-user-api-key-options.interface.ts b/src/user-management/interfaces/create-user-api-key-options.interface.ts new file mode 100644 index 000000000..0cb0be190 --- /dev/null +++ b/src/user-management/interfaces/create-user-api-key-options.interface.ts @@ -0,0 +1,21 @@ +import { PostOptions } from '../../common/interfaces'; + +export interface CreateUserApiKeyOptions { + name: string; + organizationId: string; + permissions?: string[]; + expiresAt?: Date; +} + +export interface SerializedCreateUserApiKeyOptions { + name: string; + organization_id: string; + permissions?: string[]; + expires_at?: string; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface CreateUserApiKeyRequestOptions extends Pick< + PostOptions, + 'idempotencyKey' +> {} diff --git a/src/user-management/interfaces/index.ts b/src/user-management/interfaces/index.ts index 41f6f47b5..5bfa63473 100644 --- a/src/user-management/interfaces/index.ts +++ b/src/user-management/interfaces/index.ts @@ -19,6 +19,7 @@ export * from './authorization-url-options.interface'; export * from './create-magic-auth-options.interface'; 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 './email-verification.interface'; export * from './enroll-auth-factor.interface'; @@ -31,6 +32,7 @@ export * from './list-invitations-options.interface'; export * from './list-organization-memberships-options.interface'; 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 './locale.interface'; export * from './logout-url-options.interface'; @@ -50,4 +52,6 @@ export * from './update-organization-membership-options.interface'; export * from './update-user-options.interface'; export * from './update-user-password-options.interface'; export * from './user.interface'; +export * from './user-api-key.interface'; +export * from './user-api-key-with-value.interface'; export * from './verify-email-options.interface'; diff --git a/src/user-management/interfaces/list-user-api-keys-options.interface.ts b/src/user-management/interfaces/list-user-api-keys-options.interface.ts new file mode 100644 index 000000000..5e031d1fc --- /dev/null +++ b/src/user-management/interfaces/list-user-api-keys-options.interface.ts @@ -0,0 +1,9 @@ +import { PaginationOptions } from '../../common/interfaces/pagination-options.interface'; + +export interface ListUserApiKeysOptions extends PaginationOptions { + organizationId?: string; +} + +export interface SerializedListUserApiKeysOptions extends PaginationOptions { + organization_id?: string; +} diff --git a/src/user-management/interfaces/user-api-key-with-value.interface.ts b/src/user-management/interfaces/user-api-key-with-value.interface.ts new file mode 100644 index 000000000..541850a26 --- /dev/null +++ b/src/user-management/interfaces/user-api-key-with-value.interface.ts @@ -0,0 +1,9 @@ +import { SerializedUserApiKey, UserApiKey } from './user-api-key.interface'; + +export interface UserApiKeyWithValue extends UserApiKey { + value: string; +} + +export interface SerializedUserApiKeyWithValue extends SerializedUserApiKey { + value: string; +} diff --git a/src/user-management/interfaces/user-api-key.interface.ts b/src/user-management/interfaces/user-api-key.interface.ts new file mode 100644 index 000000000..9beba2868 --- /dev/null +++ b/src/user-management/interfaces/user-api-key.interface.ts @@ -0,0 +1,33 @@ +export interface UserApiKey { + object: 'api_key'; + id: string; + owner: { + type: 'user'; + id: string; + organizationId: string; + }; + name: string; + obfuscatedValue: string; + lastUsedAt: string | null; + expiresAt: string | null; + permissions: string[]; + createdAt: string; + updatedAt: string; +} + +export interface SerializedUserApiKey { + object: 'api_key'; + id: string; + owner: { + type: 'user'; + id: string; + organization_id: string; + }; + name: string; + obfuscated_value: string; + last_used_at: string | null; + expires_at: string | null; + permissions: string[]; + created_at: string; + updated_at: string; +} diff --git a/src/user-management/serializers/create-user-api-key-options.serializer.ts b/src/user-management/serializers/create-user-api-key-options.serializer.ts new file mode 100644 index 000000000..08ccd9596 --- /dev/null +++ b/src/user-management/serializers/create-user-api-key-options.serializer.ts @@ -0,0 +1,15 @@ +import { + CreateUserApiKeyOptions, + SerializedCreateUserApiKeyOptions, +} from '../interfaces/create-user-api-key-options.interface'; + +export function serializeCreateUserApiKeyOptions( + options: CreateUserApiKeyOptions, +): SerializedCreateUserApiKeyOptions { + return { + name: options.name, + organization_id: options.organizationId, + permissions: options.permissions, + expires_at: options.expiresAt?.toISOString(), + }; +} diff --git a/src/user-management/serializers/index.ts b/src/user-management/serializers/index.ts index 4935f9932..102e1b025 100644 --- a/src/user-management/serializers/index.ts +++ b/src/user-management/serializers/index.ts @@ -12,10 +12,12 @@ export * from './authentication-factor.serializer'; export * from './authentication-response.serializer'; export * from './create-magic-auth-options.serializer'; export * from './create-password-reset-options.serializer'; +export * from './create-user-api-key-options.serializer'; export * from './email-verification.serializer'; export * from './enroll-auth-factor-options.serializer'; export * from './invitation.serializer'; export * from './list-sessions-options.serializer'; +export * from './list-user-api-keys-options.serializer'; export * from './magic-auth.serializer'; export * from './password-reset.serializer'; export * from './reset-password-options.serializer'; @@ -25,3 +27,5 @@ export * from './send-radar-sms-challenge-options.serializer'; export * from './update-user-options.serializer'; 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'; diff --git a/src/user-management/serializers/list-user-api-keys-options.serializer.ts b/src/user-management/serializers/list-user-api-keys-options.serializer.ts new file mode 100644 index 000000000..180a6bd6d --- /dev/null +++ b/src/user-management/serializers/list-user-api-keys-options.serializer.ts @@ -0,0 +1,16 @@ +import { + ListUserApiKeysOptions, + SerializedListUserApiKeysOptions, +} from '../interfaces/list-user-api-keys-options.interface'; + +export function serializeListUserApiKeysOptions( + options: ListUserApiKeysOptions, +): SerializedListUserApiKeysOptions { + return { + limit: options.limit, + before: options.before, + after: options.after, + order: options.order, + organization_id: options.organizationId, + }; +} diff --git a/src/user-management/serializers/user-api-key-with-value.serializer.ts b/src/user-management/serializers/user-api-key-with-value.serializer.ts new file mode 100644 index 000000000..c9deb2656 --- /dev/null +++ b/src/user-management/serializers/user-api-key-with-value.serializer.ts @@ -0,0 +1,14 @@ +import { + SerializedUserApiKeyWithValue, + UserApiKeyWithValue, +} from '../interfaces/user-api-key-with-value.interface'; +import { deserializeUserApiKey } from './user-api-key.serializer'; + +export function deserializeUserApiKeyWithValue( + apiKey: SerializedUserApiKeyWithValue, +): UserApiKeyWithValue { + return { + ...deserializeUserApiKey(apiKey), + value: apiKey.value, + }; +} diff --git a/src/user-management/serializers/user-api-key.serializer.ts b/src/user-management/serializers/user-api-key.serializer.ts new file mode 100644 index 000000000..b48d284bb --- /dev/null +++ b/src/user-management/serializers/user-api-key.serializer.ts @@ -0,0 +1,25 @@ +import { + SerializedUserApiKey, + UserApiKey, +} from '../interfaces/user-api-key.interface'; + +export function deserializeUserApiKey( + apiKey: SerializedUserApiKey, +): UserApiKey { + return { + object: apiKey.object, + id: apiKey.id, + owner: { + type: 'user', + id: apiKey.owner.id, + organizationId: apiKey.owner.organization_id, + }, + name: apiKey.name, + obfuscatedValue: apiKey.obfuscated_value, + lastUsedAt: apiKey.last_used_at, + expiresAt: apiKey.expires_at, + permissions: apiKey.permissions, + createdAt: apiKey.created_at, + updatedAt: apiKey.updated_at, + }; +} diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index ad4371f6b..e71cf89bb 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -1,6 +1,7 @@ import fetch from 'jest-fetch-mock'; import { fetchBody, + fetchHeaders, fetchOnce, fetchSearchParams, fetchURL, @@ -13,11 +14,13 @@ import invitationFixture from './fixtures/invitation.json'; import listInvitationsFixture from './fixtures/list-invitations.json'; import listOrganizationMembershipsFixture from './fixtures/list-organization-memberships.json'; import listSessionsFixture from './fixtures/list-sessions.json'; +import listUserApiKeysFixture from './fixtures/list-user-api-keys.json'; import listUsersFixture from './fixtures/list-users.json'; import magicAuthFixture from './fixtures/magic_auth.json'; import organizationMembershipFixture from './fixtures/organization-membership.json'; 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 * as jose from 'jose'; import { sealData } from '../common/crypto/seal'; @@ -2129,6 +2132,117 @@ describe('UserManagement', () => { }); }); + describe('listUserApiKeys', () => { + it('returns API keys for the user', async () => { + fetchOnce(listUserApiKeysFixture); + + const { data, object, listMetadata } = + await workos.userManagement.listUserApiKeys(userId); + + expect(fetchURL()).toContain(`/user_management/users/${userId}/api_keys`); + expect(object).toEqual('list'); + expect(listMetadata).toEqual({ + before: null, + after: null, + }); + expect(data).toEqual([ + { + object: 'api_key', + id: 'api_key_01EHZNVPK3SFK441A1RGBFSHRT', + owner: { + type: 'user', + id: 'user_01EHZNVPK3SFK441A1RGBFSHRT', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }, + name: 'Production API Key', + obfuscatedValue: 'sk_...3456', + lastUsedAt: null, + expiresAt: null, + permissions: ['posts:read', 'posts:write'], + createdAt: '2026-01-15T12:00:00.000Z', + updatedAt: '2026-01-15T12:00:00.000Z', + }, + ]); + }); + + it('serializes pagination and organization filters', async () => { + fetchOnce(listUserApiKeysFixture); + + await workos.userManagement.listUserApiKeys(userId, { + limit: 10, + before: 'api_key_before_id', + after: 'api_key_after_id', + order: 'asc', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }); + + expect(fetchSearchParams()).toEqual({ + limit: '10', + before: 'api_key_before_id', + after: 'api_key_after_id', + order: 'asc', + organization_id: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }); + }); + }); + + describe('createUserApiKey', () => { + it('creates an API key for the user', async () => { + fetchOnce(createUserApiKeyFixture, { status: 201 }); + + const apiKey = await workos.userManagement.createUserApiKey(userId, { + name: 'Production API Key', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + permissions: ['posts:read', 'posts:write'], + expiresAt: new Date('2030-01-01T00:00:00.000Z'), + }); + + expect(fetchURL()).toContain(`/user_management/users/${userId}/api_keys`); + expect(fetchBody()).toEqual({ + name: 'Production API Key', + organization_id: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + permissions: ['posts:read', 'posts:write'], + expires_at: '2030-01-01T00:00:00.000Z', + }); + expect(apiKey).toEqual({ + object: 'api_key', + id: 'api_key_01EHZNVPK3SFK441A1RGBFSHRT', + owner: { + type: 'user', + id: 'user_01EHZNVPK3SFK441A1RGBFSHRT', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }, + name: 'Production API Key', + obfuscatedValue: 'sk_...3456', + lastUsedAt: null, + expiresAt: '2030-01-01T00:00:00.000Z', + permissions: ['posts:read', 'posts:write'], + createdAt: '2026-01-15T12:00:00.000Z', + updatedAt: '2026-01-15T12:00:00.000Z', + value: 'test_api_key_value', + }); + }); + + it('supports idempotency keys', async () => { + fetchOnce(createUserApiKeyFixture, { status: 201 }); + + await workos.userManagement.createUserApiKey( + userId, + { + name: 'Production API Key', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }, + { + idempotencyKey: 'the-idempotency-key', + }, + ); + + expect(fetchHeaders()).toMatchObject({ + 'Idempotency-Key': 'the-idempotency-key', + }); + }); + }); + describe('getUserIdentities', () => { it('sends a Get User Identities request', async () => { fetchOnce(identityFixture); diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index 775cc4c26..bc2c5130e 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -18,12 +18,16 @@ import { CreateMagicAuthResponse, CreateMagicAuthResponseResponse, CreatePasswordResetOptions, + CreateUserApiKeyOptions, + CreateUserApiKeyRequestOptions, CreateUserOptions, CreateUserResponse, CreateUserResponseResponse, EmailVerification, EmailVerificationResponse, ListSessionsOptions, + ListUserApiKeysOptions, + SerializedListUserApiKeysOptions, ListUsersOptions, LogoutURLOptions, MagicAuth, @@ -50,8 +54,12 @@ import { SessionResponse, UpdateUserOptions, User, + UserApiKey, + UserApiKeyWithValue, UserResponse, VerifyEmailOptions, + SerializedUserApiKey, + SerializedUserApiKeyWithValue, } from './interfaces'; import { AuthenticateWithEmailVerificationOptions, @@ -144,10 +152,14 @@ import { serializeAuthenticateWithTotpOptions, serializeCreateMagicAuthOptions, serializeCreatePasswordResetOptions, + serializeCreateUserApiKeyOptions, serializeCreateUserOptions, serializeListSessionsOptions, + serializeListUserApiKeysOptions, serializeResetPasswordOptions, serializeUpdateUserOptions, + deserializeUserApiKey, + deserializeUserApiKeyWithValue, } from './serializers'; import { serializeAuthenticateWithEmailVerificationOptions } from './serializers/authenticate-with-email-verification.serializer'; import { serializeAuthenticateWithOrganizationSelectionOptions } from './serializers/authenticate-with-organization-selection-options.serializer'; @@ -1043,6 +1055,69 @@ export class UserManagement { await this.workos.delete(`/user_management/users/${userId}`); } + /** + * List API keys for a user + * + * Get a list of API keys owned by a specific user. + * @param userId - Unique identifier of the user. + * @param options - Pagination and filter options. + * @returns {Promise>} + * @throws {NotFoundException} 404 + */ + async listUserApiKeys( + userId: string, + options?: ListUserApiKeysOptions, + ): Promise> { + const serializedOptions = options + ? serializeListUserApiKeysOptions(options) + : undefined; + + return new AutoPaginatable( + await fetchAndDeserialize( + this.workos, + `/user_management/users/${userId}/api_keys`, + deserializeUserApiKey, + serializedOptions, + ), + (params) => + fetchAndDeserialize( + this.workos, + `/user_management/users/${userId}/api_keys`, + deserializeUserApiKey, + params, + ), + serializedOptions, + ); + } + + /** + * Create an API key for a user + * + * Create a new API key owned by a user. The user must have an active membership in the specified organization. + * @param userId - Unique identifier of the user. + * @param options - Object containing the API key properties. + * @returns {Promise} + * @throws {BadRequestException} 400 + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async createUserApiKey( + userId: string, + options: CreateUserApiKeyOptions, + requestOptions: CreateUserApiKeyRequestOptions = {}, + ): Promise { + const { data } = await this.workos.post< + SerializedUserApiKeyWithValue, + ReturnType + >( + `/user_management/users/${userId}/api_keys`, + serializeCreateUserApiKeyOptions(options), + requestOptions, + ); + + return deserializeUserApiKeyWithValue(data); + } + /** * Get user identities *