diff --git a/.changeset/orgs-1597-configure-sso-org-scoped.md b/.changeset/orgs-1597-configure-sso-org-scoped.md
new file mode 100644
index 00000000000..a8213d39c5f
--- /dev/null
+++ b/.changeset/orgs-1597-configure-sso-org-scoped.md
@@ -0,0 +1,12 @@
+---
+'@clerk/ui': minor
+'@clerk/shared': minor
+---
+
+`` now calls the org-scoped enterprise connections endpoints via `organization.*EnterpriseConnection*` methods. Previously, the wizard called `user.*EnterpriseConnection*` against the `/me/*` paths.
+
+Adds two new internal hooks in `@clerk/shared/react`, mirroring the user-scoped variants:
+- `__internal_useOrganizationEnterpriseConnections`
+- `__internal_useOrganizationEnterpriseConnectionTestRuns`
+
+The existing `__internal_useUserEnterpriseConnections` and `__internal_useEnterpriseConnectionTestRuns` hooks remain available as `@deprecated` aliases.
diff --git a/.changeset/orgs-1597-organization-enterprise-connections.md b/.changeset/orgs-1597-organization-enterprise-connections.md
new file mode 100644
index 00000000000..c9e88375a0a
--- /dev/null
+++ b/.changeset/orgs-1597-organization-enterprise-connections.md
@@ -0,0 +1,15 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/shared': minor
+---
+
+Add organization-scoped enterprise connection methods on the `Organization` resource: `getEnterpriseConnections`, `createEnterpriseConnection`, `updateEnterpriseConnection`, `deleteEnterpriseConnection`, `createEnterpriseConnectionTestRun`, and `getEnterpriseConnectionTestRuns`. These hit `/v1/organizations/{org_id}/enterprise_connections/*` and share the same flattened SAML/OIDC request body shape as the existing `User.*` equivalents.
+
+Renames the parameter types from `Me*` to `Organization*`:
+- `CreateOrganizationEnterpriseConnectionParams`
+- `UpdateOrganizationEnterpriseConnectionParams`
+- `OrganizationEnterpriseConnectionProvider`
+- `OrganizationEnterpriseConnectionSamlInput`
+- `OrganizationEnterpriseConnectionOidcInput`
+
+The previous `Me*` names remain available as `@deprecated` aliases for backwards compatibility.
diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts
index f0a2a21b9f2..c3c91cb4b73 100644
--- a/packages/clerk-js/src/core/resources/Organization.ts
+++ b/packages/clerk-js/src/core/resources/Organization.ts
@@ -2,8 +2,20 @@ import type {
AddMemberParams,
ClerkPaginatedResponse,
ClerkResourceReloadParams,
+ CreateOrganizationEnterpriseConnectionParams,
CreateOrganizationParams,
+ DeletedObjectJSON,
+ DeletedObjectResource,
+ EnterpriseConnectionJSON,
+ EnterpriseConnectionResource,
+ EnterpriseConnectionTestRunInitJSON,
+ EnterpriseConnectionTestRunInitResource,
+ EnterpriseConnectionTestRunJSON,
+ EnterpriseConnectionTestRunResource,
+ EnterpriseConnectionTestRunsPaginatedJSON,
GetDomainsParams,
+ GetEnterpriseConnectionsParams,
+ GetEnterpriseConnectionTestRunsParams,
GetInvitationsParams,
GetMembershipRequestParams,
GetMemberships,
@@ -23,13 +35,22 @@ import type {
RoleJSON,
SetOrganizationLogoParams,
UpdateMembershipParams,
+ UpdateOrganizationEnterpriseConnectionParams,
UpdateOrganizationParams,
} from '@clerk/shared/types';
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
import { unixEpochToDate } from '../../utils/date';
+import { toEnterpriseConnectionBody } from '../../utils/enterpriseConnection';
import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing';
-import { BaseResource, OrganizationInvitation, OrganizationMembership } from './internal';
+import {
+ BaseResource,
+ DeletedObject,
+ EnterpriseConnection,
+ EnterpriseConnectionTestRun,
+ OrganizationInvitation,
+ OrganizationMembership,
+} from './internal';
import { OrganizationDomain } from './OrganizationDomain';
import { OrganizationMembershipRequest } from './OrganizationMembershipRequest';
import { Role } from './Role';
@@ -142,6 +163,107 @@ export class Organization extends BaseResource implements OrganizationResource {
return new OrganizationDomain(json);
};
+ getEnterpriseConnections = async (
+ params?: GetEnterpriseConnectionsParams,
+ ): Promise => {
+ const { withOrganizationAccountLinking } = params || {};
+
+ const json = (
+ await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections`,
+ method: 'GET',
+ ...(withOrganizationAccountLinking !== undefined
+ ? {
+ search: {
+ with_organization_account_linking: String(withOrganizationAccountLinking),
+ },
+ }
+ : {}),
+ })
+ )?.response as unknown as EnterpriseConnectionJSON[];
+
+ return (json || []).map(connection => new EnterpriseConnection(connection));
+ };
+
+ createEnterpriseConnection = async (
+ params: CreateOrganizationEnterpriseConnectionParams,
+ ): Promise => {
+ const json = (
+ await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections`,
+ method: 'POST',
+ body: toEnterpriseConnectionBody(params, { omitOrganizationId: true }) as any,
+ })
+ )?.response as unknown as EnterpriseConnectionJSON;
+
+ return new EnterpriseConnection(json);
+ };
+
+ updateEnterpriseConnection = async (
+ enterpriseConnectionId: string,
+ params: UpdateOrganizationEnterpriseConnectionParams,
+ ): Promise => {
+ const json = (
+ await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}`,
+ method: 'PATCH',
+ body: toEnterpriseConnectionBody(params, { omitOrganizationId: true }) as any,
+ })
+ )?.response as unknown as EnterpriseConnectionJSON;
+
+ return new EnterpriseConnection(json);
+ };
+
+ deleteEnterpriseConnection = async (enterpriseConnectionId: string): Promise => {
+ const json = (
+ await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}`,
+ method: 'DELETE',
+ })
+ )?.response as unknown as DeletedObjectJSON;
+
+ return new DeletedObject(json);
+ };
+
+ createEnterpriseConnectionTestRun = async (
+ enterpriseConnectionId: string,
+ ): Promise => {
+ const json = (
+ await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
+ method: 'POST',
+ })
+ )?.response as unknown as EnterpriseConnectionTestRunInitJSON;
+
+ return { url: json.url };
+ };
+
+ getEnterpriseConnectionTestRuns = async (
+ enterpriseConnectionId: string,
+ params?: GetEnterpriseConnectionTestRunsParams,
+ ): Promise> => {
+ const { status, ...rest } = params || {};
+ const search = convertPageToOffsetSearchParams(rest);
+ if (status?.length) {
+ for (const s of status) {
+ search.append('status', s);
+ }
+ }
+
+ const res = await BaseResource._fetch({
+ path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
+ method: 'GET',
+ search,
+ });
+
+ const payload = res?.response as unknown as EnterpriseConnectionTestRunsPaginatedJSON | undefined;
+
+ return {
+ total_count: payload?.total_count ?? 0,
+ data: (payload?.data ?? []).map((row: EnterpriseConnectionTestRunJSON) => new EnterpriseConnectionTestRun(row)),
+ };
+ };
+
getMembershipRequests = async (
getRequestParam?: GetMembershipRequestParams,
): Promise> => {
diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts
index 9ec8dfd5ea3..a88775a87ca 100644
--- a/packages/clerk-js/src/core/resources/User.ts
+++ b/packages/clerk-js/src/core/resources/User.ts
@@ -5,7 +5,7 @@ import type {
ClerkPaginatedResponse,
CreateEmailAddressParams,
CreateExternalAccountParams,
- CreateMeEnterpriseConnectionParams,
+ CreateOrganizationEnterpriseConnectionParams,
CreatePhoneNumberParams,
CreateWeb3WalletParams,
DeletedObjectJSON,
@@ -34,7 +34,7 @@ import type {
SetProfileImageParams,
TOTPJSON,
TOTPResource,
- UpdateMeEnterpriseConnectionParams,
+ UpdateOrganizationEnterpriseConnectionParams,
UpdateUserMetadataParams,
UpdateUserParams,
UpdateUserPasswordParams,
@@ -47,6 +47,7 @@ import type {
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
import { unixEpochToDate } from '../../utils/date';
+import { toEnterpriseConnectionBody } from '../../utils/enterpriseConnection';
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
import { eventBus, events } from '../events';
import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing';
@@ -336,13 +337,13 @@ export class User extends BaseResource implements UserResource {
};
createEnterpriseConnection = async (
- params: CreateMeEnterpriseConnectionParams,
+ params: CreateOrganizationEnterpriseConnectionParams,
): Promise => {
const json = (
await BaseResource._fetch({
path: `${this.path()}/enterprise_connections`,
method: 'POST',
- body: toMeEnterpriseConnectionBody(params) as any,
+ body: toEnterpriseConnectionBody(params) as any,
})
)?.response as unknown as EnterpriseConnectionJSON;
@@ -351,13 +352,13 @@ export class User extends BaseResource implements UserResource {
updateEnterpriseConnection = async (
enterpriseConnectionId: string,
- params: UpdateMeEnterpriseConnectionParams,
+ params: UpdateOrganizationEnterpriseConnectionParams,
): Promise => {
const json = (
await BaseResource._fetch({
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
method: 'PATCH',
- body: toMeEnterpriseConnectionBody(params) as any,
+ body: toEnterpriseConnectionBody(params) as any,
})
)?.response as unknown as EnterpriseConnectionJSON;
@@ -553,69 +554,3 @@ export class User extends BaseResource implements UserResource {
};
}
}
-
-/**
- * Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams`
- * for the `/me/enterprise_connections` FAPI endpoints.
- *
- * The handler expects a flat form body where SAML and OIDC fields are
- * prefixed (e.g. `saml_idp_metadata_url`, `oidc_client_id`) rather
- * than nested under `saml`/`oidc` objects. `attribute_mapping` and
- * `custom_attributes` stay as object values and are JSON-stringified
- * by the form serializer downstream — their inner keys are
- * user-supplied data and must not be camel→snake transformed.
- */
-function toMeEnterpriseConnectionBody(
- params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams,
-): Record {
- const body: Record = {};
-
- // Top-level fields. `provider` is only on Create, the rest are shared
- setIfDefined(body, 'provider', (params as CreateMeEnterpriseConnectionParams).provider);
- setIfDefined(body, 'name', params.name);
- setIfDefined(body, 'organization_id', params.organizationId);
- setIfDefined(body, 'active', (params as UpdateMeEnterpriseConnectionParams).active);
- setIfDefined(body, 'sync_user_attributes', (params as UpdateMeEnterpriseConnectionParams).syncUserAttributes);
- setIfDefined(
- body,
- 'disable_additional_identifications',
- (params as UpdateMeEnterpriseConnectionParams).disableAdditionalIdentifications,
- );
- setIfDefined(body, 'custom_attributes', (params as UpdateMeEnterpriseConnectionParams).customAttributes);
-
- if (params.saml) {
- setIfDefined(body, 'saml_idp_entity_id', params.saml.idpEntityId);
- setIfDefined(body, 'saml_idp_sso_url', params.saml.idpSsoUrl);
- setIfDefined(body, 'saml_idp_certificate', params.saml.idpCertificate);
- setIfDefined(body, 'saml_idp_metadata_url', params.saml.idpMetadataUrl);
- setIfDefined(body, 'saml_idp_metadata', params.saml.idpMetadata);
- setIfDefined(body, 'saml_attribute_mapping', params.saml.attributeMapping);
- setIfDefined(body, 'saml_allow_subdomains', params.saml.allowSubdomains);
- setIfDefined(body, 'saml_allow_idp_initiated', params.saml.allowIdpInitiated);
- setIfDefined(body, 'saml_force_authn', params.saml.forceAuthn);
- }
-
- if (params.oidc) {
- setIfDefined(body, 'oidc_client_id', params.oidc.clientId);
- setIfDefined(body, 'oidc_client_secret', params.oidc.clientSecret);
- setIfDefined(body, 'oidc_discovery_url', params.oidc.discoveryUrl);
- setIfDefined(body, 'oidc_auth_url', params.oidc.authUrl);
- setIfDefined(body, 'oidc_token_url', params.oidc.tokenUrl);
- setIfDefined(body, 'oidc_user_info_url', params.oidc.userInfoUrl);
- setIfDefined(body, 'oidc_requires_pkce', params.oidc.requiresPkce);
- }
-
- return body;
-}
-
-/**
- * Adds `value` under `key` only when the caller actually provided it.
- * Mirrors the SDK's existing semantics: `undefined` means "don't send
- * this field"; `null` is forwarded so users can explicitly clear a
- * value via the form-encoded body
- */
-function setIfDefined(target: Record, key: string, value: unknown): void {
- if (value !== undefined) {
- target[key] = value;
- }
-}
diff --git a/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts b/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts
index bcdf397c8e9..d88f1a56072 100644
--- a/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts
+++ b/packages/clerk-js/src/core/resources/__tests__/Organization.test.ts
@@ -1,6 +1,28 @@
-import { describe, expect, it } from 'vitest';
+import type { EnterpriseConnectionJSON, OrganizationJSON } from '@clerk/shared/types';
+import { describe, expect, it, vi } from 'vitest';
-import { Organization } from '../internal';
+import { BaseResource, Organization } from '../internal';
+
+const ORG_ID = 'org_123';
+
+function createOrganization(): Organization {
+ return new Organization({
+ object: 'organization',
+ id: ORG_ID,
+ name: 'Acme Corp',
+ slug: 'acme',
+ image_url: '',
+ has_image: false,
+ public_metadata: {},
+ members_count: 1,
+ pending_invitations_count: 0,
+ max_allowed_memberships: 5,
+ admin_delete_enabled: true,
+ self_serve_sso_enabled: true,
+ created_at: 1,
+ updated_at: 2,
+ } as unknown as OrganizationJSON);
+}
describe('Organization', () => {
it('has the same initial properties', () => {
@@ -41,4 +63,242 @@ describe('Organization', () => {
},
});
});
+
+ describe('enterprise connections', () => {
+ it('fetches enterprise connections from the org-scoped path', async () => {
+ const enterpriseConnectionsJSON: EnterpriseConnectionJSON[] = [
+ {
+ id: 'ec_123',
+ object: 'enterprise_connection',
+ name: 'Acme Corp SSO',
+ active: true,
+ allow_organization_account_linking: true,
+ provider: 'saml_okta',
+ logo_public_url: null,
+ domains: ['acme.com'],
+ organization_id: ORG_ID,
+ sync_user_attributes: true,
+ disable_additional_identifications: false,
+ custom_attributes: [],
+ oauth_config: null,
+ saml_connection: {
+ id: 'saml_123',
+ name: 'Acme Corp SSO',
+ active: true,
+ idp_entity_id: 'https://idp.acme.com/entity',
+ idp_sso_url: 'https://idp.acme.com/sso',
+ idp_certificate: 'MIICertificatePlaceholder',
+ idp_metadata_url: 'https://idp.acme.com/metadata',
+ idp_metadata: '',
+ acs_url: 'https://clerk.example.com/v1/saml/acs',
+ sp_entity_id: 'https://clerk.example.com',
+ sp_metadata_url: 'https://clerk.example.com/v1/saml/metadata',
+ allow_subdomains: false,
+ allow_idp_initiated: false,
+ force_authn: false,
+ },
+ created_at: 1234567890,
+ updated_at: 1234567890,
+ },
+ ];
+
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionsJSON }));
+
+ const organization = createOrganization();
+
+ const connections = await organization.getEnterpriseConnections();
+
+ // @ts-ignore
+ expect(BaseResource._fetch).toHaveBeenCalledWith({
+ method: 'GET',
+ path: `/organizations/${ORG_ID}/enterprise_connections`,
+ });
+
+ expect(connections).toHaveLength(1);
+ expect(connections[0].name).toBe('Acme Corp SSO');
+ expect(connections[0].allowOrganizationAccountLinking).toBe(true);
+ });
+
+ it('creates an enterprise connection without forwarding organization_id in the body', async () => {
+ const enterpriseConnectionJSON = {
+ id: 'ec_new',
+ object: 'enterprise_connection' as const,
+ name: 'New SSO',
+ active: true,
+ provider: 'saml_okta',
+ logo_public_url: null,
+ domains: [],
+ organization_id: ORG_ID,
+ sync_user_attributes: true,
+ disable_additional_identifications: false,
+ allow_organization_account_linking: false,
+ custom_attributes: [],
+ oauth_config: null,
+ saml_connection: null,
+ created_at: 1234567890,
+ updated_at: 1234567890,
+ };
+
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON }));
+
+ const organization = createOrganization();
+
+ const conn = await organization.createEnterpriseConnection({
+ provider: 'saml_okta',
+ name: 'New SSO',
+ // Even though callers may still pass this for convenience, the SDK
+ // must not include it in the body — the org URL is authoritative.
+ organizationId: ORG_ID,
+ saml: { idpEntityId: 'https://idp.example.com' },
+ });
+
+ // @ts-ignore
+ expect(BaseResource._fetch).toHaveBeenCalledWith({
+ method: 'POST',
+ path: `/organizations/${ORG_ID}/enterprise_connections`,
+ body: {
+ provider: 'saml_okta',
+ name: 'New SSO',
+ saml_idp_entity_id: 'https://idp.example.com',
+ },
+ });
+
+ // @ts-ignore
+ const callBody = BaseResource._fetch.mock.calls[0][0].body;
+ expect(callBody.organization_id).toBeUndefined();
+
+ expect(conn.id).toBe('ec_new');
+ expect(conn.name).toBe('New SSO');
+ });
+
+ it('updates an enterprise connection without forwarding organization_id in the body', async () => {
+ const enterpriseConnectionJSON = {
+ id: 'ec_123',
+ object: 'enterprise_connection' as const,
+ name: 'Updated',
+ active: false,
+ provider: 'saml_okta',
+ logo_public_url: null,
+ domains: [],
+ organization_id: ORG_ID,
+ sync_user_attributes: true,
+ disable_additional_identifications: false,
+ allow_organization_account_linking: false,
+ custom_attributes: [],
+ oauth_config: null,
+ saml_connection: null,
+ created_at: 1234567890,
+ updated_at: 1234567900,
+ };
+
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON }));
+
+ const organization = createOrganization();
+
+ await organization.updateEnterpriseConnection('ec_123', {
+ name: 'Updated',
+ active: false,
+ syncUserAttributes: true,
+ organizationId: ORG_ID,
+ });
+
+ // @ts-ignore
+ expect(BaseResource._fetch).toHaveBeenCalledWith({
+ method: 'PATCH',
+ path: `/organizations/${ORG_ID}/enterprise_connections/ec_123`,
+ body: {
+ name: 'Updated',
+ active: false,
+ sync_user_attributes: true,
+ },
+ });
+
+ // @ts-ignore
+ const callBody = BaseResource._fetch.mock.calls[0][0].body;
+ expect(callBody.organization_id).toBeUndefined();
+ });
+
+ it('deletes an enterprise connection', async () => {
+ const deletedJSON = {
+ object: 'enterprise_connection',
+ id: 'ec_123',
+ deleted: true,
+ };
+
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedJSON }));
+
+ const organization = createOrganization();
+
+ const result = await organization.deleteEnterpriseConnection('ec_123');
+
+ // @ts-ignore
+ expect(BaseResource._fetch).toHaveBeenCalledWith({
+ method: 'DELETE',
+ path: `/organizations/${ORG_ID}/enterprise_connections/ec_123`,
+ });
+
+ expect(result.id).toBe('ec_123');
+ expect(result.deleted).toBe(true);
+ });
+
+ it('creates an enterprise connection test run', async () => {
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: { url: 'https://example.com/test' } }));
+
+ const organization = createOrganization();
+
+ const init = await organization.createEnterpriseConnectionTestRun('ec_123');
+
+ // @ts-ignore
+ expect(BaseResource._fetch).toHaveBeenCalledWith({
+ method: 'POST',
+ path: `/organizations/${ORG_ID}/enterprise_connections/ec_123/test_runs`,
+ });
+
+ expect(init.url).toBe('https://example.com/test');
+ });
+
+ it('lists enterprise connection test runs', async () => {
+ const paginated = {
+ data: [
+ {
+ object: 'enterprise_connection_test_run' as const,
+ id: 'run_1',
+ status: 'success',
+ connection_type: 'saml' as const,
+ created_at: 1700000000000,
+ },
+ ],
+ total_count: 1,
+ };
+
+ // @ts-ignore
+ BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: paginated }));
+
+ const organization = createOrganization();
+
+ const result = await organization.getEnterpriseConnectionTestRuns('ec_123', {
+ initialPage: 1,
+ pageSize: 10,
+ status: ['pending', 'success'],
+ });
+
+ // @ts-ignore
+ const call = BaseResource._fetch.mock.calls[0][0];
+ expect(call.method).toBe('GET');
+ expect(call.path).toBe(`/organizations/${ORG_ID}/enterprise_connections/ec_123/test_runs`);
+ expect(call.search.get('limit')).toBe('10');
+ expect(call.search.get('offset')).toBe('0');
+ expect(call.search.getAll('status')).toEqual(['pending', 'success']);
+
+ expect(result.total_count).toBe(1);
+ expect(result.data).toHaveLength(1);
+ expect(result.data[0].id).toBe('run_1');
+ expect(result.data[0].connectionType).toBe('saml');
+ });
+ });
});
diff --git a/packages/clerk-js/src/utils/enterpriseConnection.ts b/packages/clerk-js/src/utils/enterpriseConnection.ts
new file mode 100644
index 00000000000..54413ee218d
--- /dev/null
+++ b/packages/clerk-js/src/utils/enterpriseConnection.ts
@@ -0,0 +1,92 @@
+import type {
+ CreateOrganizationEnterpriseConnectionParams,
+ UpdateOrganizationEnterpriseConnectionParams,
+} from '@clerk/shared/types';
+
+type EnterpriseConnectionParams =
+ | CreateOrganizationEnterpriseConnectionParams
+ | UpdateOrganizationEnterpriseConnectionParams;
+
+export type ToEnterpriseConnectionBodyOptions = {
+ /**
+ * When `true`, omit `organization_id` from the body even if it was provided
+ * in `params`. Use this for org-scoped endpoints
+ * (`/v1/organizations/{org_id}/enterprise_connections/*`) where the URL
+ * path is authoritative.
+ */
+ omitOrganizationId?: boolean;
+};
+
+/**
+ * Serializes `CreateOrganizationEnterpriseConnectionParams` /
+ * `UpdateOrganizationEnterpriseConnectionParams` for the enterprise
+ * connection FAPI endpoints.
+ *
+ * The handlers expect a flat form body where SAML and OIDC fields are
+ * prefixed (e.g. `saml_idp_metadata_url`, `oidc_client_id`) rather than
+ * nested under `saml`/`oidc` objects. `attribute_mapping` and
+ * `custom_attributes` stay as object values and are JSON-stringified by
+ * the form serializer downstream — their inner keys are user-supplied
+ * data and must not be camel→snake transformed.
+ */
+export function toEnterpriseConnectionBody(
+ params: EnterpriseConnectionParams,
+ options: ToEnterpriseConnectionBodyOptions = {},
+): Record {
+ const body: Record = {};
+
+ // Top-level fields. `provider` is only on Create, the rest are shared.
+ setIfDefined(body, 'provider', (params as CreateOrganizationEnterpriseConnectionParams).provider);
+ setIfDefined(body, 'name', params.name);
+ if (!options.omitOrganizationId) {
+ setIfDefined(body, 'organization_id', params.organizationId);
+ }
+ setIfDefined(body, 'active', (params as UpdateOrganizationEnterpriseConnectionParams).active);
+ setIfDefined(
+ body,
+ 'sync_user_attributes',
+ (params as UpdateOrganizationEnterpriseConnectionParams).syncUserAttributes,
+ );
+ setIfDefined(
+ body,
+ 'disable_additional_identifications',
+ (params as UpdateOrganizationEnterpriseConnectionParams).disableAdditionalIdentifications,
+ );
+ setIfDefined(body, 'custom_attributes', (params as UpdateOrganizationEnterpriseConnectionParams).customAttributes);
+
+ if (params.saml) {
+ setIfDefined(body, 'saml_idp_entity_id', params.saml.idpEntityId);
+ setIfDefined(body, 'saml_idp_sso_url', params.saml.idpSsoUrl);
+ setIfDefined(body, 'saml_idp_certificate', params.saml.idpCertificate);
+ setIfDefined(body, 'saml_idp_metadata_url', params.saml.idpMetadataUrl);
+ setIfDefined(body, 'saml_idp_metadata', params.saml.idpMetadata);
+ setIfDefined(body, 'saml_attribute_mapping', params.saml.attributeMapping);
+ setIfDefined(body, 'saml_allow_subdomains', params.saml.allowSubdomains);
+ setIfDefined(body, 'saml_allow_idp_initiated', params.saml.allowIdpInitiated);
+ setIfDefined(body, 'saml_force_authn', params.saml.forceAuthn);
+ }
+
+ if (params.oidc) {
+ setIfDefined(body, 'oidc_client_id', params.oidc.clientId);
+ setIfDefined(body, 'oidc_client_secret', params.oidc.clientSecret);
+ setIfDefined(body, 'oidc_discovery_url', params.oidc.discoveryUrl);
+ setIfDefined(body, 'oidc_auth_url', params.oidc.authUrl);
+ setIfDefined(body, 'oidc_token_url', params.oidc.tokenUrl);
+ setIfDefined(body, 'oidc_user_info_url', params.oidc.userInfoUrl);
+ setIfDefined(body, 'oidc_requires_pkce', params.oidc.requiresPkce);
+ }
+
+ return body;
+}
+
+/**
+ * Adds `value` under `key` only when the caller actually provided it.
+ * Mirrors the SDK's existing semantics: `undefined` means "don't send
+ * this field"; `null` is forwarded so users can explicitly clear a
+ * value via the form-encoded body.
+ */
+function setIfDefined(target: Record, key: string, value: unknown): void {
+ if (value !== undefined) {
+ target[key] = value;
+ }
+}
diff --git a/packages/shared/src/react/hooks/index.ts b/packages/shared/src/react/hooks/index.ts
index fd2c7e5cb0d..020bad2f0b3 100644
--- a/packages/shared/src/react/hooks/index.ts
+++ b/packages/shared/src/react/hooks/index.ts
@@ -43,6 +43,16 @@ export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';
+export { __internal_useOrganizationEnterpriseConnections } from './useOrganizationEnterpriseConnections';
+export type {
+ UseOrganizationEnterpriseConnectionsParams,
+ UseOrganizationEnterpriseConnectionsReturn,
+} from './useOrganizationEnterpriseConnections';
+export { __internal_useOrganizationEnterpriseConnectionTestRuns } from './useOrganizationEnterpriseConnectionTestRuns';
+export type {
+ UseOrganizationEnterpriseConnectionTestRunsParams,
+ UseOrganizationEnterpriseConnectionTestRunsReturn,
+} from './useOrganizationEnterpriseConnectionTestRuns';
export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
diff --git a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts
new file mode 100644
index 00000000000..08690c8d7d4
--- /dev/null
+++ b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts
@@ -0,0 +1,31 @@
+import { useMemo } from 'react';
+
+import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
+import { INTERNAL_STABLE_KEYS } from '../stable-keys';
+import { createCacheKeys } from './createCacheKeys';
+
+/**
+ * @internal
+ */
+export function useOrganizationEnterpriseConnectionTestRunsCacheKeys(params: {
+ organizationId: string | null;
+ enterpriseConnectionId: string | null;
+ args: GetEnterpriseConnectionTestRunsParams;
+}) {
+ const { organizationId, enterpriseConnectionId, args } = params;
+ return useMemo(() => {
+ return createCacheKeys({
+ stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
+ authenticated: Boolean(organizationId),
+ tracked: {
+ organizationId: organizationId ?? null,
+ enterpriseConnectionId: enterpriseConnectionId ?? null,
+ },
+ untracked: {
+ args,
+ },
+ });
+ // The args object is intentionally serialized via the consumer to keep stability.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [organizationId, enterpriseConnectionId, JSON.stringify(args)]);
+}
diff --git a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.tsx b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.tsx
new file mode 100644
index 00000000000..7d3a7af75f4
--- /dev/null
+++ b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.tsx
@@ -0,0 +1,139 @@
+import { useCallback, useEffect, useState } from 'react';
+
+import type {
+ EnterpriseConnectionTestRunResource,
+ GetEnterpriseConnectionTestRunsParams,
+} from '../../types/enterpriseConnectionTestRun';
+import { useClerkInstanceContext } from '../contexts';
+import { useClerkQueryClient } from '../query/use-clerk-query-client';
+import { useClerkQuery } from '../query/useQuery';
+import { useOrganizationBase } from './base/useOrganizationBase';
+import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
+import { useOrganizationEnterpriseConnectionTestRunsCacheKeys } from './useOrganizationEnterpriseConnectionTestRuns.shared';
+
+const DEFAULT_POLL_INTERVAL_MS = 2_000;
+
+export type UseOrganizationEnterpriseConnectionTestRunsParams = {
+ enterpriseConnectionId: string | null;
+ /**
+ * Pass-through fetch parameters (pagination, status filter).
+ * Defaults to `{ initialPage: 1, pageSize: 10 }`.
+ */
+ params?: GetEnterpriseConnectionTestRunsParams;
+ /**
+ * Polling interval (ms) applied between `revalidate()` and the moment the
+ * first record arrives in the response.
+ *
+ * @default 2000
+ */
+ pollIntervalMs?: number;
+ /**
+ * If `false`, the hook is dormant — no fetch, no polling.
+ *
+ * @default true
+ */
+ enabled?: boolean;
+};
+
+export type UseOrganizationEnterpriseConnectionTestRunsReturn = {
+ data: EnterpriseConnectionTestRunResource[] | undefined;
+ totalCount: number | undefined;
+ error: Error | null;
+ isLoading: boolean;
+ isFetching: boolean;
+ /**
+ * `true` while the hook is actively polling for the first record to appear
+ */
+ isPolling: boolean;
+ /**
+ * Force a refetch and (if the list is currently empty) arm polling
+ */
+ revalidate: () => Promise;
+};
+
+/**
+ * Subscribes to the list of enterprise-connection test runs for the active organization
+ *
+ * @internal
+ */
+function useOrganizationEnterpriseConnectionTestRuns(
+ params: UseOrganizationEnterpriseConnectionTestRunsParams,
+): UseOrganizationEnterpriseConnectionTestRunsReturn {
+ const {
+ enterpriseConnectionId,
+ params: fetchParams = { initialPage: 1, pageSize: 10 },
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
+ enabled = true,
+ } = params;
+
+ const clerk = useClerkInstanceContext();
+ const organization = useOrganizationBase();
+ const [queryClient] = useClerkQueryClient();
+
+ const { queryKey, invalidationKey, stableKey, authenticated } = useOrganizationEnterpriseConnectionTestRunsCacheKeys({
+ organizationId: organization?.id ?? null,
+ enterpriseConnectionId,
+ args: fetchParams,
+ });
+
+ useClearQueriesOnSignOut({
+ isSignedOut: organization === null,
+ authenticated,
+ stableKeys: stableKey,
+ });
+
+ const queryEnabled = enabled && clerk.loaded && Boolean(organization) && Boolean(enterpriseConnectionId);
+
+ const [shouldPoll, setShouldPoll] = useState(false);
+
+ const query = useClerkQuery({
+ queryKey,
+ queryFn: () => {
+ if (!enterpriseConnectionId) {
+ throw new Error('enterpriseConnectionId is required to fetch test runs');
+ }
+ return organization?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
+ },
+ refetchInterval: q => {
+ if (!shouldPoll) {
+ return false;
+ }
+
+ const hasRows = (q.state.data?.data?.length ?? 0) > 0;
+ return hasRows ? false : pollIntervalMs;
+ },
+ enabled: queryEnabled,
+ refetchIntervalInBackground: false,
+ });
+
+ const hasRows = (query.data?.data?.length ?? 0) > 0;
+
+ useEffect(() => {
+ if (shouldPoll && hasRows) {
+ setShouldPoll(false);
+ }
+ }, [shouldPoll, hasRows]);
+
+ const revalidate = useCallback(async () => {
+ // Only arm polling when there is nothing in the list yet — once any record
+ // has been seen, this is a one-shot refetch.
+ if (!hasRows) {
+ setShouldPoll(true);
+ }
+ await queryClient.invalidateQueries({ queryKey: invalidationKey });
+ }, [queryClient, invalidationKey, hasRows]);
+
+ const isPolling = queryEnabled && shouldPoll && !hasRows;
+
+ return {
+ data: query.data?.data,
+ totalCount: query.data?.total_count,
+ error: query.error ?? null,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ isPolling,
+ revalidate,
+ };
+}
+
+export { useOrganizationEnterpriseConnectionTestRuns as __internal_useOrganizationEnterpriseConnectionTestRuns };
diff --git a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.shared.ts b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.shared.ts
new file mode 100644
index 00000000000..299f1d40883
--- /dev/null
+++ b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.shared.ts
@@ -0,0 +1,27 @@
+import { useMemo } from 'react';
+
+import { INTERNAL_STABLE_KEYS } from '../stable-keys';
+import { createCacheKeys } from './createCacheKeys';
+
+/**
+ * @internal
+ */
+export function useOrganizationEnterpriseConnectionsCacheKeys(params: {
+ organizationId: string | null;
+ withOrganizationAccountLinking?: boolean;
+}) {
+ const { organizationId, withOrganizationAccountLinking = false } = params;
+ return useMemo(() => {
+ return createCacheKeys({
+ stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY,
+ authenticated: Boolean(organizationId),
+ tracked: {
+ organizationId: organizationId ?? null,
+ withOrganizationAccountLinking,
+ },
+ untracked: {
+ args: {},
+ },
+ });
+ }, [organizationId, withOrganizationAccountLinking]);
+}
diff --git a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.tsx b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.tsx
new file mode 100644
index 00000000000..7cec73bce99
--- /dev/null
+++ b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnections.tsx
@@ -0,0 +1,116 @@
+import { useCallback } from 'react';
+
+import type { DeletedObjectResource } from '../../types/deletedObject';
+import type {
+ CreateOrganizationEnterpriseConnectionParams,
+ EnterpriseConnectionResource,
+ UpdateOrganizationEnterpriseConnectionParams,
+} from '../../types/enterpriseConnection';
+import { useClerkInstanceContext } from '../contexts';
+import { defineKeepPreviousDataFn } from '../query/keep-previous-data';
+import { useClerkQueryClient } from '../query/use-clerk-query-client';
+import { useClerkQuery } from '../query/useQuery';
+import { useOrganizationBase } from './base/useOrganizationBase';
+import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
+import { useOrganizationEnterpriseConnectionsCacheKeys } from './useOrganizationEnterpriseConnections.shared';
+
+export type UseOrganizationEnterpriseConnectionsParams = {
+ enabled?: boolean;
+ keepPreviousData?: boolean;
+ withOrganizationAccountLinking?: boolean;
+};
+
+export type UseOrganizationEnterpriseConnectionsReturn = {
+ data: EnterpriseConnectionResource[] | undefined;
+ error: Error | null;
+ isLoading: boolean;
+ isFetching: boolean;
+ createEnterpriseConnection: (
+ params: CreateOrganizationEnterpriseConnectionParams,
+ ) => Promise;
+ updateEnterpriseConnection: (
+ enterpriseConnectionId: string,
+ params: UpdateOrganizationEnterpriseConnectionParams,
+ ) => Promise;
+ deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise;
+ revalidate: () => Promise;
+};
+
+/**
+ * Enterprise connections for the active organization
+ *
+ * @internal
+ */
+function useOrganizationEnterpriseConnections(
+ params: UseOrganizationEnterpriseConnectionsParams = {},
+): UseOrganizationEnterpriseConnectionsReturn {
+ const { keepPreviousData = true, enabled = true, withOrganizationAccountLinking = false } = params;
+ const clerk = useClerkInstanceContext();
+ const organization = useOrganizationBase();
+ const [queryClient] = useClerkQueryClient();
+
+ const { queryKey, stableKey, authenticated } = useOrganizationEnterpriseConnectionsCacheKeys({
+ organizationId: organization?.id ?? null,
+ withOrganizationAccountLinking,
+ });
+
+ const queryEnabled = enabled && clerk.loaded && Boolean(organization);
+
+ useClearQueriesOnSignOut({
+ isSignedOut: organization === null,
+ authenticated,
+ stableKeys: stableKey,
+ });
+
+ const query = useClerkQuery({
+ queryKey,
+ queryFn: () => organization?.getEnterpriseConnections({ withOrganizationAccountLinking }),
+ enabled: queryEnabled,
+ placeholderData: defineKeepPreviousDataFn(keepPreviousData),
+ });
+
+ const revalidate = useCallback(
+ () => queryClient.invalidateQueries({ queryKey: [stableKey] }),
+ [queryClient, stableKey],
+ );
+
+ const createEnterpriseConnection = useCallback(
+ async (createParams: CreateOrganizationEnterpriseConnectionParams) => {
+ const created = await organization?.createEnterpriseConnection(createParams);
+ await revalidate();
+ return created;
+ },
+ [organization, revalidate],
+ );
+
+ const updateEnterpriseConnection = useCallback(
+ async (enterpriseConnectionId: string, updateParams: UpdateOrganizationEnterpriseConnectionParams) => {
+ const updated = await organization?.updateEnterpriseConnection(enterpriseConnectionId, updateParams);
+ await revalidate();
+ return updated;
+ },
+ [organization, revalidate],
+ );
+
+ const deleteEnterpriseConnection = useCallback(
+ async (enterpriseConnectionId: string) => {
+ const deleted = await organization?.deleteEnterpriseConnection(enterpriseConnectionId);
+ await revalidate();
+ return deleted;
+ },
+ [organization, revalidate],
+ );
+
+ return {
+ data: query.data,
+ error: query.error ?? null,
+ isLoading: query.isLoading,
+ isFetching: query.isFetching,
+ createEnterpriseConnection,
+ updateEnterpriseConnection,
+ deleteEnterpriseConnection,
+ revalidate,
+ };
+}
+
+export { useOrganizationEnterpriseConnections as __internal_useOrganizationEnterpriseConnections };
diff --git a/packages/shared/src/react/stable-keys.ts b/packages/shared/src/react/stable-keys.ts
index 91940a47b57..628b3640824 100644
--- a/packages/shared/src/react/stable-keys.ts
+++ b/packages/shared/src/react/stable-keys.ts
@@ -74,6 +74,8 @@ const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';
+const ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY = 'organizationEnterpriseConnections';
+const ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'organizationEnterpriseConnectionTestRuns';
export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
@@ -81,6 +83,8 @@ export const INTERNAL_STABLE_KEYS = {
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
+ ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY,
+ ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;
export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
diff --git a/packages/shared/src/types/enterpriseConnection.ts b/packages/shared/src/types/enterpriseConnection.ts
index c47641f2242..2a99a291081 100644
--- a/packages/shared/src/types/enterpriseConnection.ts
+++ b/packages/shared/src/types/enterpriseConnection.ts
@@ -98,7 +98,7 @@ export interface EnterpriseOAuthConfigResource {
updatedAt: Date | null;
}
-export type MeEnterpriseConnectionProvider =
+export type OrganizationEnterpriseConnectionProvider =
| 'saml_custom'
| 'saml_okta'
| 'saml_google'
@@ -107,7 +107,10 @@ export type MeEnterpriseConnectionProvider =
| 'oidc_github_enterprise'
| 'oidc_gitlab';
-export type MeEnterpriseConnectionSamlInput = {
+/** @deprecated Use `OrganizationEnterpriseConnectionProvider` instead. */
+export type MeEnterpriseConnectionProvider = OrganizationEnterpriseConnectionProvider;
+
+export type OrganizationEnterpriseConnectionSamlInput = {
idpEntityId?: string | null;
idpSsoUrl?: string | null;
idpCertificate?: string | null;
@@ -119,7 +122,10 @@ export type MeEnterpriseConnectionSamlInput = {
forceAuthn?: boolean | null;
};
-export type MeEnterpriseConnectionOidcInput = {
+/** @deprecated Use `OrganizationEnterpriseConnectionSamlInput` instead. */
+export type MeEnterpriseConnectionSamlInput = OrganizationEnterpriseConnectionSamlInput;
+
+export type OrganizationEnterpriseConnectionOidcInput = {
clientId?: string | null;
clientSecret?: string | null;
discoveryUrl?: string | null;
@@ -129,21 +135,30 @@ export type MeEnterpriseConnectionOidcInput = {
requiresPkce?: boolean | null;
};
-export type CreateMeEnterpriseConnectionParams = {
- provider: MeEnterpriseConnectionProvider;
+/** @deprecated Use `OrganizationEnterpriseConnectionOidcInput` instead. */
+export type MeEnterpriseConnectionOidcInput = OrganizationEnterpriseConnectionOidcInput;
+
+export type CreateOrganizationEnterpriseConnectionParams = {
+ provider: OrganizationEnterpriseConnectionProvider;
name: string;
organizationId?: string | null;
- saml?: MeEnterpriseConnectionSamlInput | null;
- oidc?: MeEnterpriseConnectionOidcInput | null;
+ saml?: OrganizationEnterpriseConnectionSamlInput | null;
+ oidc?: OrganizationEnterpriseConnectionOidcInput | null;
};
-export type UpdateMeEnterpriseConnectionParams = {
+/** @deprecated Use `CreateOrganizationEnterpriseConnectionParams` instead. */
+export type CreateMeEnterpriseConnectionParams = CreateOrganizationEnterpriseConnectionParams;
+
+export type UpdateOrganizationEnterpriseConnectionParams = {
name?: string | null;
active?: boolean | null;
syncUserAttributes?: boolean | null;
disableAdditionalIdentifications?: boolean | null;
organizationId?: string | null;
customAttributes?: Record | null;
- saml?: MeEnterpriseConnectionSamlInput | null;
- oidc?: MeEnterpriseConnectionOidcInput | null;
+ saml?: OrganizationEnterpriseConnectionSamlInput | null;
+ oidc?: OrganizationEnterpriseConnectionOidcInput | null;
};
+
+/** @deprecated Use `UpdateOrganizationEnterpriseConnectionParams` instead. */
+export type UpdateMeEnterpriseConnectionParams = UpdateOrganizationEnterpriseConnectionParams;
diff --git a/packages/shared/src/types/organization.ts b/packages/shared/src/types/organization.ts
index 1bd4f266317..81d123bcbaf 100644
--- a/packages/shared/src/types/organization.ts
+++ b/packages/shared/src/types/organization.ts
@@ -1,4 +1,15 @@
import type { BillingPayerMethods } from './billing';
+import type { DeletedObjectResource } from './deletedObject';
+import type {
+ CreateOrganizationEnterpriseConnectionParams,
+ EnterpriseConnectionResource,
+ UpdateOrganizationEnterpriseConnectionParams,
+} from './enterpriseConnection';
+import type {
+ EnterpriseConnectionTestRunInitResource,
+ EnterpriseConnectionTestRunResource,
+ GetEnterpriseConnectionTestRunsParams,
+} from './enterpriseConnectionTestRun';
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from './organizationDomain';
import type { OrganizationInvitationResource, OrganizationInvitationStatus } from './organizationInvitation';
import type { OrganizationCustomRoleKey, OrganizationMembershipResource } from './organizationMembership';
@@ -7,6 +18,7 @@ import type { ClerkPaginatedResponse, ClerkPaginationParams } from './pagination
import type { ClerkResource } from './resource';
import type { RoleResource } from './role';
import type { OrganizationJSONSnapshot } from './snapshots';
+import type { GetEnterpriseConnectionsParams } from './user';
declare global {
/**
@@ -64,6 +76,22 @@ export interface OrganizationResource extends ClerkResource, BillingPayerMethods
removeMember: (userId: string) => Promise;
createDomain: (domainName: string) => Promise;
getDomain: ({ domainId }: { domainId: string }) => Promise;
+ getEnterpriseConnections: (params?: GetEnterpriseConnectionsParams) => Promise;
+ createEnterpriseConnection: (
+ params: CreateOrganizationEnterpriseConnectionParams,
+ ) => Promise;
+ updateEnterpriseConnection: (
+ enterpriseConnectionId: string,
+ params: UpdateOrganizationEnterpriseConnectionParams,
+ ) => Promise;
+ deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise;
+ createEnterpriseConnectionTestRun: (
+ enterpriseConnectionId: string,
+ ) => Promise;
+ getEnterpriseConnectionTestRuns: (
+ enterpriseConnectionId: string,
+ params?: GetEnterpriseConnectionTestRunsParams,
+ ) => Promise>;
destroy: () => Promise;
setLogo: (params: SetOrganizationLogoParams) => Promise;
__internal_toSnapshot: () => OrganizationJSONSnapshot;
diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts
index 5f6c392cc93..cdd8003eaab 100644
--- a/packages/shared/src/types/user.ts
+++ b/packages/shared/src/types/user.ts
@@ -4,9 +4,9 @@ import type { DeletedObjectResource } from './deletedObject';
import type { EmailAddressResource } from './emailAddress';
import type { EnterpriseAccountResource } from './enterpriseAccount';
import type {
- CreateMeEnterpriseConnectionParams,
+ CreateOrganizationEnterpriseConnectionParams,
EnterpriseConnectionResource,
- UpdateMeEnterpriseConnectionParams,
+ UpdateOrganizationEnterpriseConnectionParams,
} from './enterpriseConnection';
import type {
EnterpriseConnectionTestRunInitResource,
@@ -130,10 +130,12 @@ export interface UserResource extends ClerkResource, BillingPayerMethods {
getOrganizationCreationDefaults: () => Promise;
leaveOrganization: (organizationId: string) => Promise;
getEnterpriseConnections: (params?: GetEnterpriseConnectionsParams) => Promise;
- createEnterpriseConnection: (params: CreateMeEnterpriseConnectionParams) => Promise;
+ createEnterpriseConnection: (
+ params: CreateOrganizationEnterpriseConnectionParams,
+ ) => Promise;
updateEnterpriseConnection: (
enterpriseConnectionId: string,
- params: UpdateMeEnterpriseConnectionParams,
+ params: UpdateOrganizationEnterpriseConnectionParams,
) => Promise;
deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise;
createEnterpriseConnectionTestRun: (
diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
index 678f1212793..5959eff4b04 100644
--- a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
+++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
@@ -1,6 +1,6 @@
import {
- __internal_useEnterpriseConnectionTestRuns,
- __internal_useUserEnterpriseConnections,
+ __internal_useOrganizationEnterpriseConnections,
+ __internal_useOrganizationEnterpriseConnectionTestRuns,
useSession,
} from '@clerk/shared/react';
import type { ConfigureSSOProps, EnterpriseConnectionResource } from '@clerk/shared/types';
@@ -72,7 +72,7 @@ export const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObjec
createEnterpriseConnection,
updateEnterpriseConnection,
deleteEnterpriseConnection,
- } = __internal_useUserEnterpriseConnections({ enabled: true });
+ } = __internal_useOrganizationEnterpriseConnections({ enabled: true });
// Currently FAPI only supports one enterprise connection per user
const enterpriseConnection = enterpriseConnections?.[0];
@@ -238,7 +238,7 @@ const ResetCardErrorOnStepChange = (): null => {
const useHasSuccessfulTestRun = (
enterpriseConnection: EnterpriseConnectionResource | undefined,
): { hasSuccessfulTestRun: boolean; isLoading: boolean } => {
- const { data: successfulTestRuns, isLoading } = __internal_useEnterpriseConnectionTestRuns({
+ const { data: successfulTestRuns, isLoading } = __internal_useOrganizationEnterpriseConnectionTestRuns({
enterpriseConnectionId: enterpriseConnection?.id ?? null,
params: { initialPage: 1, pageSize: 1, status: ['success'] },
});
diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx
index 51c217f17a3..6bc3b5d9277 100644
--- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx
+++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx
@@ -1,4 +1,4 @@
-import type { UseUserEnterpriseConnectionsReturn } from '@clerk/shared/react/index';
+import type { UseOrganizationEnterpriseConnectionsReturn } from '@clerk/shared/react/index';
import { useSession, useUser } from '@clerk/shared/react/index';
import type {
EmailAddressResource,
@@ -44,11 +44,11 @@ export interface ConfigureSSOData {
/**
* Updates an existing enterprise connection
*/
- updateEnterpriseConnection: UseUserEnterpriseConnectionsReturn['updateEnterpriseConnection'];
+ updateEnterpriseConnection: UseOrganizationEnterpriseConnectionsReturn['updateEnterpriseConnection'];
/**
* Deletes an enterprise connection
*/
- deleteEnterpriseConnection: UseUserEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
+ deleteEnterpriseConnection: UseOrganizationEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
/**
* Determines if the user's domain is already wired to an enterprise connection that
* doesn't belong to the org they're currently configuring
@@ -60,9 +60,9 @@ interface ConfigureSSOProviderProps {
enterpriseConnection: EnterpriseConnectionResource | undefined;
hasSuccessfulTestRun: boolean;
contentRef: React.RefObject;
- createEnterpriseConnection: UseUserEnterpriseConnectionsReturn['createEnterpriseConnection'];
- updateEnterpriseConnection: UseUserEnterpriseConnectionsReturn['updateEnterpriseConnection'];
- deleteEnterpriseConnection: UseUserEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
+ createEnterpriseConnection: UseOrganizationEnterpriseConnectionsReturn['createEnterpriseConnection'];
+ updateEnterpriseConnection: UseOrganizationEnterpriseConnectionsReturn['updateEnterpriseConnection'];
+ deleteEnterpriseConnection: UseOrganizationEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
}
const ConfigureSSOContext = React.createContext(null);
@@ -90,7 +90,6 @@ export const ConfigureSSOProvider = ({
const createEnterpriseConnection = useCallback(
async (provider: ProviderType, primaryEmailAddress?: EmailAddressResource): Promise => {
const emailDomain = primaryEmailAddress?.emailAddress.split('@')[1];
- const organizationId = session?.lastActiveOrganizationId ?? null;
if (!emailDomain) {
return;
@@ -99,16 +98,17 @@ export const ConfigureSSOProvider = ({
card.setLoading();
try {
+ // The organization is inferred from the URL path on the org-scoped
+ // endpoint, so we don't need to pass `organizationId` in the body.
await createEnterpriseConnectionApi({
provider,
name: emailDomain,
- organizationId,
});
} finally {
card.setIdle();
}
},
- [card, session, createEnterpriseConnectionApi],
+ [card, createEnterpriseConnectionApi],
);
const value = React.useMemo(
diff --git a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx
index 55f7d53cf28..88215e22798 100644
--- a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx
+++ b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx
@@ -20,7 +20,7 @@ describe('ConfigureSSO', () => {
});
});
- fixtures.clerk.user?.getEnterpriseConnections.mockResolvedValue([]);
+ fixtures.clerk.organization?.getEnterpriseConnections.mockResolvedValue([]);
const { findByText, queryByText } = render(, { wrapper });
@@ -40,7 +40,7 @@ describe('ConfigureSSO', () => {
});
});
- fixtures.clerk.user?.getEnterpriseConnections.mockResolvedValue([]);
+ fixtures.clerk.organization?.getEnterpriseConnections.mockResolvedValue([]);
const { findByText, queryByText } = render(, { wrapper });
@@ -59,7 +59,7 @@ describe('ConfigureSSO', () => {
f.withUser({ email_addresses: ['test@clerk.com'] });
});
- fixtures.clerk.user?.getEnterpriseConnections.mockResolvedValue([]);
+ fixtures.clerk.organization?.getEnterpriseConnections.mockResolvedValue([]);
const { findByText, queryByText } = render(, { wrapper });
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx
index f0d842a4b1e..65532d68738 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx
@@ -1,6 +1,6 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import { useReverification } from '@clerk/shared/react';
-import type { FieldId, UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types';
+import type { FieldId, UpdateOrganizationEnterpriseConnectionParams } from '@clerk/shared/types';
import React, { type JSX } from 'react';
import {
@@ -651,7 +651,7 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => {
const updateConnection = useReverification(
React.useCallback(
- async (params: UpdateMeEnterpriseConnectionParams) => {
+ async (params: UpdateOrganizationEnterpriseConnectionParams) => {
if (!enterpriseConnection) {
throw new Error('Enterprise connection required');
}
@@ -711,7 +711,7 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => {
if (mode === 'metadataUrl') {
await updateConnection({ saml: { idpMetadataUrl: trimmedMetadataUrl } });
} else {
- const samlPayload: NonNullable = {
+ const samlPayload: NonNullable = {
idpSsoUrl: trimmedSignOnUrl,
idpEntityId: trimmedIssuer,
};
diff --git a/packages/ui/src/components/ConfigureSSO/steps/TestConfigurationStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/TestConfigurationStep.tsx
index 7a7ce7d2f9b..ec48c9447c3 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/TestConfigurationStep.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/TestConfigurationStep.tsx
@@ -1,4 +1,4 @@
-import { __internal_useEnterpriseConnectionTestRuns, useUser } from '@clerk/shared/react/index';
+import { __internal_useOrganizationEnterpriseConnectionTestRuns, useOrganization } from '@clerk/shared/react/index';
import type { EnterpriseConnectionTestRunResource } from '@clerk/shared/types';
import type { ReactNode } from 'react';
import { useState } from 'react';
@@ -59,7 +59,7 @@ export const TestConfigurationStep = (): JSX.Element => {
isFetching: areTestRunsFetching,
isPolling,
revalidate: revalidateTestRuns,
- } = __internal_useEnterpriseConnectionTestRuns({
+ } = __internal_useOrganizationEnterpriseConnectionTestRuns({
enterpriseConnectionId: enterpriseConnection?.id ?? null,
params: { initialPage: currentPage, pageSize: TEST_RUNS_PAGE_SIZE },
});
@@ -197,13 +197,13 @@ const ContinueTestSsoStepButton = ({
enterpriseConnectionId,
onContinue,
}: ContinueTestSsoStepButtonProps): JSX.Element => {
- const { user } = useUser();
+ const { organization } = useOrganization();
const { t } = useLocalizations();
const card = useCardState();
const [isValidating, setIsValidating] = useState(false);
const handleContinue = async () => {
- if (!user || !enterpriseConnectionId) {
+ if (!organization || !enterpriseConnectionId) {
return;
}
@@ -211,7 +211,7 @@ const ContinueTestSsoStepButton = ({
card.setError(undefined);
try {
- const result = await user.getEnterpriseConnectionTestRuns(enterpriseConnectionId, {
+ const result = await organization.getEnterpriseConnectionTestRuns(enterpriseConnectionId, {
initialPage: 1,
pageSize: 1,
status: ['success'],
@@ -717,20 +717,20 @@ type OpenTestUrlButtonProps = {
};
const OpenTestUrlButton = ({ onTestRunCreated }: OpenTestUrlButtonProps): JSX.Element => {
- const { user } = useUser();
+ const { organization } = useOrganization();
const card = useCardState();
const { enterpriseConnection } = useConfigureSSO();
const [isCreatingTestRun, setIsCreatingTestRun] = useState(false);
const openTestRun = () => {
- if (!user || !enterpriseConnection) {
+ if (!organization || !enterpriseConnection) {
return;
}
setIsCreatingTestRun(true);
- user
+ organization
.createEnterpriseConnectionTestRun(enterpriseConnection.id)
.then(({ url }) => {
onTestRunCreated?.(url);