diff --git a/packages/ui/src/Atoms/ThirdParties/Clerk.tsx b/packages/ui/src/Atoms/ThirdParties/Clerk.tsx
new file mode 100644
index 000000000..91e98cdf2
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/Clerk.tsx
@@ -0,0 +1,16 @@
+import type { ComponentProps } from "react";
+
+export function Clerk(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
index f3ee9f028..3b0dcf16c 100644
--- a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
+++ b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
@@ -18,6 +18,7 @@ import { Anthropic } from "./Anthropic";
import { Asana } from "./Asana";
import { Bitbucket } from "./Bitbucket";
import { Brex } from "./Brex";
+import { Clerk } from "./Clerk";
import { ClickUp } from "./ClickUp";
import { Cloudflare } from "./Cloudflare";
import { Cursor } from "./Cursor";
@@ -55,6 +56,7 @@ const thirdParties: Record>> = {
ASANA: Asana,
BITBUCKET: Bitbucket,
BREX: Brex,
+ CLERK: Clerk,
CLICKUP: ClickUp,
CLOUDFLARE: Cloudflare,
CURSOR: Cursor,
diff --git a/packages/ui/src/Atoms/ThirdParties/index.ts b/packages/ui/src/Atoms/ThirdParties/index.ts
index b12188d41..3c14999c1 100644
--- a/packages/ui/src/Atoms/ThirdParties/index.ts
+++ b/packages/ui/src/Atoms/ThirdParties/index.ts
@@ -2,6 +2,7 @@ export { Anthropic } from "./Anthropic";
export { Asana } from "./Asana";
export { Bitbucket } from "./Bitbucket";
export { Brex } from "./Brex";
+export { Clerk } from "./Clerk";
export { ClickUp } from "./ClickUp";
export { Cloudflare } from "./Cloudflare";
export { Cursor } from "./Cursor";
diff --git a/pkg/accessreview/drivers/clerk.go b/pkg/accessreview/drivers/clerk.go
index 0238ab34d..2687825ba 100644
--- a/pkg/accessreview/drivers/clerk.go
+++ b/pkg/accessreview/drivers/clerk.go
@@ -18,8 +18,8 @@ import (
"context"
"encoding/json"
"fmt"
- "io"
"net/http"
+ "strconv"
"strings"
"time"
@@ -49,6 +49,7 @@ type clerkUser struct {
BackupCodeEnabled bool `json:"backup_code_enabled"`
Banned bool `json:"banned"`
Locked bool `json:"locked"`
+ Deprovisioned bool `json:"deprovisioned"`
LastSignInAt *int64 `json:"last_sign_in_at"`
CreatedAt int64 `json:"created_at"`
EmailAddresses []struct {
@@ -57,10 +58,6 @@ type clerkUser struct {
} `json:"email_addresses"`
}
-type clerkUsersEnvelope struct {
- Data []clerkUser `json:"data"`
-}
-
func NewClerkDriver(httpClient *http.Client) *ClerkDriver {
return &ClerkDriver{
httpClient: &http.Client{
@@ -84,10 +81,6 @@ func (d *ClerkDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
return nil, err
}
- if len(users) == 0 {
- return records, nil
- }
-
for _, u := range users {
email := clerkPrimaryEmail(u)
if email == "" {
@@ -97,7 +90,7 @@ func (d *ClerkDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
record := AccountRecord{
Email: email,
FullName: clerkFullName(u, email),
- Active: new(!u.Banned && !u.Locked),
+ Active: new(!u.Banned && !u.Locked && !u.Deprovisioned),
IsAdmin: false,
MFAStatus: clerkMFAStatus(u),
AuthMethod: clerkAuthMethod(u),
@@ -133,8 +126,8 @@ func (d *ClerkDriver) fetchUsersPage(ctx context.Context, offset int) ([]clerkUs
}
q := req.URL.Query()
- q.Set("limit", fmt.Sprintf("%d", clerkUsersPageSize))
- q.Set("offset", fmt.Sprintf("%d", offset))
+ q.Set("limit", strconv.Itoa(clerkUsersPageSize))
+ q.Set("offset", strconv.Itoa(offset))
req.URL.RawQuery = q.Encode()
req.Header.Set("Accept", "application/json")
@@ -152,23 +145,12 @@ func (d *ClerkDriver) fetchUsersPage(ctx context.Context, offset int) ([]clerkUs
return nil, fmt.Errorf("cannot fetch clerk users: unexpected status %d", httpResp.StatusCode)
}
- body, err := io.ReadAll(httpResp.Body)
- if err != nil {
- return nil, fmt.Errorf("cannot read clerk users response: %w", err)
- }
-
- var paged clerkUsersEnvelope
- if err := json.Unmarshal(body, &paged); err != nil {
- return nil, fmt.Errorf("cannot decode clerk users response: %w", err)
- }
-
- if paged.Data != nil {
- return paged.Data, nil
- }
-
+ // GET /v1/users returns a bare JSON array of user objects; the total
+ // count is exposed separately via /v1/users/count. Decode directly
+ // into a slice.
var users []clerkUser
- if err := json.Unmarshal(body, &users); err != nil {
- return nil, fmt.Errorf("cannot decode clerk users list response: %w", err)
+ if err := json.NewDecoder(httpResp.Body).Decode(&users); err != nil {
+ return nil, fmt.Errorf("cannot decode clerk users response: %w", err)
}
return users, nil
diff --git a/pkg/accessreview/drivers/clerk_test.go b/pkg/accessreview/drivers/clerk_test.go
index de71ebdee..f233ced77 100644
--- a/pkg/accessreview/drivers/clerk_test.go
+++ b/pkg/accessreview/drivers/clerk_test.go
@@ -35,29 +35,32 @@ func TestClerkDriver(t *testing.T) {
require.NoError(t, err)
require.Len(t, records, 3)
+ // Clerk returns users newest-first (default order_by=-created_at).
first := records[0]
- assert.Equal(t, "usr_000000000000000000000001", first.ExternalID)
- assert.Equal(t, "jane@example.com", first.Email)
- assert.Equal(t, "Jane Doe", first.FullName)
+ assert.Equal(t, "user_3EfkCEWmtIsoMD3rRxIpDsBOPzv", first.ExternalID)
+ assert.Equal(t, "c@example.com", first.Email)
+ assert.Equal(t, "c c", first.FullName)
+ assert.Equal(t, coredata.AccessEntryAccountTypeUser, first.AccountType)
require.NotNil(t, first.Active)
assert.True(t, *first.Active)
assert.Equal(t, coredata.MFAStatusDisabled, first.MFAStatus)
assert.Equal(t, coredata.AccessEntryAuthMethodPassword, first.AuthMethod)
assert.NotNil(t, first.CreatedAt)
- assert.NotNil(t, first.LastLogin)
+ assert.Nil(t, first.LastLogin)
second := records[1]
- assert.Equal(t, "developer-user", second.FullName)
+ assert.Equal(t, "b@example.com", second.Email)
+ assert.Equal(t, "b b", second.FullName)
require.NotNil(t, second.Active)
assert.True(t, *second.Active)
- assert.Equal(t, coredata.MFAStatusEnabled, second.MFAStatus)
- assert.Equal(t, coredata.AccessEntryAuthMethodUnknown, second.AuthMethod)
+ // a@example.com is locked, so it must be reported inactive.
third := records[2]
- assert.Equal(t, "blocked@example.com", third.FullName)
+ assert.Equal(t, "a@example.com", third.Email)
+ assert.Equal(t, "a a", third.FullName)
require.NotNil(t, third.Active)
assert.False(t, *third.Active)
- assert.Equal(t, coredata.MFAStatusDisabled, third.MFAStatus)
+ assert.Equal(t, coredata.AccessEntryAuthMethodPassword, third.AuthMethod)
}
func TestClerkPrimaryEmail(t *testing.T) {
diff --git a/pkg/accessreview/drivers/testdata/clerk.yaml b/pkg/accessreview/drivers/testdata/clerk.yaml
index d6acf2993..1fdf431aa 100644
--- a/pkg/accessreview/drivers/testdata/clerk.yaml
+++ b/pkg/accessreview/drivers/testdata/clerk.yaml
@@ -24,14 +24,20 @@ interactions:
proto_minor: 0
content_length: -1
uncompressed: true
- body: '{"data":[{"id":"usr_000000000000000000000001","primary_email_address_id":"eml_primary_1","username":null,"first_name":"Jane","last_name":"Doe","password_enabled":true,"two_factor_enabled":false,"totp_enabled":false,"backup_code_enabled":false,"banned":false,"locked":false,"last_sign_in_at":1748471521000,"created_at":1748342000000,"email_addresses":[{"id":"eml_secondary_1","email_address":"jane+alt@example.com"},{"id":"eml_primary_1","email_address":"jane@example.com"}]},{"id":"usr_000000000000000000000002","primary_email_address_id":null,"username":"developer-user","first_name":null,"last_name":null,"password_enabled":false,"two_factor_enabled":true,"totp_enabled":false,"backup_code_enabled":false,"banned":false,"locked":false,"last_sign_in_at":null,"created_at":1748343000000,"email_addresses":[{"id":"eml_2","email_address":"developer@example.com"}]},{"id":"usr_000000000000000000000003","primary_email_address_id":"eml_primary_3","username":null,"first_name":null,"last_name":null,"password_enabled":false,"two_factor_enabled":false,"totp_enabled":false,"backup_code_enabled":false,"banned":true,"locked":false,"last_sign_in_at":null,"created_at":1748344000000,"email_addresses":[{"id":"eml_primary_3","email_address":"blocked@example.com"}]}],"total_count":3}'
+ body: '[{"id":"user_3EfkCEWmtIsoMD3rRxIpDsBOPzv","object":"user","username":null,"first_name":"c","last_name":"c","locale":null,"image_url":"https://img.clerk.com/anonymized","has_image":false,"primary_email_address_id":"idn_3EfkCCq17KQsZzWLhtKnhAlguva","primary_phone_number_id":null,"primary_web3_wallet_id":null,"password_enabled":true,"two_factor_enabled":false,"totp_enabled":false,"backup_code_enabled":false,"email_addresses":[{"id":"idn_3EfkCCq17KQsZzWLhtKnhAlguva","object":"email_address","email_address":"c@example.com","reserved":false,"verification":{"object":"verification_admin","status":"verified","strategy":"admin","attempts":null,"expire_at":null},"linked_to":[],"matches_sso_connection":false,"created_at":1780576881106,"updated_at":1780576881106}],"phone_numbers":[],"web3_wallets":[],"passkeys":[],"external_accounts":[],"saml_accounts":[],"enterprise_accounts":[],"password_last_updated_at":1780576881104,"public_metadata":{},"private_metadata":{},"unsafe_metadata":{},"external_id":null,"last_sign_in_at":null,"banned":false,"locked":false,"lockout_expires_in_seconds":null,"verification_attempts_remaining":100,"created_at":1780576881104,"updated_at":1780576881109,"delete_self_enabled":true,"bypass_client_trust":false,"create_organization_enabled":true,"last_active_at":null,"mfa_enabled_at":null,"mfa_disabled_at":null,"legal_accepted_at":null,"requires_password_reset":false,"deprovisioned":false,"profile_image_url":"https://www.gravatar.com/avatar?d=mp"},{"id":"user_3EfkAUnJSm40lEsbkAuF9xUs5p2","object":"user","username":null,"first_name":"b","last_name":"b","locale":null,"image_url":"https://img.clerk.com/anonymized","has_image":false,"primary_email_address_id":"idn_3EfkAT1dfG5X3qrNnhntuaNC85w","primary_phone_number_id":null,"primary_web3_wallet_id":null,"password_enabled":true,"two_factor_enabled":false,"totp_enabled":false,"backup_code_enabled":false,"email_addresses":[{"id":"idn_3EfkAT1dfG5X3qrNnhntuaNC85w","object":"email_address","email_address":"b@example.com","reserved":false,"verification":{"object":"verification_admin","status":"verified","strategy":"admin","attempts":null,"expire_at":null},"linked_to":[],"matches_sso_connection":false,"created_at":1780576867751,"updated_at":1780576867751}],"phone_numbers":[],"web3_wallets":[],"passkeys":[],"external_accounts":[],"saml_accounts":[],"enterprise_accounts":[],"password_last_updated_at":1780576867746,"public_metadata":{},"private_metadata":{},"unsafe_metadata":{},"external_id":null,"last_sign_in_at":null,"banned":false,"locked":false,"lockout_expires_in_seconds":null,"verification_attempts_remaining":100,"created_at":1780576867746,"updated_at":1780576960608,"delete_self_enabled":true,"bypass_client_trust":false,"create_organization_enabled":true,"last_active_at":null,"mfa_enabled_at":null,"mfa_disabled_at":null,"legal_accepted_at":null,"requires_password_reset":true,"deprovisioned":false,"profile_image_url":"https://www.gravatar.com/avatar?d=mp"},{"id":"user_3Efk88E4dBmhk95VtBSXcOJ9vCx","object":"user","username":null,"first_name":"a","last_name":"a","locale":null,"image_url":"https://img.clerk.com/anonymized","has_image":false,"primary_email_address_id":"idn_3Efk87Z8o0h9gP86FnA8lUC2Alz","primary_phone_number_id":null,"primary_web3_wallet_id":null,"password_enabled":true,"two_factor_enabled":false,"totp_enabled":false,"backup_code_enabled":false,"email_addresses":[{"id":"idn_3Efk87Z8o0h9gP86FnA8lUC2Alz","object":"email_address","email_address":"a@example.com","reserved":false,"verification":{"object":"verification_admin","status":"verified","strategy":"admin","attempts":null,"expire_at":null},"linked_to":[],"matches_sso_connection":false,"created_at":1780576848230,"updated_at":1780576848230}],"phone_numbers":[],"web3_wallets":[],"passkeys":[],"external_accounts":[],"saml_accounts":[],"enterprise_accounts":[],"password_last_updated_at":1780576848227,"public_metadata":{},"private_metadata":{},"unsafe_metadata":{},"external_id":null,"last_sign_in_at":null,"banned":false,"locked":true,"lockout_expires_in_seconds":3554,"verification_attempts_remaining":100,"created_at":1780576848227,"updated_at":1780576947260,"delete_self_enabled":true,"bypass_client_trust":false,"create_organization_enabled":true,"last_active_at":null,"mfa_enabled_at":null,"mfa_disabled_at":null,"legal_accepted_at":null,"requires_password_reset":false,"deprovisioned":false,"profile_image_url":"https://www.gravatar.com/avatar?d=mp"}]'
headers:
+ Cf-Cache-Status:
+ - DYNAMIC
+ Clerk-Api-Version:
+ - "2025-11-10"
Content-Type:
- application/json
Date:
- - Fri, 29 May 2026 03:30:00 GMT
+ - Thu, 04 Jun 2026 12:43:12 GMT
Server:
- cloudflare
+ X-Cfworker:
+ - "1"
status: 200 OK
code: 200
- duration: 120.5ms
+ duration: 221.280708ms
diff --git a/pkg/connector/provider/clerk.go b/pkg/connector/provider/clerk.go
index e2d029925..fa740a65c 100644
--- a/pkg/connector/provider/clerk.go
+++ b/pkg/connector/provider/clerk.go
@@ -28,9 +28,22 @@ func clerkRegistration() *Registration {
Provider: coredata.ConnectorProviderClerk,
DisplayName: "Clerk",
SupportsAPIKey: true,
- // Clerk's Backend API is authenticated with a server-side secret key
- // via Authorization: Bearer ; there is no third-party OAuth2
- // flow for account-listing access reviews.
+ // Clerk's Backend API authenticates with a server-side secret key
+ // (sk_...) presented as Authorization: Bearer, the default
+ // APIKeyConnection scheme. There is no third-party OAuth2 flow for
+ // account-listing: Clerk's OAuth is an end-user IdP (scoped consent
+ // to a single user's profile), not a partner grant over the Backend
+ // API. The secret key is bound to one Clerk instance, so there is
+ // nothing to pick (Pattern 3): no settings struct, no picker, no
+ // SetOrganizationSettings.
+ //
+ // ProbeURL is intentionally empty: the connection probe runs only
+ // for OAuth2 connections, so it would be dead config for an API-key
+ // provider; a dead key surfaces on the first ListAccounts instead.
+ //
+ // No NewNameResolver: the Backend API exposes no instance/application
+ // name endpoint reachable with a secret key, so the source keeps its
+ // generic name (the source-name worker degrades gracefully).
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
return drivers.NewClerkDriver(c), nil
},