Skip to content

Commit c2ba134

Browse files
authored
feat(clerk-js,ui,shared): route <ConfigureSSO /> through org-scoped enterprise_connections (#8671)
1 parent fb184de commit c2ba134

19 files changed

Lines changed: 904 additions & 116 deletions

.changeset/wide-items-flow.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
'@clerk/ui': minor
5+
---
6+
7+
Internal `<ConfigureSSO />` refactor to call new org-scoped enterprise connections FAPI endpoints, replacing the `/me/` deprecated scope.

packages/clerk-js/src/core/resources/Organization.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@ import type {
22
AddMemberParams,
33
ClerkPaginatedResponse,
44
ClerkResourceReloadParams,
5+
CreateOrganizationEnterpriseConnectionParams,
56
CreateOrganizationParams,
7+
DeletedObjectJSON,
8+
DeletedObjectResource,
9+
EnterpriseConnectionJSON,
10+
EnterpriseConnectionResource,
11+
EnterpriseConnectionTestRunInitJSON,
12+
EnterpriseConnectionTestRunInitResource,
13+
EnterpriseConnectionTestRunJSON,
14+
EnterpriseConnectionTestRunResource,
15+
EnterpriseConnectionTestRunsPaginatedJSON,
616
GetDomainsParams,
17+
GetEnterpriseConnectionsParams,
18+
GetEnterpriseConnectionTestRunsParams,
719
GetInvitationsParams,
820
GetMembershipRequestParams,
921
GetMemberships,
@@ -23,13 +35,22 @@ import type {
2335
RoleJSON,
2436
SetOrganizationLogoParams,
2537
UpdateMembershipParams,
38+
UpdateOrganizationEnterpriseConnectionParams,
2639
UpdateOrganizationParams,
2740
} from '@clerk/shared/types';
2841

2942
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
3043
import { unixEpochToDate } from '../../utils/date';
44+
import { toEnterpriseConnectionBody } from '../../utils/enterpriseConnection';
3145
import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing';
32-
import { BaseResource, OrganizationInvitation, OrganizationMembership } from './internal';
46+
import {
47+
BaseResource,
48+
DeletedObject,
49+
EnterpriseConnection,
50+
EnterpriseConnectionTestRun,
51+
OrganizationInvitation,
52+
OrganizationMembership,
53+
} from './internal';
3354
import { OrganizationDomain } from './OrganizationDomain';
3455
import { OrganizationMembershipRequest } from './OrganizationMembershipRequest';
3556
import { Role } from './Role';
@@ -142,6 +163,107 @@ export class Organization extends BaseResource implements OrganizationResource {
142163
return new OrganizationDomain(json);
143164
};
144165

166+
getEnterpriseConnections = async (
167+
params?: GetEnterpriseConnectionsParams,
168+
): Promise<EnterpriseConnectionResource[]> => {
169+
const { withOrganizationAccountLinking } = params || {};
170+
171+
const json = (
172+
await BaseResource._fetch({
173+
path: `/organizations/${this.id}/enterprise_connections`,
174+
method: 'GET',
175+
...(withOrganizationAccountLinking !== undefined
176+
? {
177+
search: {
178+
with_organization_account_linking: String(withOrganizationAccountLinking),
179+
},
180+
}
181+
: {}),
182+
})
183+
)?.response as unknown as EnterpriseConnectionJSON[];
184+
185+
return (json || []).map(connection => new EnterpriseConnection(connection));
186+
};
187+
188+
createEnterpriseConnection = async (
189+
params: CreateOrganizationEnterpriseConnectionParams,
190+
): Promise<EnterpriseConnectionResource> => {
191+
const json = (
192+
await BaseResource._fetch<EnterpriseConnectionJSON>({
193+
path: `/organizations/${this.id}/enterprise_connections`,
194+
method: 'POST',
195+
body: toEnterpriseConnectionBody(params, { omitOrganizationId: true }) as any,
196+
})
197+
)?.response as unknown as EnterpriseConnectionJSON;
198+
199+
return new EnterpriseConnection(json);
200+
};
201+
202+
updateEnterpriseConnection = async (
203+
enterpriseConnectionId: string,
204+
params: UpdateOrganizationEnterpriseConnectionParams,
205+
): Promise<EnterpriseConnectionResource> => {
206+
const json = (
207+
await BaseResource._fetch<EnterpriseConnectionJSON>({
208+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}`,
209+
method: 'PATCH',
210+
body: toEnterpriseConnectionBody(params, { omitOrganizationId: true }) as any,
211+
})
212+
)?.response as unknown as EnterpriseConnectionJSON;
213+
214+
return new EnterpriseConnection(json);
215+
};
216+
217+
deleteEnterpriseConnection = async (enterpriseConnectionId: string): Promise<DeletedObjectResource> => {
218+
const json = (
219+
await BaseResource._fetch<DeletedObjectJSON>({
220+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}`,
221+
method: 'DELETE',
222+
})
223+
)?.response as unknown as DeletedObjectJSON;
224+
225+
return new DeletedObject(json);
226+
};
227+
228+
createEnterpriseConnectionTestRun = async (
229+
enterpriseConnectionId: string,
230+
): Promise<EnterpriseConnectionTestRunInitResource> => {
231+
const json = (
232+
await BaseResource._fetch({
233+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
234+
method: 'POST',
235+
})
236+
)?.response as unknown as EnterpriseConnectionTestRunInitJSON;
237+
238+
return { url: json.url };
239+
};
240+
241+
getEnterpriseConnectionTestRuns = async (
242+
enterpriseConnectionId: string,
243+
params?: GetEnterpriseConnectionTestRunsParams,
244+
): Promise<ClerkPaginatedResponse<EnterpriseConnectionTestRunResource>> => {
245+
const { status, ...rest } = params || {};
246+
const search = convertPageToOffsetSearchParams(rest);
247+
if (status?.length) {
248+
for (const s of status) {
249+
search.append('status', s);
250+
}
251+
}
252+
253+
const res = await BaseResource._fetch({
254+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
255+
method: 'GET',
256+
search,
257+
});
258+
259+
const payload = res?.response as unknown as EnterpriseConnectionTestRunsPaginatedJSON | undefined;
260+
261+
return {
262+
total_count: payload?.total_count ?? 0,
263+
data: (payload?.data ?? []).map((row: EnterpriseConnectionTestRunJSON) => new EnterpriseConnectionTestRun(row)),
264+
};
265+
};
266+
145267
getMembershipRequests = async (
146268
getRequestParam?: GetMembershipRequestParams,
147269
): Promise<ClerkPaginatedResponse<OrganizationMembershipRequestResource>> => {

packages/clerk-js/src/core/resources/User.ts

Lines changed: 7 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
ClerkPaginatedResponse,
66
CreateEmailAddressParams,
77
CreateExternalAccountParams,
8-
CreateMeEnterpriseConnectionParams,
8+
CreateOrganizationEnterpriseConnectionParams,
99
CreatePhoneNumberParams,
1010
CreateWeb3WalletParams,
1111
DeletedObjectJSON,
@@ -34,7 +34,7 @@ import type {
3434
SetProfileImageParams,
3535
TOTPJSON,
3636
TOTPResource,
37-
UpdateMeEnterpriseConnectionParams,
37+
UpdateOrganizationEnterpriseConnectionParams,
3838
UpdateUserMetadataParams,
3939
UpdateUserParams,
4040
UpdateUserPasswordParams,
@@ -47,6 +47,7 @@ import type {
4747

4848
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
4949
import { unixEpochToDate } from '../../utils/date';
50+
import { toEnterpriseConnectionBody } from '../../utils/enterpriseConnection';
5051
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
5152
import { eventBus, events } from '../events';
5253
import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing';
@@ -336,13 +337,13 @@ export class User extends BaseResource implements UserResource {
336337
};
337338

338339
createEnterpriseConnection = async (
339-
params: CreateMeEnterpriseConnectionParams,
340+
params: CreateOrganizationEnterpriseConnectionParams,
340341
): Promise<EnterpriseConnectionResource> => {
341342
const json = (
342343
await BaseResource._fetch<EnterpriseConnectionJSON>({
343344
path: `${this.path()}/enterprise_connections`,
344345
method: 'POST',
345-
body: toMeEnterpriseConnectionBody(params) as any,
346+
body: toEnterpriseConnectionBody(params) as any,
346347
})
347348
)?.response as unknown as EnterpriseConnectionJSON;
348349

@@ -351,13 +352,13 @@ export class User extends BaseResource implements UserResource {
351352

352353
updateEnterpriseConnection = async (
353354
enterpriseConnectionId: string,
354-
params: UpdateMeEnterpriseConnectionParams,
355+
params: UpdateOrganizationEnterpriseConnectionParams,
355356
): Promise<EnterpriseConnectionResource> => {
356357
const json = (
357358
await BaseResource._fetch<EnterpriseConnectionJSON>({
358359
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
359360
method: 'PATCH',
360-
body: toMeEnterpriseConnectionBody(params) as any,
361+
body: toEnterpriseConnectionBody(params) as any,
361362
})
362363
)?.response as unknown as EnterpriseConnectionJSON;
363364

@@ -553,69 +554,3 @@ export class User extends BaseResource implements UserResource {
553554
};
554555
}
555556
}
556-
557-
/**
558-
* Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams`
559-
* for the `/me/enterprise_connections` FAPI endpoints.
560-
*
561-
* The handler expects a flat form body where SAML and OIDC fields are
562-
* prefixed (e.g. `saml_idp_metadata_url`, `oidc_client_id`) rather
563-
* than nested under `saml`/`oidc` objects. `attribute_mapping` and
564-
* `custom_attributes` stay as object values and are JSON-stringified
565-
* by the form serializer downstream — their inner keys are
566-
* user-supplied data and must not be camel→snake transformed.
567-
*/
568-
function toMeEnterpriseConnectionBody(
569-
params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams,
570-
): Record<string, unknown> {
571-
const body: Record<string, unknown> = {};
572-
573-
// Top-level fields. `provider` is only on Create, the rest are shared
574-
setIfDefined(body, 'provider', (params as CreateMeEnterpriseConnectionParams).provider);
575-
setIfDefined(body, 'name', params.name);
576-
setIfDefined(body, 'organization_id', params.organizationId);
577-
setIfDefined(body, 'active', (params as UpdateMeEnterpriseConnectionParams).active);
578-
setIfDefined(body, 'sync_user_attributes', (params as UpdateMeEnterpriseConnectionParams).syncUserAttributes);
579-
setIfDefined(
580-
body,
581-
'disable_additional_identifications',
582-
(params as UpdateMeEnterpriseConnectionParams).disableAdditionalIdentifications,
583-
);
584-
setIfDefined(body, 'custom_attributes', (params as UpdateMeEnterpriseConnectionParams).customAttributes);
585-
586-
if (params.saml) {
587-
setIfDefined(body, 'saml_idp_entity_id', params.saml.idpEntityId);
588-
setIfDefined(body, 'saml_idp_sso_url', params.saml.idpSsoUrl);
589-
setIfDefined(body, 'saml_idp_certificate', params.saml.idpCertificate);
590-
setIfDefined(body, 'saml_idp_metadata_url', params.saml.idpMetadataUrl);
591-
setIfDefined(body, 'saml_idp_metadata', params.saml.idpMetadata);
592-
setIfDefined(body, 'saml_attribute_mapping', params.saml.attributeMapping);
593-
setIfDefined(body, 'saml_allow_subdomains', params.saml.allowSubdomains);
594-
setIfDefined(body, 'saml_allow_idp_initiated', params.saml.allowIdpInitiated);
595-
setIfDefined(body, 'saml_force_authn', params.saml.forceAuthn);
596-
}
597-
598-
if (params.oidc) {
599-
setIfDefined(body, 'oidc_client_id', params.oidc.clientId);
600-
setIfDefined(body, 'oidc_client_secret', params.oidc.clientSecret);
601-
setIfDefined(body, 'oidc_discovery_url', params.oidc.discoveryUrl);
602-
setIfDefined(body, 'oidc_auth_url', params.oidc.authUrl);
603-
setIfDefined(body, 'oidc_token_url', params.oidc.tokenUrl);
604-
setIfDefined(body, 'oidc_user_info_url', params.oidc.userInfoUrl);
605-
setIfDefined(body, 'oidc_requires_pkce', params.oidc.requiresPkce);
606-
}
607-
608-
return body;
609-
}
610-
611-
/**
612-
* Adds `value` under `key` only when the caller actually provided it.
613-
* Mirrors the SDK's existing semantics: `undefined` means "don't send
614-
* this field"; `null` is forwarded so users can explicitly clear a
615-
* value via the form-encoded body
616-
*/
617-
function setIfDefined(target: Record<string, unknown>, key: string, value: unknown): void {
618-
if (value !== undefined) {
619-
target[key] = value;
620-
}
621-
}

0 commit comments

Comments
 (0)