Skip to content

Commit 785f057

Browse files
authored
chore(clerk-js,shared): Add API methods to manage enterprise connections (#8421)
1 parent 244920d commit 785f057

11 files changed

Lines changed: 844 additions & 4 deletions

File tree

.changeset/pink-dolls-rush.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
---
5+
6+
Add internal API methods to manage enterprise connections

packages/clerk-js/bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "543KB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "68KB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "70KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" },
66
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "309KB" },
7-
{ "path": "./dist/clerk.native.js", "maxSize": "68KB" },
7+
{ "path": "./dist/clerk.native.js", "maxSize": "70KB" },
88
{ "path": "./dist/vendors*.js", "maxSize": "7KB" },
99
{ "path": "./dist/coinbase*.js", "maxSize": "36KB" },
1010
{ "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" },
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import type {
2+
ClerkResourceReloadParams,
3+
EnterpriseConnectionTestRunJSON,
4+
EnterpriseConnectionTestRunJSONSnapshot,
5+
EnterpriseConnectionTestRunLogResource,
6+
EnterpriseConnectionTestRunOauthPayloadJSON,
7+
EnterpriseConnectionTestRunOauthPayloadResource,
8+
EnterpriseConnectionTestRunParsedUserInfoJSON,
9+
EnterpriseConnectionTestRunParsedUserInfoResource,
10+
EnterpriseConnectionTestRunResource,
11+
EnterpriseConnectionTestRunSamlPayloadJSON,
12+
EnterpriseConnectionTestRunSamlPayloadResource,
13+
} from '@clerk/shared/types';
14+
15+
import { unixEpochToDate } from '../../utils/date';
16+
import { clerkUnsupportedReloadMethod } from '../errors';
17+
18+
export class EnterpriseConnectionTestRun implements EnterpriseConnectionTestRunResource {
19+
pathRoot = '/me';
20+
21+
id!: string;
22+
status!: string;
23+
connectionType!: 'saml' | 'oauth';
24+
parsedUserInfo: EnterpriseConnectionTestRunParsedUserInfoResource | null = null;
25+
logs: EnterpriseConnectionTestRunLogResource[] = [];
26+
saml: EnterpriseConnectionTestRunSamlPayloadResource | null = null;
27+
oauth: EnterpriseConnectionTestRunOauthPayloadResource | null = null;
28+
createdAt: Date | null = null;
29+
30+
constructor(data: EnterpriseConnectionTestRunJSON) {
31+
this.fromJSON(data);
32+
}
33+
34+
reload(_?: ClerkResourceReloadParams): Promise<this> {
35+
clerkUnsupportedReloadMethod('EnterpriseConnectionTestRun');
36+
}
37+
38+
private fromJSON(data: EnterpriseConnectionTestRunJSON | null): this {
39+
if (!data) {
40+
return this;
41+
}
42+
43+
this.id = data.id;
44+
this.status = data.status;
45+
this.connectionType = data.connection_type;
46+
this.parsedUserInfo = parsedUserInfoFromJSON(data.parsed_user_info ?? null);
47+
this.saml = samlPayloadFromJSON(data.saml ?? null);
48+
this.oauth = oauthPayloadFromJSON(data.oauth ?? null);
49+
this.createdAt = unixEpochToDate(data.created_at);
50+
this.logs = (data.logs ?? []).map(log => ({
51+
level: log.level,
52+
code: log.code,
53+
shortMessage: log.short_message,
54+
message: log.message,
55+
}));
56+
57+
return this;
58+
}
59+
60+
public __internal_toSnapshot(): EnterpriseConnectionTestRunJSONSnapshot {
61+
return {
62+
object: 'enterprise_connection_test_run',
63+
id: this.id,
64+
status: this.status,
65+
connection_type: this.connectionType,
66+
parsed_user_info: parsedUserInfoToJSON(this.parsedUserInfo),
67+
saml: samlPayloadToJSON(this.saml),
68+
oauth: oauthPayloadToJSON(this.oauth),
69+
logs: this.logs.map(log => ({
70+
level: log.level,
71+
code: log.code,
72+
short_message: log.shortMessage,
73+
message: log.message,
74+
})),
75+
created_at: this.createdAt?.getTime() ?? 0,
76+
};
77+
}
78+
}
79+
80+
function parsedUserInfoFromJSON(
81+
data: EnterpriseConnectionTestRunParsedUserInfoJSON | null | undefined,
82+
): EnterpriseConnectionTestRunParsedUserInfoResource | null {
83+
if (!data) {
84+
return null;
85+
}
86+
87+
return {
88+
emailAddress: data.email_address,
89+
firstName: data.first_name,
90+
lastName: data.last_name,
91+
userId: data.user_id,
92+
};
93+
}
94+
95+
function parsedUserInfoToJSON(
96+
data: EnterpriseConnectionTestRunParsedUserInfoResource | null,
97+
): EnterpriseConnectionTestRunParsedUserInfoJSON | null {
98+
if (!data) {
99+
return null;
100+
}
101+
102+
return {
103+
email_address: data.emailAddress,
104+
first_name: data.firstName,
105+
last_name: data.lastName,
106+
user_id: data.userId,
107+
};
108+
}
109+
110+
function samlPayloadFromJSON(
111+
data: EnterpriseConnectionTestRunSamlPayloadJSON | null | undefined,
112+
): EnterpriseConnectionTestRunSamlPayloadResource | null {
113+
if (!data) {
114+
return null;
115+
}
116+
117+
return {
118+
samlRequest: data.saml_request,
119+
samlResponse: data.saml_response,
120+
relayState: data.relay_state,
121+
};
122+
}
123+
124+
function samlPayloadToJSON(
125+
data: EnterpriseConnectionTestRunSamlPayloadResource | null,
126+
): EnterpriseConnectionTestRunSamlPayloadJSON | null {
127+
if (!data) {
128+
return null;
129+
}
130+
131+
return {
132+
saml_request: data.samlRequest,
133+
saml_response: data.samlResponse,
134+
relay_state: data.relayState,
135+
};
136+
}
137+
138+
function oauthPayloadFromJSON(
139+
data: EnterpriseConnectionTestRunOauthPayloadJSON | null | undefined,
140+
): EnterpriseConnectionTestRunOauthPayloadResource | null {
141+
if (!data) {
142+
return null;
143+
}
144+
145+
return {
146+
idToken: data.id_token,
147+
accessToken: data.access_token,
148+
userInfo: data.user_info,
149+
};
150+
}
151+
152+
function oauthPayloadToJSON(
153+
data: EnterpriseConnectionTestRunOauthPayloadResource | null,
154+
): EnterpriseConnectionTestRunOauthPayloadJSON | null {
155+
if (!data) {
156+
return null;
157+
}
158+
159+
return {
160+
id_token: data.idToken,
161+
access_token: data.accessToken,
162+
user_info: data.userInfo,
163+
};
164+
}

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { getFullName } from '@clerk/shared/internal/clerk-js/user';
22
import type {
33
BackupCodeJSON,
44
BackupCodeResource,
5+
ClerkPaginatedResponse,
56
CreateEmailAddressParams,
67
CreateExternalAccountParams,
8+
CreateMeEnterpriseConnectionParams,
79
CreatePhoneNumberParams,
810
CreateWeb3WalletParams,
911
DeletedObjectJSON,
@@ -12,9 +14,15 @@ import type {
1214
EnterpriseAccountResource,
1315
EnterpriseConnectionJSON,
1416
EnterpriseConnectionResource,
17+
EnterpriseConnectionTestRunInitJSON,
18+
EnterpriseConnectionTestRunInitResource,
19+
EnterpriseConnectionTestRunJSON,
20+
EnterpriseConnectionTestRunResource,
21+
EnterpriseConnectionTestRunsPaginatedJSON,
1522
ExternalAccountJSON,
1623
ExternalAccountResource,
1724
GetEnterpriseConnectionsParams,
25+
GetEnterpriseConnectionTestRunsParams,
1826
GetOrganizationMemberships,
1927
GetUserOrganizationInvitationsParams,
2028
GetUserOrganizationSuggestionsParams,
@@ -26,6 +34,7 @@ import type {
2634
SetProfileImageParams,
2735
TOTPJSON,
2836
TOTPResource,
37+
UpdateMeEnterpriseConnectionParams,
2938
UpdateUserParams,
3039
UpdateUserPasswordParams,
3140
UserJSON,
@@ -34,7 +43,9 @@ import type {
3443
VerifyTOTPParams,
3544
Web3WalletResource,
3645
} from '@clerk/shared/types';
46+
import { deepCamelToSnake } from '@clerk/shared/underscore';
3747

48+
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
3849
import { unixEpochToDate } from '../../utils/date';
3950
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
4051
import { eventBus, events } from '../events';
@@ -46,6 +57,7 @@ import {
4657
EmailAddress,
4758
EnterpriseAccount,
4859
EnterpriseConnection,
60+
EnterpriseConnectionTestRun,
4961
ExternalAccount,
5062
Image,
5163
OrganizationMembership,
@@ -316,6 +328,85 @@ export class User extends BaseResource implements UserResource {
316328
return (json || []).map(connection => new EnterpriseConnection(connection));
317329
};
318330

331+
createEnterpriseConnection = async (
332+
params: CreateMeEnterpriseConnectionParams,
333+
): Promise<EnterpriseConnectionResource> => {
334+
const json = (
335+
await BaseResource._fetch<EnterpriseConnectionJSON>({
336+
path: `${this.path()}/enterprise_connections`,
337+
method: 'POST',
338+
body: toMeEnterpriseConnectionBody(params) as any,
339+
})
340+
)?.response as unknown as EnterpriseConnectionJSON;
341+
342+
return new EnterpriseConnection(json);
343+
};
344+
345+
updateEnterpriseConnection = async (
346+
enterpriseConnectionId: string,
347+
params: UpdateMeEnterpriseConnectionParams,
348+
): Promise<EnterpriseConnectionResource> => {
349+
const json = (
350+
await BaseResource._fetch<EnterpriseConnectionJSON>({
351+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
352+
method: 'PATCH',
353+
body: toMeEnterpriseConnectionBody(params) as any,
354+
})
355+
)?.response as unknown as EnterpriseConnectionJSON;
356+
357+
return new EnterpriseConnection(json);
358+
};
359+
360+
deleteEnterpriseConnection = async (enterpriseConnectionId: string): Promise<DeletedObjectResource> => {
361+
const json = (
362+
await BaseResource._fetch<DeletedObjectJSON>({
363+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
364+
method: 'DELETE',
365+
})
366+
)?.response as unknown as DeletedObjectJSON;
367+
368+
return new DeletedObject(json);
369+
};
370+
371+
createEnterpriseConnectionTestRun = async (
372+
enterpriseConnectionId: string,
373+
): Promise<EnterpriseConnectionTestRunInitResource> => {
374+
const json = (
375+
await BaseResource._fetch({
376+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
377+
method: 'POST',
378+
})
379+
)?.response as unknown as EnterpriseConnectionTestRunInitJSON;
380+
381+
return { url: json.url };
382+
};
383+
384+
getEnterpriseConnectionTestRuns = async (
385+
enterpriseConnectionId: string,
386+
params?: GetEnterpriseConnectionTestRunsParams,
387+
): Promise<ClerkPaginatedResponse<EnterpriseConnectionTestRunResource>> => {
388+
const { status, ...rest } = params || {};
389+
const search = convertPageToOffsetSearchParams(rest);
390+
if (status?.length) {
391+
for (const s of status) {
392+
search.append('status', s);
393+
}
394+
}
395+
396+
const res = await BaseResource._fetch({
397+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}/test_runs`,
398+
method: 'GET',
399+
search,
400+
});
401+
402+
const payload = res?.response as unknown as EnterpriseConnectionTestRunsPaginatedJSON | undefined;
403+
404+
return {
405+
total_count: payload?.total_count ?? 0,
406+
data: (payload?.data ?? []).map((row: EnterpriseConnectionTestRunJSON) => new EnterpriseConnectionTestRun(row)),
407+
};
408+
};
409+
319410
initializePaymentMethod: typeof initializePaymentMethod = params => {
320411
return initializePaymentMethod(params);
321412
};
@@ -455,3 +546,30 @@ export class User extends BaseResource implements UserResource {
455546
};
456547
}
457548
}
549+
550+
/**
551+
* Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams`
552+
* for the `/me/enterprise_connections` FAPI endpoints.
553+
*
554+
* Uses `deepCamelToSnake` but preserves `saml.attributeMapping` and `customAttributes` as-is. Their keys are
555+
* user-supplied data and must not be camel→snake transformed.
556+
*/
557+
function toMeEnterpriseConnectionBody(
558+
params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams,
559+
): Record<string, unknown> {
560+
const originalAttributeMapping =
561+
params.saml && typeof params.saml === 'object' ? params.saml.attributeMapping : undefined;
562+
const originalCustomAttributes = 'customAttributes' in params ? params.customAttributes : undefined;
563+
564+
const body = deepCamelToSnake(params) as Record<string, any>;
565+
566+
if (originalAttributeMapping !== undefined && body.saml && typeof body.saml === 'object') {
567+
body.saml.attribute_mapping = originalAttributeMapping;
568+
}
569+
570+
if (originalCustomAttributes !== undefined) {
571+
body.custom_attributes = originalCustomAttributes;
572+
}
573+
574+
return body;
575+
}

0 commit comments

Comments
 (0)