diff --git a/docs/V9_MIGRATION_GUIDE.md b/docs/V9_MIGRATION_GUIDE.md index 6797967e2..e484ef1b8 100644 --- a/docs/V9_MIGRATION_GUIDE.md +++ b/docs/V9_MIGRATION_GUIDE.md @@ -363,3 +363,31 @@ This is intended to make strict TypeScript error handling easier, especially if The v9 update also tightens webhook/event deserialization behavior for unknown event types. If your integration assumed unknown event types would be ignored or treated loosely, re-test webhook handling on the latest v9 release. + +### User Management + +#### `getUserIdentities` now normalizes `GithubOAuth` to `GitHubOAuth` + +The WorkOS API returns `GithubOAuth` (lowercase 'h') as the provider in identity responses, but `getAuthorizationUrl` expects `GitHubOAuth` (uppercase 'H'). The SDK now normalizes the casing during deserialization so that identity provider values can be passed directly to `getAuthorizationUrl`. + +If your code was comparing against the raw API value `'GithubOAuth'`, update it to `'GitHubOAuth'`: + +**Before (v8):** + +```typescript +const identities = await workos.userManagement.getUserIdentities(userId); +const github = identities.find((i) => i.provider === 'GithubOAuth'); +``` + +**After (v9):** + +```typescript +const identities = await workos.userManagement.getUserIdentities(userId); +const github = identities.find((i) => i.provider === 'GitHubOAuth'); + +// The provider value can now be passed directly to getAuthorizationUrl +const url = workos.userManagement.getAuthorizationUrl({ + provider: github.provider, + redirectUri: 'https://example.com/callback', +}); +``` diff --git a/src/user-management/interfaces/identity-response.interface.ts b/src/user-management/interfaces/identity-response.interface.ts new file mode 100644 index 000000000..0d7042625 --- /dev/null +++ b/src/user-management/interfaces/identity-response.interface.ts @@ -0,0 +1,13 @@ +interface RawIdentityResponse { + idp_id: string; + type: 'OAuth'; + provider: + | 'AppleOAuth' + | 'GithubOAuth' + | 'GitHubOAuth' + | 'GoogleOAuth' + | 'MicrosoftOAuth' + | 'SalesforceOAuth'; +} + +export type { RawIdentityResponse }; diff --git a/src/user-management/interfaces/identity.interface.ts b/src/user-management/interfaces/identity.interface.ts index 00fe07178..c7b51724c 100644 --- a/src/user-management/interfaces/identity.interface.ts +++ b/src/user-management/interfaces/identity.interface.ts @@ -8,14 +8,3 @@ export interface Identity { | 'MicrosoftOAuth' | 'SalesforceOAuth'; } - -export interface IdentityResponse { - idp_id: string; - type: 'OAuth'; - provider: - | 'AppleOAuth' - | 'GoogleOAuth' - | 'GitHubOAuth' - | 'MicrosoftOAuth' - | 'SalesforceOAuth'; -} diff --git a/src/user-management/serializers/identity.serializer.spec.ts b/src/user-management/serializers/identity.serializer.spec.ts new file mode 100644 index 000000000..65c029a22 --- /dev/null +++ b/src/user-management/serializers/identity.serializer.spec.ts @@ -0,0 +1,25 @@ +import { deserializeIdentities } from './identity.serializer'; + +describe('deserializeIdentities', () => { + it('normalizes GithubOAuth to GitHubOAuth', () => { + const result = deserializeIdentities([ + { idp_id: '123', type: 'OAuth', provider: 'GithubOAuth' }, + ]); + + expect(result).toEqual([ + { idpId: '123', type: 'OAuth', provider: 'GitHubOAuth' }, + ]); + }); + + it('leaves other providers unchanged', () => { + const result = deserializeIdentities([ + { idp_id: '456', type: 'OAuth', provider: 'GoogleOAuth' }, + { idp_id: '789', type: 'OAuth', provider: 'AppleOAuth' }, + ]); + + expect(result).toEqual([ + { idpId: '456', type: 'OAuth', provider: 'GoogleOAuth' }, + { idpId: '789', type: 'OAuth', provider: 'AppleOAuth' }, + ]); + }); +}); diff --git a/src/user-management/serializers/identity.serializer.ts b/src/user-management/serializers/identity.serializer.ts index 04df64498..135fc6aca 100644 --- a/src/user-management/serializers/identity.serializer.ts +++ b/src/user-management/serializers/identity.serializer.ts @@ -1,13 +1,26 @@ -import { Identity, IdentityResponse } from '../interfaces/identity.interface'; +import { Identity } from '../interfaces/identity.interface'; +import { RawIdentityResponse } from '../interfaces/identity-response.interface'; + +// The API returns 'GithubOAuth' but getAuthorizationUrl expects 'GitHubOAuth'. +// Normalize here so callers can pass identity.provider directly. +// See: https://github.com/workos/workos-node/issues/1227 +const normalizeProvider = ( + provider: RawIdentityResponse['provider'], +): Identity['provider'] => { + if (provider === 'GithubOAuth') { + return 'GitHubOAuth'; + } + return provider; +}; export const deserializeIdentities = ( - identities: IdentityResponse[], + identities: RawIdentityResponse[], ): Identity[] => { return identities.map((identity) => { return { idpId: identity.idp_id, type: identity.type, - provider: identity.provider, + provider: normalizeProvider(identity.provider), }; }); }; diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index bf4073d1d..66894530c 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -1732,7 +1732,7 @@ describe('UserManagement', () => { { idpId: '108872335', type: 'OAuth', - provider: 'GithubOAuth', + provider: 'GitHubOAuth', }, { idpId: '111966195055680542408', @@ -1741,6 +1741,24 @@ describe('UserManagement', () => { }, ]); }); + + it('normalizes GithubOAuth to GitHubOAuth so it can be passed to getAuthorizationUrl', async () => { + fetchOnce(identityFixture); + + const identities = await workos.userManagement.getUserIdentities(userId); + + const githubIdentity = identities.find( + (i) => i.provider === 'GitHubOAuth', + ); + expect(githubIdentity).toBeDefined(); + + const url = workos.userManagement.getAuthorizationUrl({ + provider: githubIdentity!.provider, + redirectUri: 'https://example.com/callback', + }); + + expect(url).toContain('provider=GitHubOAuth'); + }); }); describe('getOrganizationMembership', () => { diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index cff94a5ce..ada58b696 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -73,7 +73,8 @@ import { CreateOrganizationMembershipOptions, SerializedCreateOrganizationMembershipOptions, } from './interfaces/create-organization-membership-options.interface'; -import { Identity, IdentityResponse } from './interfaces/identity.interface'; +import { Identity } from './interfaces/identity.interface'; +import { RawIdentityResponse } from './interfaces/identity-response.interface'; import { Invitation, InvitationResponse, @@ -812,7 +813,7 @@ export class UserManagement { throw new TypeError(`Incomplete arguments. Need to specify 'userId'.`); } - const { data } = await this.workos.get( + const { data } = await this.workos.get( `/user_management/users/${userId}/identities`, );