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
5 changes: 5 additions & 0 deletions .changeset/metal-colts-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/nextjs': patch
---

Stabilize the SDK
6 changes: 4 additions & 2 deletions packages/javascript/src/AsgardeoJavaScriptClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ abstract class AsgardeoJavaScriptClient<T = Config> implements AsgardeoClient<T>

abstract isSignedIn(): Promise<boolean>;

abstract getConfiguration(): T;

abstract signIn(
options?: SignInOptions,
sessionId?: string,
Expand All @@ -58,11 +60,11 @@ abstract class AsgardeoJavaScriptClient<T = Config> implements AsgardeoClient<T>
onSignInSuccess?: (afterSignInUrl: string) => void,
): Promise<User>;

abstract signOut(options?: SignOutOptions, afterSignOut?: (redirectUrl: string) => void): Promise<string>;
abstract signOut(options?: SignOutOptions, afterSignOut?: (afterSignOutUrl: string) => void): Promise<string>;
abstract signOut(
options?: SignOutOptions,
sessionId?: string,
afterSignOut?: (redirectUrl: string) => void,
afterSignOut?: (afterSignOutUrl: string) => void,
): Promise<string>;

abstract signUp(options?: SignUpOptions): Promise<void>;
Expand Down
120 changes: 120 additions & 0 deletions packages/javascript/src/api/__tests__/getScim2Me.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {describe, it, expect, vi} from 'vitest';
import getScim2Me from '../getScim2Me';
import AsgardeoAPIError from '../../../errors/AsgardeoAPIError';

Check failure on line 21 in packages/javascript/src/api/__tests__/getScim2Me.test.ts

View workflow job for this annotation

GitHub Actions / 👾 Unit Test (TESTING) (lts/*)

src/api/__tests__/getScim2Me.test.ts

Error: Cannot find module '../../../errors/AsgardeoAPIError' imported from '/home/runner/work/web-ui-sdks/web-ui-sdks/packages/javascript/src/api/__tests__/getScim2Me.test.ts' ❯ src/api/__tests__/getScim2Me.test.ts:21:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../../errors/AsgardeoAPIError (resolved id: ../../../errors/AsgardeoAPIError) in /home/runner/work/web-ui-sdks/web-ui-sdks/packages/javascript/src/api/__tests__/getScim2Me.test.ts. Does the file exist? ❯ loadAndTransform ../../node_modules/.pnpm/vite@6.3.5_@types+node@22.15.30_jiti@2.4.2_lightningcss@1.30.1_sass@1.89.0_terser@5.39.2_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

// Mock user data
const mockUser = {
id: '123',
username: 'testuser',
email: 'test@example.com',
givenName: 'Test',
familyName: 'User',
};

describe('getScim2Me', () => {
it('should fetch user profile successfully with default fetch', async () => {
// Mock fetch
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
json: () => Promise.resolve(mockUser),
text: () => Promise.resolve(JSON.stringify(mockUser)),
});

// Replace global fetch
global.fetch = mockFetch;

const result = await getScim2Me({
url: 'https://api.asgardeo.io/t/test/scim2/Me',
});

expect(result).toEqual(mockUser);
expect(mockFetch).toHaveBeenCalledWith('https://api.asgardeo.io/t/test/scim2/Me', {
method: 'GET',
headers: {
'Content-Type': 'application/scim+json',
Accept: 'application/json',
},
});
});

it('should use custom fetcher when provided', async () => {
const customFetcher = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
json: () => Promise.resolve(mockUser),
text: () => Promise.resolve(JSON.stringify(mockUser)),
});

const result = await getScim2Me({
url: 'https://api.asgardeo.io/t/test/scim2/Me',
fetcher: customFetcher,
});

expect(result).toEqual(mockUser);
expect(customFetcher).toHaveBeenCalledWith('https://api.asgardeo.io/t/test/scim2/Me', {
method: 'GET',
headers: {
'Content-Type': 'application/scim+json',
Accept: 'application/json',
},
});
});

it('should throw AsgardeoAPIError for invalid URL', async () => {
await expect(
getScim2Me({
url: 'invalid-url',
}),
).rejects.toThrow(AsgardeoAPIError);
});

it('should throw AsgardeoAPIError for failed response', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
text: () => Promise.resolve('User not found'),
});

global.fetch = mockFetch;

await expect(
getScim2Me({
url: 'https://api.asgardeo.io/t/test/scim2/Me',
}),
).rejects.toThrow(AsgardeoAPIError);
});

it('should handle network errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));

global.fetch = mockFetch;

await expect(
getScim2Me({
url: 'https://api.asgardeo.io/t/test/scim2/Me',
}),
).rejects.toThrow(AsgardeoAPIError);
});
});
149 changes: 149 additions & 0 deletions packages/javascript/src/api/getScim2Me.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {User} from '../models/user';
import AsgardeoAPIError from '../errors/AsgardeoAPIError';

/**
* Configuration for the getScim2Me request
*/
export interface GetScim2MeConfig extends Omit<RequestInit, 'method'> {
/**
* The absolute API endpoint.
*/
url?: string;
/**
* The base path of the API endpoint.
*/
baseUrl?: string;
/**
* Optional custom fetcher function.
* If not provided, native fetch will be used
*/
fetcher?: (url: string, config: RequestInit) => Promise<Response>;
}

/**
* Retrieves the user profile information from the specified SCIM2 /Me endpoint.
*
* @param config - Request configuration object.
* @returns A promise that resolves with the user profile information.
* @example
* ```typescript
* // Using default fetch
* try {
* const userProfile = await getScim2Me({
* url: "https://api.asgardeo.io/t/<ORGANIZATION>/scim2/Me",
* });
* console.log(userProfile);
* } catch (error) {
* if (error instanceof AsgardeoAPIError) {
* console.error('Failed to get user profile:', error.message);
* }
* }
* ```
*
* @example
* ```typescript
* // Using custom fetcher (e.g., axios-based httpClient)
* try {
* const userProfile = await getScim2Me({
* url: "https://api.asgardeo.io/t/<ORGANIZATION>/scim2/Me",
* fetcher: async (url, config) => {
* const response = await httpClient({
* url,
* method: config.method,
* headers: config.headers,
* ...config
* });
* // Convert axios-like response to fetch-like Response
* return {
* ok: response.status >= 200 && response.status < 300,
* status: response.status,
* statusText: response.statusText,
* json: () => Promise.resolve(response.data),
* text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data))
* } as Response;
* }
* });
* console.log(userProfile);
* } catch (error) {
* if (error instanceof AsgardeoAPIError) {
* console.error('Failed to get user profile:', error.message);
* }
* }
* ```
*/
const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeConfig): Promise<User> => {
try {
new URL(url ?? baseUrl);
} catch (error) {
throw new AsgardeoAPIError(
`Invalid URL provided. ${error?.toString()}`,
'getScim2Me-ValidationError-001',
'javascript',
400,
'The provided `url` or `baseUrl` path does not adhere to the URL schema.',
);
}

const fetchFn = fetcher || fetch;
const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me`

const requestInit: RequestInit = {
method: 'GET',
headers: {
'Content-Type': 'application/scim+json',
Accept: 'application/json',
...requestConfig.headers,
},
...requestConfig,
};

try {
const response: Response = await fetchFn(resolvedUrl, requestInit);

if (!response?.ok) {
const errorText = await response.text();

throw new AsgardeoAPIError(
`Failed to fetch user profile: ${errorText}`,
'getScim2Me-ResponseError-001',
'javascript',
response.status,
response.statusText,
);
}

return (await response.json()) as User;
} catch (error) {
if (error instanceof AsgardeoAPIError) {
throw error;
}

throw new AsgardeoAPIError(
`Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`,
'getScim2Me-NetworkError-001',
'javascript',
0,
'Network Error',
);
}
};

export default getScim2Me;
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions packages/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {default as initializeEmbeddedSignInFlow} from './api/initializeEmbeddedS
export {default as executeEmbeddedSignInFlow} from './api/executeEmbeddedSignInFlow';
export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpFlow';
export {default as getUserInfo} from './api/getUserInfo';
export {default as getScim2Me, GetScim2MeConfig} from './api/getScim2Me';

export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants';
export {default as TokenConstants} from './constants/TokenConstants';
Expand Down
6 changes: 4 additions & 2 deletions packages/javascript/src/models/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface AsgardeoClient<T> {
*/
switchOrganization(organization: Organization): Promise<void>;

getConfiguration(): T;

/**
* Gets user information from the session.
*
Expand Down Expand Up @@ -132,7 +134,7 @@ export interface AsgardeoClient<T> {
* @param afterSignOut - Callback function to be executed after sign-out is complete.
* @returns A promise that resolves to true if sign-out is successful
*/
signOut(options?: SignOutOptions, afterSignOut?: (redirectUrl: string) => void): Promise<string>;
signOut(options?: SignOutOptions, afterSignOut?: (afterSignOutUrl: string) => void): Promise<string>;

/**
* Signs out the currently signed-in user with an optional session ID.
Expand All @@ -143,7 +145,7 @@ export interface AsgardeoClient<T> {
* @param afterSignOut - Callback function to be executed after sign-out is complete.
* @returns A promise that resolves to true if sign-out is successful
*/
signOut(options?: SignOutOptions, sessionId?: string, afterSignOut?: (redirectUrl: string) => void): Promise<string>;
signOut(options?: SignOutOptions, sessionId?: string, afterSignOut?: (afterSignOutUrl: string) => void): Promise<string>;

/**
* Initiates a redirection-based sign-up process for the user.
Expand Down
15 changes: 15 additions & 0 deletions packages/javascript/src/models/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ export interface BaseConfig<T = unknown> extends WithPreferences {
* scopes: ["openid", "profile", "email"]
*/
scopes?: string | string[] | undefined;

/**
* Optional URL to redirect the user to sign-in.
* By default, this will be the sign-in page of Asgardeo.
* If you want to use a custom sign-in page, you can provide the URL here and use the `SignIn` component to render it.
*/
signInUrl?: string | undefined;

/**
* Optional URL to redirect the user to sign-up.
* By default, this will be the sign-up page of Asgardeo.
* If you want to use a custom sign-up page, you can provide the URL here
* and use the `SignUp` component to render it.
*/
signUpUrl?: string | undefined;
}

export interface WithPreferences {
Expand Down
Loading
Loading