Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/major-poets-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@clerk/backend': minor
---

Align the `EnterpriseConnection` response resource with what the Backend API actually returns:

- `EnterpriseConnection` now exposes `provider`, `logoPublicUrl`, `allowOrganizationAccountLinking`, `authenticatable`, `disableJitProvisioning`, and `customAttributes`.
- `EnterpriseConnectionSamlConnection` now exposes `active`, `forceAuthn`, and `loginHint`.
- `EnterpriseConnectionOauthConfig` now exposes `providerKey`, `authUrl`, `tokenUrl`, `userInfoUrl`, and `requiresPkce`.
- Deprecated properties the Backend API never returns, which were always `undefined` despite their declared types: `allowSubdomains` on `EnterpriseConnection` (use `samlConnection.allowSubdomains`), and `idpMetadata` and `syncUserAttributes` on `EnterpriseConnectionSamlConnection` (use the top-level `syncUserAttributes`).
- `organizationId` is now normalized to `null` when the Backend API omits it, matching its declared `string | null` type. Properties backed by optional API fields (for example `oauthConfig.clientId` and the SAML IdP fields) are now typed as possibly `undefined` to match runtime behavior.
55 changes: 51 additions & 4 deletions packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,26 @@ describe('EnterpriseConnectionAPI', () => {
object: 'enterprise_connection',
id: 'entconn_123',
name: 'Clerk',
provider: 'saml_custom',
logo_public_url: 'https://img.example.com/connection-logo.png',
domains: ['clerk.dev'],
organization_id: null,
created_at: 1672531200000,
updated_at: 1672531200000,
active: true,
sync_user_attributes: false,
allow_subdomains: false,
disable_additional_identifications: false,
allow_organization_account_linking: true,
authenticatable: true,
disable_jit_provisioning: false,
custom_attributes: [
{
name: 'Employee Number',
key: 'employee_number',
sso_path: 'user.employeeNumber',
scim_path: '',
multi_valued: false,
},
],
saml_connection: {
id: 'samlc_1',
name: 'Acme SAML',
Expand All @@ -35,20 +47,29 @@ describe('EnterpriseConnectionAPI', () => {
idp_certificate_issued_at: 1672531200000,
idp_certificate_expires_at: 1704067200000,
idp_metadata_url: 'https://idp.example.com/metadata',
idp_metadata: '<xml/>',
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',
sync_user_attributes: true,
active: true,
allow_subdomains: true,
allow_idp_initiated: false,
force_authn: false,
login_hint: {
mode: 'custom_attribute',
source: 'employee_number',
},
},
oauth_config: {
id: 'eaoc_1',
provider_key: 'custom_oidc',
name: 'Acme OIDC',
client_id: 'client_abc',
discovery_url: 'https://oauth.example.com/.well-known/openid-configuration',
auth_url: 'https://oauth.example.com/authorize',
token_url: 'https://oauth.example.com/token',
user_info_url: 'https://oauth.example.com/userinfo',
logo_public_url: 'https://img.example.com/logo.png',
requires_pkce: false,
created_at: 1672531200000,
updated_at: 1672531200000,
},
Expand Down Expand Up @@ -326,17 +347,43 @@ describe('EnterpriseConnectionAPI', () => {

expect(response.id).toBe('entconn_123');
expect(response.name).toBe('Clerk');
expect(response.provider).toBe('saml_custom');
expect(response.logoPublicUrl).toBe('https://img.example.com/connection-logo.png');
expect(response.domains).toEqual(['clerk.dev']);
expect(response.active).toBe(true);
// organization_id is omitted by the Backend API when unset and normalized to null
expect(response.organizationId).toBeNull();
expect(response.allowOrganizationAccountLinking).toBe(true);
expect(response.authenticatable).toBe(true);
expect(response.disableJitProvisioning).toBe(false);
expect(response.customAttributes).toEqual([
{
name: 'Employee Number',
key: 'employee_number',
ssoPath: 'user.employeeNumber',
scimPath: '',
multiValued: false,
},
]);
expect(response.samlConnection).not.toBeNull();
expect(response.samlConnection?.id).toBe('samlc_1');
expect(response.samlConnection?.idpEntityId).toBe('https://idp.example.com');
expect(response.samlConnection?.idpCertificateIssuedAt).toBe(1672531200000);
expect(response.samlConnection?.idpCertificateExpiresAt).toBe(1704067200000);
expect(response.samlConnection?.active).toBe(true);
expect(response.samlConnection?.forceAuthn).toBe(false);
expect(response.samlConnection?.loginHint).toEqual({
mode: 'custom_attribute',
source: 'employee_number',
});
expect(response.oauthConfig).not.toBeNull();
expect(response.oauthConfig?.providerKey).toBe('custom_oidc');
expect(response.oauthConfig?.clientId).toBe('client_abc');
expect(response.oauthConfig?.discoveryUrl).toBe('https://oauth.example.com/.well-known/openid-configuration');
expect(response.oauthConfig?.authUrl).toBe('https://oauth.example.com/authorize');
expect(response.oauthConfig?.tokenUrl).toBe('https://oauth.example.com/token');
expect(response.oauthConfig?.userInfoUrl).toBe('https://oauth.example.com/userinfo');
expect(response.oauthConfig?.requiresPkce).toBe(false);
Comment on lines +350 to +386

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover omitted optional response fields.

This fixture populates every newly optional SAML/OAuth field and custom_attributes. Add a case omitting them and assert the resource exposes undefined; this locks in the intended serializer-omission behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/api/__tests__/EnterpriseConnectionApi.test.ts` around
lines 350 - 386, Add a separate test case in the EnterpriseConnectionApi
response tests using a fixture that omits the optional SAML/OAuth fields and
custom_attributes. Assert the normalized resource exposes undefined for each
omitted optional property, including the relevant fields on samlConnection and
oauthConfig, to preserve serializer-omission behavior.

Source: Coding guidelines

});
});

Expand Down
153 changes: 135 additions & 18 deletions packages/backend/src/api/resources/EnterpriseConnection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
import type {
EnterpriseConnectionCustomAttributeJSON,
EnterpriseConnectionJSON,
EnterpriseConnectionOauthConfigJSON,
EnterpriseConnectionSamlConnectionJSON,
EnterpriseConnectionSamlConnectionLoginHintJSON,
} from './JSON';

/**
* The `login_hint` configuration included on a Backend API {@link EnterpriseConnectionSamlConnection} response.
*/
export class EnterpriseConnectionSamlConnectionLoginHint {
constructor(
/** 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`. */
readonly mode: 'email_address' | 'custom_attribute' | 'off',
/** The user `publicMetadata` key the `login_hint` value is read from. Only set when `mode` is `'custom_attribute'`. */
readonly source?: string,
) {}

static fromJSON(data: EnterpriseConnectionSamlConnectionLoginHintJSON): EnterpriseConnectionSamlConnectionLoginHint {
return new EnterpriseConnectionSamlConnectionLoginHint(data.mode, data.source);
}
}

/**
* A custom attribute mapping included on a Backend API {@link EnterpriseConnection} response.
*/
export class EnterpriseConnectionCustomAttribute {
constructor(
/** The display name of the custom attribute. */
readonly name: string,
/** The key the custom attribute is stored under. */
readonly key: string,
/** The SSO (SAML or OIDC) attribute path the value is read from. */
readonly ssoPath: string,
/** The SCIM attribute path the value is read from. */
readonly scimPath: string,
/** Whether the custom attribute holds multiple values. */
readonly multiValued: boolean,
) {}

static fromJSON(data: EnterpriseConnectionCustomAttributeJSON): EnterpriseConnectionCustomAttribute {
return new EnterpriseConnectionCustomAttribute(
data.name,
data.key,
data.sso_path,
data.scim_path,
data.multi_valued,
);
}
}

/**
* The Backend `EnterpriseConnectionSamlConnection` object holds information about a SAML enterprise connection for an instance or organization.
*/
Expand All @@ -14,31 +60,43 @@ export class EnterpriseConnectionSamlConnection {
/** The name to use as a label for the connection. */
readonly name: string,
/** The Entity ID as provided by the Identity Provider (IdP). */
readonly idpEntityId: string,
readonly idpEntityId: string | undefined,
/** The Single-Sign On URL as provided by the Identity Provider (IdP). */
readonly idpSsoUrl: string,
readonly idpSsoUrl: string | undefined,
/** The X.509 certificate as provided by the Identity Provider (IdP). */
readonly idpCertificate: string,
readonly idpCertificate: string | undefined,
/** The Unix timestamp when the Identity Provider (IdP) certificate was issued. */
readonly idpCertificateIssuedAt: number,
readonly idpCertificateIssuedAt: number | undefined,
/** The Unix timestamp when the Identity Provider (IdP) certificate expires. */
readonly idpCertificateExpiresAt: number,
readonly idpCertificateExpiresAt: number | undefined,
/** The URL which serves the Identity Provider (IdP) metadata. */
readonly idpMetadataUrl: string,
/** The XML content of the Identity Provider (IdP) metadata file. */
readonly idpMetadata: string,
readonly idpMetadataUrl: string | undefined,
/**
* The XML content of the Identity Provider (IdP) metadata file.
* @deprecated The Backend API does not return this field, so it is always `undefined`.
*/
readonly idpMetadata: string | undefined,
/** The Assertion Consumer Service (ACS) URL of the connection. */
readonly acsUrl: string,
readonly acsUrl: string | undefined,
/** The Entity ID as provided by the Service Provider (Clerk). */
readonly spEntityId: string,
readonly spEntityId: string | undefined,
/** The metadata URL as provided by the Service Provider (Clerk). */
readonly spMetadataUrl: string,
/** Whether the connection syncs user attributes between the IdP and Clerk. */
readonly syncUserAttributes: boolean,
readonly spMetadataUrl: string | undefined,
/**
* Whether the connection syncs user attributes between the IdP and Clerk.
* @deprecated The Backend API does not return this field on the nested SAML connection, so it is always `undefined`. Use the top-level `syncUserAttributes` on {@link EnterpriseConnection} instead.
*/
readonly syncUserAttributes: boolean | undefined,
/** Whether users with an email address subdomain are allowed to use this connection. */
readonly allowSubdomains: boolean,
/** Whether IdP-initiated SSO is allowed. */
readonly allowIdpInitiated: boolean,
/** Whether the SAML connection is active. */
readonly active: boolean,
/** Whether the SAML connection requires force authentication. */
readonly forceAuthn: boolean,
/** The `login_hint` configuration of the SAML connection. */
readonly loginHint: EnterpriseConnectionSamlConnectionLoginHint,
) {}

static fromJSON(data: EnterpriseConnectionSamlConnectionJSON): EnterpriseConnectionSamlConnection {
Expand All @@ -58,6 +116,9 @@ export class EnterpriseConnectionSamlConnection {
data.sync_user_attributes,
data.allow_subdomains,
data.allow_idp_initiated,
data.active,
data.force_authn,
EnterpriseConnectionSamlConnectionLoginHint.fromJSON(data.login_hint),
);
}
}
Expand All @@ -78,15 +139,15 @@ export class EnterpriseConnectionOauthConfig {
/**
* The OAuth client ID.
*/
readonly clientId: string,
readonly clientId: string | undefined,
/**
* The OpenID Connect discovery URL.
*/
readonly discoveryUrl: string,
readonly discoveryUrl: string | undefined,
/**
* The public URL of the OAuth provider logo, if available.
*/
readonly logoPublicUrl: string,
readonly logoPublicUrl: string | undefined,
/**
* The date when the configuration was first created.
*/
Expand All @@ -95,6 +156,26 @@ export class EnterpriseConnectionOauthConfig {
* The date when the configuration was last updated.
*/
readonly updatedAt: number,
/**
* The OAuth provider key of the configuration. For example, `'custom_oidc'`.
*/
readonly providerKey: string,
/**
* The OAuth authorization URL.
*/
readonly authUrl: string | undefined,
/**
* The OAuth token URL.
*/
readonly tokenUrl: string | undefined,
/**
* The OAuth user info URL.
*/
readonly userInfoUrl: string | undefined,
/**
* Whether the OAuth configuration requires PKCE.
*/
readonly requiresPkce: boolean,
) {}

static fromJSON(data: EnterpriseConnectionOauthConfigJSON): EnterpriseConnectionOauthConfig {
Expand All @@ -106,6 +187,11 @@ export class EnterpriseConnectionOauthConfig {
data.logo_public_url,
data.created_at,
data.updated_at,
data.provider_key,
data.auth_url,
data.token_url,
data.user_info_url,
data.requires_pkce,
);
}
}
Expand Down Expand Up @@ -141,8 +227,9 @@ export class EnterpriseConnection {
readonly syncUserAttributes: boolean,
/**
* Whether users with an email address subdomain are allowed to use this connection or not.
* @deprecated The Backend API does not return this field at the top level, so it is always `undefined`. Use `samlConnection.allowSubdomains` instead.
*/
readonly allowSubdomains: boolean,
readonly allowSubdomains: boolean | undefined,
/**
* Whether additional identifications are disabled for this connection.
*/
Expand All @@ -163,14 +250,38 @@ export class EnterpriseConnection {
* OAuth (OIDC) configuration when the enterprise connection uses OAuth.
*/
readonly oauthConfig: EnterpriseConnectionOauthConfig | null,
/**
* The identity provider (IdP) of the connection. For example, `'saml_custom'` or `'oidc_custom'`.
*/
readonly provider: string,
/**
* The public URL of the provider logo, if available.
*/
readonly logoPublicUrl: string | undefined,
/**
* Whether existing users who are members of the organization can link their account to their enterprise identity.
*/
readonly allowOrganizationAccountLinking: boolean,
/**
* Whether the connection can be used for sign-in and sign-up.
*/
readonly authenticatable: boolean,
/**
* Whether Just-in-Time (JIT) provisioning of users is disabled for the connection.
*/
readonly disableJitProvisioning: boolean,
/**
* The custom attribute mappings of the connection. Only returned when the custom attributes feature is enabled for the instance.
*/
readonly customAttributes: EnterpriseConnectionCustomAttribute[] | undefined,
Comment on lines +253 to +276

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 '"version"|"exports"' packages/backend/package.json
ast-grep outline packages/backend/src/api/resources/EnterpriseConnection.ts --items all --type class
rg -n -C 3 '\bEnterpriseConnection(SamlConnection|OauthConfig)?\b' packages/backend/src/index.ts

Repository: clerk/javascript

Length of output: 1546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Constructor definitions\n'
sed -n '1,340p' packages/backend/src/api/resources/EnterpriseConnection.ts | nl -ba | sed -n '1,340p'

printf '\n## Direct usages in repo\n'
rg -n 'new\s+EnterpriseConnection(SamlConnectionLoginHint|CustomAttribute|SamlConnection|OauthConfig|)\b' packages/backend/src || true

printf '\n## Package entry exports around backend index\n'
sed -n '120,190p' packages/backend/src/index.ts | nl -ba | sed -n '1,120p'

printf '\n## Changeset\n'
sed -n '1,80p' .changeset/major-poets-refuse.md | nl -ba | sed -n '1,80p'

Repository: clerk/javascript

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## EnterpriseConnection.ts constructors\n'
grep -nE '^export class (EnterpriseConnectionSamlConnectionLoginHint|EnterpriseConnectionCustomAttribute|EnterpriseConnectionSamlConnection|EnterpriseConnectionOauthConfig|EnterpriseConnection)\b|^  constructor\(' packages/backend/src/api/resources/EnterpriseConnection.ts

printf '\n## Relevant source around constructors\n'
sed -n '1,340p' packages/backend/src/api/resources/EnterpriseConnection.ts | cat -n | sed -n '1,340p'

printf '\n## Public exports from backend entry\n'
grep -nE 'EnterpriseConnection(SamlConnectionLoginHint|CustomAttribute|SamlConnection|OauthConfig)?' packages/backend/src/index.ts

printf '\n## Direct constructor usages in repo\n'
rg -n 'new\s+EnterpriseConnection(SamlConnectionLoginHint|CustomAttribute|SamlConnection|OauthConfig)?\b' packages/backend/src || true

printf '\n## Changeset\n'
cat -n .changeset/major-poets-refuse.md

Repository: clerk/javascript

Length of output: 15917


Bump @clerk/backend to a major release

These exported constructors are part of the public API, so adding required trailing parameters is a breaking change. Update .changeset/major-poets-refuse.md to major, or make the new arguments backward-compatible.

  • packages/backend/src/api/resources/EnterpriseConnection.ts
  • .changeset/major-poets-refuse.md
📍 Affects 2 files
  • packages/backend/src/api/resources/EnterpriseConnection.ts#L253-L276 (this comment)
  • packages/backend/src/api/resources/EnterpriseConnection.ts#L94-L99
  • packages/backend/src/api/resources/EnterpriseConnection.ts#L159-L178
  • .changeset/major-poets-refuse.md#L2-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/api/resources/EnterpriseConnection.ts` around lines 253
- 276, Update the public EnterpriseConnection constructors at
packages/backend/src/api/resources/EnterpriseConnection.ts lines 94-99, 159-178,
and 253-276 so the newly added parameters are backward-compatible, or classify
the API change as major by changing .changeset/major-poets-refuse.md line 2 to
major; ensure all affected constructor signatures follow the same choice.

Source: Coding guidelines

) {}

static fromJSON(data: EnterpriseConnectionJSON): EnterpriseConnection {
return new EnterpriseConnection(
data.id,
data.name,
data.domains,
data.organization_id,
data.organization_id ?? null,
data.active,
data.sync_user_attributes,
data.allow_subdomains,
Expand All @@ -179,6 +290,12 @@ export class EnterpriseConnection {
data.updated_at,
data.saml_connection != null ? EnterpriseConnectionSamlConnection.fromJSON(data.saml_connection) : null,
data.oauth_config != null ? EnterpriseConnectionOauthConfig.fromJSON(data.oauth_config) : null,
data.provider,
data.logo_public_url,
data.allow_organization_account_linking,
data.authenticatable,
data.disable_jit_provisioning,
data.custom_attributes?.map(attr => EnterpriseConnectionCustomAttribute.fromJSON(attr)),
);
}
}
Loading
Loading