Skip to content

Commit 2b69461

Browse files
committed
feat(self-serve-ds): add DirectorySync resource and Organization contract
Adds DirectorySync/DirectorySyncUser types, connection-scoped Directory Sync methods on the Organization contract and resource (hitting .../enterprise_connections/{id}/scim_directory), and the self_serve_directory_sync user-settings flag (absent on older backends, defaulting to false).
1 parent 7e063bb commit 2b69461

11 files changed

Lines changed: 517 additions & 2 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {
2+
DirectorySyncJSON,
3+
DirectorySyncJSONSnapshot,
4+
DirectorySyncProvider,
5+
DirectorySyncResource,
6+
DirectorySyncUserJSON,
7+
DirectorySyncUserResource,
8+
} from '@clerk/shared/types';
9+
10+
import { unixEpochToDate } from '../../utils/date';
11+
import { BaseResource } from './Base';
12+
13+
export class DirectorySync extends BaseResource implements DirectorySyncResource {
14+
id!: string;
15+
name!: string;
16+
enterpriseConnectionId: string | null = null;
17+
endpointUrl!: string;
18+
provider!: DirectorySyncProvider;
19+
enabled!: boolean;
20+
groupRoleMappingEnabled!: boolean;
21+
attributeMapping: Record<string, string> = {};
22+
apiKey: string | null = null;
23+
createdAt: Date | null = null;
24+
updatedAt: Date | null = null;
25+
26+
constructor(data: DirectorySyncJSON | DirectorySyncJSONSnapshot | null) {
27+
super();
28+
this.fromJSON(data);
29+
}
30+
31+
protected fromJSON(data: DirectorySyncJSON | DirectorySyncJSONSnapshot | null): this {
32+
if (!data) {
33+
return this;
34+
}
35+
36+
this.id = data.id;
37+
this.name = data.name;
38+
this.enterpriseConnectionId = data.enterprise_connection_id ?? null;
39+
this.endpointUrl = data.endpoint_url;
40+
this.provider = data.provider;
41+
this.enabled = data.enabled;
42+
this.groupRoleMappingEnabled = data.group_role_mapping_enabled;
43+
this.attributeMapping = data.attribute_mapping ?? {};
44+
this.apiKey = data.api_key ?? null;
45+
this.createdAt = unixEpochToDate(data.created_at);
46+
this.updatedAt = unixEpochToDate(data.updated_at);
47+
48+
return this;
49+
}
50+
51+
public __internal_toSnapshot(): DirectorySyncJSONSnapshot {
52+
return {
53+
object: 'directory',
54+
id: this.id,
55+
name: this.name,
56+
enterprise_connection_id: this.enterpriseConnectionId,
57+
endpoint_url: this.endpointUrl,
58+
provider: this.provider,
59+
enabled: this.enabled,
60+
group_role_mapping_enabled: this.groupRoleMappingEnabled,
61+
attribute_mapping: this.attributeMapping,
62+
// The bearer token is deliberately absent: snapshots may be persisted
63+
// and the secret must never outlive the response it arrived on.
64+
created_at: this.createdAt?.getTime() ?? 0,
65+
updated_at: this.updatedAt?.getTime() ?? 0,
66+
};
67+
}
68+
}
69+
70+
export class DirectorySyncUser extends BaseResource implements DirectorySyncUserResource {
71+
id!: string;
72+
userId!: string;
73+
firstName: string | null = null;
74+
lastName: string | null = null;
75+
identifier: string | null = null;
76+
imageUrl!: string;
77+
hasImage!: boolean;
78+
active!: boolean;
79+
provisionedAt: Date | null = null;
80+
updatedAt: Date | null = null;
81+
82+
constructor(data: DirectorySyncUserJSON | null) {
83+
super();
84+
this.fromJSON(data);
85+
}
86+
87+
protected fromJSON(data: DirectorySyncUserJSON | null): this {
88+
if (!data) {
89+
return this;
90+
}
91+
92+
this.id = data.id;
93+
this.userId = data.user_id;
94+
this.firstName = data.first_name;
95+
this.lastName = data.last_name;
96+
this.identifier = data.identifier;
97+
this.imageUrl = data.image_url;
98+
this.hasImage = data.has_image;
99+
this.active = data.active;
100+
this.provisionedAt = unixEpochToDate(data.provisioned_at);
101+
this.updatedAt = unixEpochToDate(data.updated_at);
102+
103+
return this;
104+
}
105+
}

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ import type {
22
AddMemberParams,
33
ClerkPaginatedResponse,
44
ClerkResourceReloadParams,
5+
CreateDirectorySyncParams,
56
CreateOrganizationDomainParams,
67
CreateOrganizationEnterpriseConnectionParams,
78
CreateOrganizationParams,
89
DeletedObjectJSON,
910
DeletedObjectResource,
11+
DirectorySyncJSON,
12+
DirectorySyncResource,
13+
DirectorySyncUserJSON,
14+
DirectorySyncUserResource,
1015
EnterpriseConnectionJSON,
1116
EnterpriseConnectionResource,
1217
EnterpriseConnectionTestRunInitJSON,
1318
EnterpriseConnectionTestRunInitResource,
1419
EnterpriseConnectionTestRunJSON,
1520
EnterpriseConnectionTestRunResource,
1621
EnterpriseConnectionTestRunsPaginatedJSON,
22+
GetDirectorySyncUsersParams,
1723
GetDomainsParams,
1824
GetEnterpriseConnectionsParams,
1925
GetEnterpriseConnectionTestRunsParams,
@@ -37,6 +43,7 @@ import type {
3743
OrganizationResource,
3844
RoleJSON,
3945
SetOrganizationLogoParams,
46+
UpdateDirectorySyncParams,
4047
UpdateMembershipParams,
4148
UpdateOrganizationEnterpriseConnectionParams,
4249
UpdateOrganizationParams,
@@ -49,6 +56,8 @@ import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '..
4956
import {
5057
BaseResource,
5158
DeletedObject,
59+
DirectorySync,
60+
DirectorySyncUser,
5261
EnterpriseConnection,
5362
EnterpriseConnectionTestRun,
5463
OrganizationInvitation,
@@ -274,6 +283,95 @@ export class Organization extends BaseResource implements OrganizationResource {
274283
};
275284
};
276285

286+
getDirectorySync = async (enterpriseConnectionId: string): Promise<DirectorySyncResource> => {
287+
const json = (
288+
await BaseResource._fetch<DirectorySyncJSON>({
289+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory`,
290+
method: 'GET',
291+
})
292+
)?.response as unknown as DirectorySyncJSON;
293+
294+
return new DirectorySync(json);
295+
};
296+
297+
createDirectorySync = async (
298+
enterpriseConnectionId: string,
299+
params?: CreateDirectorySyncParams,
300+
): Promise<DirectorySyncResource> => {
301+
const json = (
302+
await BaseResource._fetch<DirectorySyncJSON>({
303+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory`,
304+
method: 'POST',
305+
body: (params?.name ? { name: params.name } : {}) as any,
306+
})
307+
)?.response as unknown as DirectorySyncJSON;
308+
309+
return new DirectorySync(json);
310+
};
311+
312+
updateDirectorySync = async (
313+
enterpriseConnectionId: string,
314+
params: UpdateDirectorySyncParams,
315+
): Promise<DirectorySyncResource> => {
316+
const body: Record<string, string | boolean> = {};
317+
if (params.enabled !== undefined) {
318+
body.enabled = params.enabled;
319+
}
320+
if (params.attributeMapping !== undefined) {
321+
body.attribute_mapping = JSON.stringify(params.attributeMapping);
322+
}
323+
324+
const json = (
325+
await BaseResource._fetch<DirectorySyncJSON>({
326+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory`,
327+
method: 'PATCH',
328+
body: body as any,
329+
})
330+
)?.response as unknown as DirectorySyncJSON;
331+
332+
return new DirectorySync(json);
333+
};
334+
335+
rotateDirectorySyncToken = async (enterpriseConnectionId: string): Promise<DirectorySyncResource> => {
336+
const json = (
337+
await BaseResource._fetch<DirectorySyncJSON>({
338+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory/rotate_api_key`,
339+
method: 'POST',
340+
})
341+
)?.response as unknown as DirectorySyncJSON;
342+
343+
return new DirectorySync(json);
344+
};
345+
346+
deleteDirectorySync = async (enterpriseConnectionId: string): Promise<DeletedObjectResource> => {
347+
const json = (
348+
await BaseResource._fetch<DeletedObjectJSON>({
349+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory`,
350+
method: 'DELETE',
351+
})
352+
)?.response as unknown as DeletedObjectJSON;
353+
354+
return new DeletedObject(json);
355+
};
356+
357+
getDirectorySyncUsers = async (
358+
enterpriseConnectionId: string,
359+
params?: GetDirectorySyncUsersParams,
360+
): Promise<ClerkPaginatedResponse<DirectorySyncUserResource>> => {
361+
const res = await BaseResource._fetch({
362+
path: `/organizations/${this.id}/enterprise_connections/${enterpriseConnectionId}/directory/users`,
363+
method: 'GET',
364+
search: convertPageToOffsetSearchParams(params),
365+
});
366+
367+
const payload = res?.response as unknown as ClerkPaginatedResponse<DirectorySyncUserJSON> | undefined;
368+
369+
return {
370+
total_count: payload?.total_count ?? 0,
371+
data: (payload?.data ?? []).map(row => new DirectorySyncUser(row)),
372+
};
373+
};
374+
277375
getMembershipRequests = async (
278376
getRequestParam?: GetMembershipRequestParams,
279377
): Promise<ClerkPaginatedResponse<OrganizationMembershipRequestResource>> => {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export class UserSettings extends BaseResource implements UserSettingsResource {
108108
enterpriseSSO: EnterpriseSSOSettings = {
109109
enabled: false,
110110
self_serve_sso: false,
111+
self_serve_directory_sync: false,
111112
};
112113
passkeySettings: PasskeySettingsData = {
113114
allow_autofill: false,
@@ -225,7 +226,10 @@ export class UserSettings extends BaseResource implements UserSettingsResource {
225226
this.attackProtection.enumeration_protection.enabled,
226227
},
227228
};
228-
this.enterpriseSSO = this.withDefault(data.enterprise_sso, this.enterpriseSSO);
229+
this.enterpriseSSO = {
230+
...this.withDefault(data.enterprise_sso, this.enterpriseSSO),
231+
self_serve_directory_sync: data.enterprise_sso?.self_serve_directory_sync ?? false,
232+
};
229233
this.passkeySettings = this.withDefault(data.passkey_settings, this.passkeySettings);
230234
this.passwordSettings = data.password_settings
231235
? {

0 commit comments

Comments
 (0)