Skip to content
2 changes: 2 additions & 0 deletions lib/management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import withPassword from './password';
import WithFGA from './fga';
import withInboundApplication from './inboundapplication';
import withOutboundApplication from './outboundapplication';
import withOutboundSCIM from './outboundscim';
import withDescoper from './descoper';
import withManagementKey from './managementKey';
import withEngine from './engine';
Expand All @@ -35,6 +36,7 @@ const withManagement = (client: HttpClient, fgaConfig?: FGAConfig) => ({
ssoApplication: withSSOApplication(client),
inboundApplication: withInboundApplication(client),
outboundApplication: withOutboundApplication(client),
outboundSCIM: withOutboundSCIM(client),
sso: withSSOSettings(client),
jwt: withJWT(client),
permission: withPermission(client),
Expand Down
197 changes: 197 additions & 0 deletions lib/management/outboundscim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { SdkResponse } from '@descope/core-js-sdk';
import withManagement from '.';
import apiPaths from './paths';
import { OutboundSCIMConfiguration } from './types';
import { mockHttpClient, resetMockHttpClient } from './testutils';

const management = withManagement(mockHttpClient);

const mockOutboundSCIMConfig: OutboundSCIMConfiguration = {
appId: 'app1',
configuration: { baseUrl: 'https://scim.example.com', token: 'shh' },
enabled: true,
lastExportTime: 1_700_000_000,
lastProcessingTime: 1_700_000_100,
failures: 0,
version: 3,
};

const mockOutboundSCIMConfigResponse = {
configuration: mockOutboundSCIMConfig,
};

describe('Management OutboundSCIM', () => {
afterEach(() => {
jest.clearAllMocks();
resetMockHttpClient();
});

describe('createConfiguration', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => mockOutboundSCIMConfigResponse,
clone: () => ({
json: () => Promise.resolve(mockOutboundSCIMConfigResponse),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<OutboundSCIMConfiguration> =
await management.outboundSCIM.createConfiguration({
appId: 'app1',
configuration: { baseUrl: 'https://scim.example.com', token: 'shh' },
});

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.outboundSCIM.create, {
appId: 'app1',
configuration: { baseUrl: 'https://scim.example.com', token: 'shh' },
});

expect(resp).toEqual({
code: 200,
data: mockOutboundSCIMConfig,
ok: true,
response: httpResponse,
});
});
});

describe('updateConfiguration', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => mockOutboundSCIMConfigResponse,
clone: () => ({
json: () => Promise.resolve(mockOutboundSCIMConfigResponse),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<OutboundSCIMConfiguration> =
await management.outboundSCIM.updateConfiguration({
appId: 'app1',
configuration: { baseUrl: 'https://scim2.example.com', token: 'shh2' },
version: 3,
});

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.outboundSCIM.update, {
appId: 'app1',
configuration: { baseUrl: 'https://scim2.example.com', token: 'shh2' },
version: 3,
});

expect(resp).toEqual({
code: 200,
data: mockOutboundSCIMConfig,
ok: true,
response: httpResponse,
});
});
});

describe('deleteConfiguration', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => {},
clone: () => ({
json: () => Promise.resolve({}),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp = await management.outboundSCIM.deleteConfiguration('app1');

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.outboundSCIM.delete, {
appId: 'app1',
});

expect(resp).toEqual({
code: 200,
data: {},
ok: true,
response: httpResponse,
});
});
});

describe('loadConfiguration', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => mockOutboundSCIMConfigResponse,
clone: () => ({
json: () => Promise.resolve(mockOutboundSCIMConfigResponse),
}),
status: 200,
};
mockHttpClient.get.mockResolvedValue(httpResponse);

const resp: SdkResponse<OutboundSCIMConfiguration> =
await management.outboundSCIM.loadConfiguration('app1');

expect(mockHttpClient.get).toHaveBeenCalledWith(`${apiPaths.outboundSCIM.load}/app1`);

expect(resp).toEqual({
code: 200,
data: mockOutboundSCIMConfig,
ok: true,
response: httpResponse,
});
});
});

describe('setEnabled', () => {
it('should send the correct request when enabling', async () => {
const httpResponse = {
ok: true,
json: () => mockOutboundSCIMConfigResponse,
clone: () => ({
json: () => Promise.resolve(mockOutboundSCIMConfigResponse),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<OutboundSCIMConfiguration> = await management.outboundSCIM.setEnabled(
'app1',
true,
);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.outboundSCIM.setEnabled, {
appId: 'app1',
enabled: true,
});

expect(resp).toEqual({
code: 200,
data: mockOutboundSCIMConfig,
ok: true,
response: httpResponse,
});
});

it('should send the correct request when disabling', async () => {
const httpResponse = {
ok: true,
json: () => mockOutboundSCIMConfigResponse,
clone: () => ({
json: () => Promise.resolve(mockOutboundSCIMConfigResponse),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.outboundSCIM.setEnabled('app1', false);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.outboundSCIM.setEnabled, {
appId: 'app1',
enabled: false,
});
});
});
});
50 changes: 50 additions & 0 deletions lib/management/outboundscim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SdkResponse, transformResponse, HttpClient } from '@descope/core-js-sdk';
import apiPaths from './paths';
import {
OutboundSCIMConfiguration,
CreateOutboundSCIMConfigurationRequest,
UpdateOutboundSCIMConfigurationRequest,
} from './types';

type OutboundSCIMConfigurationResponse = {
configuration: OutboundSCIMConfiguration;
};

const withOutboundSCIM = (httpClient: HttpClient) => ({
/** Create a new outbound SCIM configuration on the federated SSO application identified by `appId`. */
createConfiguration: (
request: CreateOutboundSCIMConfigurationRequest,
): Promise<SdkResponse<OutboundSCIMConfiguration>> =>
transformResponse<OutboundSCIMConfigurationResponse, OutboundSCIMConfiguration>(
httpClient.post(apiPaths.outboundSCIM.create, { ...request }),
(data) => data.configuration,
),
/**
* Update the outbound SCIM configuration attached to the given federated SSO app. `version`
* must match the currently stored version — a mismatch fails the update (optimistic concurrency).
*/
updateConfiguration: (
request: UpdateOutboundSCIMConfigurationRequest,
): Promise<SdkResponse<OutboundSCIMConfiguration>> =>
transformResponse<OutboundSCIMConfigurationResponse, OutboundSCIMConfiguration>(
httpClient.post(apiPaths.outboundSCIM.update, { ...request }),
(data) => data.configuration,
),
/** Delete the outbound SCIM configuration attached to the given federated SSO app. */
deleteConfiguration: (appId: string): Promise<SdkResponse<never>> =>
transformResponse(httpClient.post(apiPaths.outboundSCIM.delete, { appId })),
/** Load the outbound SCIM configuration attached to the given federated SSO app. */
loadConfiguration: (appId: string): Promise<SdkResponse<OutboundSCIMConfiguration>> =>
transformResponse<OutboundSCIMConfigurationResponse, OutboundSCIMConfiguration>(
httpClient.get(`${apiPaths.outboundSCIM.load}/${appId}`),
(data) => data.configuration,
),
/** Enable or disable the outbound SCIM configuration attached to the given federated SSO app. */
setEnabled: (appId: string, enabled: boolean): Promise<SdkResponse<OutboundSCIMConfiguration>> =>
transformResponse<OutboundSCIMConfigurationResponse, OutboundSCIMConfiguration>(
httpClient.post(apiPaths.outboundSCIM.setEnabled, { appId, enabled }),
(data) => data.configuration,
),
});

export default withOutboundSCIM;
7 changes: 7 additions & 0 deletions lib/management/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export default {
batchUploadUserTokens: '/v1/mgmt/outbound/app/user/oauthtoken/batch/upload',
batchUploadTenantTokens: '/v1/mgmt/outbound/app/tenant/oauthtoken/batch/upload',
},
outboundSCIM: {
create: '/v1/mgmt/outbound/scim/create',
update: '/v1/mgmt/outbound/scim/update',
delete: '/v1/mgmt/outbound/scim/delete',
load: '/v1/mgmt/outbound/scim',
setEnabled: '/v1/mgmt/outbound/scim/enabled/set',
},
sso: {
settings: '/v1/mgmt/sso/settings',
settingsNew: '/v1/mgmt/sso/settings/new',
Expand Down
33 changes: 33 additions & 0 deletions lib/management/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,39 @@ export type BatchUploadOutboundAppTokensResponse = {
failures: OutboundAppTokenUploadFailure[];
};

/**
* Configuration for an outbound SCIM provisioning integration. Bound to an outbound
* application (`appId`); `configuration` holds the provider-specific settings blob.
*/
export type OutboundSCIMConfiguration = {
appId: string;
configuration: Record<string, unknown>;
Comment thread
dorsha marked this conversation as resolved.
Outdated
enabled: boolean;
lastExportTime: number;
lastProcessingTime: number;
failures: number;
version: number;
};

/**
* Request body for creating an outbound SCIM configuration on a federated SSO
* application. The connector name is derived server-side from the app.
*/
export type CreateOutboundSCIMConfigurationRequest = {
appId: string;
configuration: Record<string, unknown>;
};

/**
* Request body for updating an outbound SCIM configuration. `version` is required for
* optimistic concurrency; the update fails if it does not match the currently stored version.
*/
export type UpdateOutboundSCIMConfigurationRequest = {
appId: string;
configuration: Record<string, unknown>;
version: number;
};

export type ManagementFlowOptions = {
input?: Record<string, any>;
preview?: boolean;
Expand Down
Loading