From c6348b1e3dd3cc67a881d5b5eef35df88069142a Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Mon, 13 Jul 2026 23:00:47 -0500 Subject: [PATCH 1/3] fix(backend): Require name and domains when creating enterprise connections Stacked on #9153 (required provider field). Aligns the remaining CreateEnterpriseConnectionParams gaps with the Backend API contract, which validates name and domains (min 1) as required. Deprecates syncUserAttributes on create and provider on update, since the Backend API ignores both. Co-Authored-By: Claude Fable 5 --- .changeset/violet-planes-repeat.md | 9 +++++++++ .../src/api/endpoints/EnterpriseConnectionApi.ts | 16 +++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 .changeset/violet-planes-repeat.md diff --git a/.changeset/violet-planes-repeat.md b/.changeset/violet-planes-repeat.md new file mode 100644 index 00000000000..4aee6753981 --- /dev/null +++ b/.changeset/violet-planes-repeat.md @@ -0,0 +1,9 @@ +--- +'@clerk/backend': patch +--- + +Align `CreateEnterpriseConnectionParams` and `UpdateEnterpriseConnectionParams` with the Backend API contract: + +- `name` and `domains` are now required on `CreateEnterpriseConnectionParams`. The Backend API already rejected requests missing either of them, so calls that omitted these fields failed at runtime; the types now surface this at compile time. +- Deprecated `syncUserAttributes` on `CreateEnterpriseConnectionParams`. The Backend API ignores this parameter on create; use `updateEnterpriseConnection()` to set it. +- Deprecated `provider` on `UpdateEnterpriseConnectionParams`. The Backend API ignores this parameter on update; the provider cannot be changed after creation. diff --git a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts index 3a4a246fce2..7e3fdacab91 100644 --- a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts +++ b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts @@ -74,14 +74,17 @@ export interface EnterpriseConnectionSamlParams { /** @generateWithEmptyComment */ export type CreateEnterpriseConnectionParams = { /** The name of the enterprise connection. */ - name?: string; - /** The [Verified Domains](https://clerk.com/docs/guides/organizations/add-members/verified-domains) of the enterprise connection. */ - domains?: string[]; + name: string; + /** The [Verified Domains](https://clerk.com/docs/guides/organizations/add-members/verified-domains) of the enterprise connection. Must contain at least one domain. */ + domains: string[]; /** The organization ID of the enterprise connection. */ organizationId?: string; /** Whether the enterprise connection should be active. */ active?: boolean; - /** Whether the enterprise connection should sync user attributes between the IdP and Clerk. */ + /** + * Whether the enterprise connection should sync user attributes between the IdP and Clerk. + * @deprecated The Backend API does not support this parameter on create and ignores it. Use `updateEnterpriseConnection()` to set it. + */ syncUserAttributes?: boolean; /** The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. */ provider: OrganizationEnterpriseConnectionProvider; @@ -103,7 +106,10 @@ export type UpdateEnterpriseConnectionParams = { active?: boolean; /** Whether the enterprise connection should sync user attributes between the IdP and Clerk. */ syncUserAttributes?: boolean; - /** The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. */ + /** + * The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. + * @deprecated The Backend API does not support this parameter on update and ignores it. The provider cannot be changed after creation. + */ provider?: string; /** Configuration for if the enterprise connection uses OAuth (OIDC). */ oidc?: EnterpriseConnectionOidcParams; From 7b6b5c002591bfeeb6629ab5c38c54c016acc5a0 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Mon, 13 Jul 2026 23:12:34 -0500 Subject: [PATCH 2/3] fix(backend): Add missing optional enterprise connection params Adds the optional create/update params the Backend API supports but the SDK types omitted: allowOrganizationAccountLinking, customAttributes, authenticatable, disableJitProvisioning, disableAdditionalIdentifications (update only), and saml.loginHint. Verified against BAPI's CreateParams/UpdateParams/SAMLParams in clerk_go. Co-Authored-By: Claude Fable 5 --- .changeset/tidy-donuts-attend.md | 5 + .../__tests__/EnterpriseConnectionApi.test.ts | 94 +++++++++++++++++++ .../api/endpoints/EnterpriseConnectionApi.ts | 42 +++++++++ 3 files changed, 141 insertions(+) create mode 100644 .changeset/tidy-donuts-attend.md diff --git a/.changeset/tidy-donuts-attend.md b/.changeset/tidy-donuts-attend.md new file mode 100644 index 00000000000..82cccf3a5e8 --- /dev/null +++ b/.changeset/tidy-donuts-attend.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add the remaining optional enterprise connection parameters supported by the Backend API. `CreateEnterpriseConnectionParams` and `UpdateEnterpriseConnectionParams` now accept `allowOrganizationAccountLinking`, `customAttributes`, `authenticatable`, and `disableJitProvisioning` (update also accepts `disableAdditionalIdentifications`), and SAML params accept `loginHint` for configuring the `login_hint` sent to the IdP. diff --git a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts index d9675e0080c..90ce3e6ec88 100644 --- a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts +++ b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts @@ -129,6 +129,59 @@ describe('EnterpriseConnectionAPI', () => { expectTypeOf<'saml_bogus'>().not.toExtend(); expectTypeOf<'saml_okta'>().toExtend(); }); + + it('sends provisioning, custom attribute, and login hint params in snake_case', async () => { + server.use( + http.post( + 'https://api.clerk.test/v1/enterprise_connections', + validateHeaders(async ({ request }) => { + const body = (await request.json()) as Record; + + expect(body.allow_organization_account_linking).toBe(true); + expect(body.authenticatable).toBe(false); + expect(body.disable_jit_provisioning).toBe(true); + expect(body.custom_attributes).toEqual([ + { + name: 'Employee Number', + key: 'employee_number', + sso_path: 'user.employeeNumber', + multi_valued: false, + }, + ]); + expect((body.saml as Record).login_hint).toEqual({ + mode: 'custom_attribute', + source: 'employee_number', + }); + + return HttpResponse.json(mockEnterpriseConnectionResponse); + }), + ), + ); + + await apiClient.enterpriseConnections.createEnterpriseConnection({ + name: 'Clerk', + domains: ['clerk.dev'], + provider: 'saml_custom', + allowOrganizationAccountLinking: true, + authenticatable: false, + disableJitProvisioning: true, + customAttributes: [ + { + name: 'Employee Number', + key: 'employee_number', + ssoPath: 'user.employeeNumber', + multiValued: false, + }, + ], + saml: { + idpEntityId: 'xxx', + loginHint: { + mode: 'custom_attribute', + source: 'employee_number', + }, + }, + }); + }); }); describe('updateEnterpriseConnection', () => { @@ -157,6 +210,47 @@ describe('EnterpriseConnectionAPI', () => { }, }); }); + + it('sends provisioning and custom attribute params in snake_case', async () => { + server.use( + http.patch( + 'https://api.clerk.test/v1/enterprise_connections/entconn_123', + validateHeaders(async ({ request }) => { + const body = (await request.json()) as Record; + + expect(body.sync_user_attributes).toBe(true); + expect(body.disable_additional_identifications).toBe(true); + expect(body.allow_organization_account_linking).toBe(false); + expect(body.authenticatable).toBe(true); + expect(body.disable_jit_provisioning).toBe(false); + expect(body.custom_attributes).toEqual([ + { + name: 'Department', + key: 'department', + scim_path: 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department', + }, + ]); + + return HttpResponse.json(mockEnterpriseConnectionResponse); + }), + ), + ); + + await apiClient.enterpriseConnections.updateEnterpriseConnection('entconn_123', { + syncUserAttributes: true, + disableAdditionalIdentifications: true, + allowOrganizationAccountLinking: false, + authenticatable: true, + disableJitProvisioning: false, + customAttributes: [ + { + name: 'Department', + key: 'department', + scimPath: 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department', + }, + ], + }); + }); }); describe('getEnterpriseConnectionList', () => { diff --git a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts index 7e3fdacab91..467b164e29e 100644 --- a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts +++ b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts @@ -49,6 +49,28 @@ export interface EnterpriseConnectionSamlAttributeMappingParams { lastName?: string | null; } +/** @inline */ +export interface EnterpriseConnectionSamlLoginHintParams { + /** How the SAML connection emits the `login_hint` sent to the IdP: `'email_address'` sends the typed identifier, `'custom_attribute'` sends the value stored at the user `publicMetadata` key named by `source`, and `'off'` omits the `login_hint`. */ + mode: 'email_address' | 'custom_attribute' | 'off'; + /** The user `publicMetadata` key to read the `login_hint` value from. Required when `mode` is `'custom_attribute'`, and must be omitted otherwise. */ + source?: string; +} + +/** @inline */ +export interface EnterpriseConnectionCustomAttributeParams { + /** The display name of the custom attribute. */ + name: string; + /** The key to store the custom attribute under. */ + key: string; + /** The SSO (SAML or OIDC) attribute path to read the value from. */ + ssoPath?: string; + /** The SCIM attribute path to read the value from. */ + scimPath?: string; + /** Whether the custom attribute holds multiple values. */ + multiValued?: boolean; +} + /** @inline */ export interface EnterpriseConnectionSamlParams { /** Whether the SAML connection allows Identity Provider (IdP) initiated flows. */ @@ -69,6 +91,8 @@ export interface EnterpriseConnectionSamlParams { idpMetadataUrl?: string; /** The IdP Single-Sign On URL for the SAML connection. */ idpSsoUrl?: string; + /** Configuration for the `login_hint` the SAML connection sends to the IdP. */ + loginHint?: EnterpriseConnectionSamlLoginHintParams; } /** @generateWithEmptyComment */ @@ -88,6 +112,14 @@ export type CreateEnterpriseConnectionParams = { syncUserAttributes?: boolean; /** The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. */ provider: OrganizationEnterpriseConnectionProvider; + /** Whether existing users who are members of the organization can link their account to their enterprise identity. If `false`, only sign-up flows apply. */ + allowOrganizationAccountLinking?: boolean; + /** The custom attribute mappings for the enterprise connection. */ + customAttributes?: EnterpriseConnectionCustomAttributeParams[]; + /** Whether the enterprise connection can be used for sign-in and sign-up. Requires the authenticatable enterprise connections feature to be enabled for the instance. */ + authenticatable?: boolean; + /** Whether Just-in-Time (JIT) provisioning of users is disabled for the enterprise connection. */ + disableJitProvisioning?: boolean; /** Configuration for if the enterprise connection uses OAuth (OIDC). */ oidc?: EnterpriseConnectionOidcParams; /** Configuration for if the enterprise connection uses SAML. */ @@ -106,6 +138,16 @@ export type UpdateEnterpriseConnectionParams = { active?: boolean; /** Whether the enterprise connection should sync user attributes between the IdP and Clerk. */ syncUserAttributes?: boolean; + /** Whether additional identifications are disabled for the enterprise connection. */ + disableAdditionalIdentifications?: boolean; + /** Whether existing users who are members of the organization can link their account to their enterprise identity. If `false`, only sign-up flows apply. */ + allowOrganizationAccountLinking?: boolean; + /** The custom attribute mappings for the enterprise connection. */ + customAttributes?: EnterpriseConnectionCustomAttributeParams[]; + /** Whether the enterprise connection can be used for sign-in and sign-up. Requires the authenticatable enterprise connections feature to be enabled for the instance. */ + authenticatable?: boolean; + /** Whether Just-in-Time (JIT) provisioning of users is disabled for the enterprise connection. */ + disableJitProvisioning?: boolean; /** * The identity provider (IdP) of the enterprise connection. For example, `'saml_custom'` or `'oidc_custom'`. * @deprecated The Backend API does not support this parameter on update and ignores it. The provider cannot be changed after creation. From 1508711f22743a0d26cf39f2900a70432f3af8f9 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Mon, 13 Jul 2026 23:41:33 -0500 Subject: [PATCH 3/3] fix(backend): Enforce login hint source/mode pairing at the type level Codex review: BAPI requires login_hint.source exactly when mode is custom_attribute and rejects it otherwise; model as a discriminated union. Also lock name/domains as required with type-level tests. Co-Authored-By: Claude Fable 5 --- .../__tests__/EnterpriseConnectionApi.test.ts | 22 ++++++++++++++++++- .../api/endpoints/EnterpriseConnectionApi.ts | 19 +++++++++++----- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts index 90ce3e6ec88..f46f0281e8f 100644 --- a/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts +++ b/packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts @@ -2,7 +2,10 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, expectTypeOf, it } from 'vitest'; import { server, validateHeaders } from '../../mock-server'; -import type { CreateEnterpriseConnectionParams } from '../endpoints/EnterpriseConnectionApi'; +import type { + CreateEnterpriseConnectionParams, + EnterpriseConnectionSamlLoginHintParams, +} from '../endpoints/EnterpriseConnectionApi'; import { createBackendApiClient } from '../factory'; describe('EnterpriseConnectionAPI', () => { @@ -130,6 +133,23 @@ describe('EnterpriseConnectionAPI', () => { expectTypeOf<'saml_okta'>().toExtend(); }); + it('requires name and domains at the type level', () => { + expectTypeOf<{ + name: string; + domains: string[]; + provider: 'saml_custom'; + }>().toExtend(); + expectTypeOf<{ domains: string[]; provider: 'saml_custom' }>().not.toExtend(); + expectTypeOf<{ name: string; provider: 'saml_custom' }>().not.toExtend(); + }); + + it('requires a login hint source exactly when mode is custom_attribute at the type level', () => { + expectTypeOf<{ mode: 'custom_attribute'; source: string }>().toExtend(); + expectTypeOf<{ mode: 'email_address' }>().toExtend(); + expectTypeOf<{ mode: 'custom_attribute' }>().not.toExtend(); + expectTypeOf<{ mode: 'off'; source: string }>().not.toExtend(); + }); + it('sends provisioning, custom attribute, and login hint params in snake_case', async () => { server.use( http.post( diff --git a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts index 467b164e29e..644fc0c88d8 100644 --- a/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts +++ b/packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts @@ -50,12 +50,19 @@ export interface EnterpriseConnectionSamlAttributeMappingParams { } /** @inline */ -export interface EnterpriseConnectionSamlLoginHintParams { - /** How the SAML connection emits the `login_hint` sent to the IdP: `'email_address'` sends the typed identifier, `'custom_attribute'` sends the value stored at the user `publicMetadata` key named by `source`, and `'off'` omits the `login_hint`. */ - mode: 'email_address' | 'custom_attribute' | 'off'; - /** The user `publicMetadata` key to read the `login_hint` value from. Required when `mode` is `'custom_attribute'`, and must be omitted otherwise. */ - source?: string; -} +export type EnterpriseConnectionSamlLoginHintParams = + | { + /** Sends the value stored at the user `publicMetadata` key named by `source` as the `login_hint`. */ + mode: 'custom_attribute'; + /** The user `publicMetadata` key to read the `login_hint` value from. */ + source: string; + } + | { + /** How the SAML connection emits the `login_hint` sent to the IdP: `'email_address'` sends the typed identifier and `'off'` omits the `login_hint`. */ + mode: 'email_address' | 'off'; + /** Only supported when `mode` is `'custom_attribute'`. */ + source?: never; + }; /** @inline */ export interface EnterpriseConnectionCustomAttributeParams {