Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
699 changes: 32 additions & 667 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
"format": "prettier --write \"{src,__tests__}/**/*.{js,ts,tsx}\""
},
"dependencies": {
"@workos-inc/node": "^7.41.0",
"iron-session": "^8.0.1",
"jose": "^5.2.3"
"jose": "^5.2.3",
"tslib": "^2.8.1",
"valibot": "^1.2.0"
},
Comment on lines 29 to 35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 @workos-inc/node dropped from runtime dependencies

@workos-inc/node was removed from dependencies and kept only in devDependencies, but workos.ts does import { WorkOS } from '@workos-inc/node' — a non-type, runtime import. Any consumer who installs this package fresh (without @workos-inc/node in their own project) will get a module-not-found error at runtime.

The PR description says this was a version bump (^7.41.0^8.9.0), not a removal, so this looks like an unintentional regression. It should either be restored to dependencies or, if the intent is for apps to manage the version themselves, explicitly added to peerDependencies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — restored in 7bd2c83. @workos-inc/node@^8.9.0 is back in dependencies, and I also realigned the devDependencies pin from ^8.13.0 to ^8.9.0 so both entries reference the same range.

"peerDependencies": {
"react": "^18.0 || ^19.0.0",
Expand All @@ -42,7 +43,7 @@
"@types/jest": "^29.5.14",
"@types/node": "^24.10.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@workos-inc/node": "^7.77.0",
"@workos-inc/node": "^8.13.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-require-extensions": "^0.1.3",
Expand Down
39 changes: 31 additions & 8 deletions src/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,34 @@ jest.mock('react-router', () => {
describe('auth', () => {
beforeEach(() => {
jest.spyOn(authorizationUrl, 'getAuthorizationUrl');
getConfig.mockImplementation((key: string) => {
const map: Record<string, unknown> = {
clientId: 'client_1234567890',
redirectUri: 'http://localhost:5173/callback',
cookiePassword: 'kR620keEzOIzPThfnMEAba8XYgKdQ5vg',
apiKey: 'sk_test_1234567890',
cookieName: 'wos-session',
};
return map[key];
});
});

describe('getSignInUrl', () => {
it('should return a URL', async () => {
expect(await getSignInUrl('/test')).toMatch(/^https:\/\/api\.workos\.com/);
it('returns a URL and a PKCE Set-Cookie header', async () => {
const result = await getSignInUrl('/test');
expect(result.url).toMatch(/^https:\/\/api\.workos\.com/);
expect(result.headers['Set-Cookie']).toMatch(/^wos-auth-verifier-/);
expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalledWith(
expect.objectContaining({ returnPathname: '/test', screenHint: 'sign-in' }),
);
});
});

describe('getSignUpUrl', () => {
it('should return a URL', async () => {
expect(await getSignUpUrl()).toMatch(/^https:\/\/api\.workos\.com/);
it('returns a URL and a PKCE Set-Cookie header', async () => {
const result = await getSignUpUrl();
expect(result.url).toMatch(/^https:\/\/api\.workos\.com/);
expect(result.headers['Set-Cookie']).toMatch(/^wos-auth-verifier-/);
expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalledWith(
expect.objectContaining({ screenHint: 'sign-up' }),
);
Expand Down Expand Up @@ -187,36 +201,45 @@ describe('auth', () => {

it('should redirect to authorization URL for SSO_required errors', async () => {
const authUrl = 'https://api.workos.com/sso/authorize';
const pkceHeaders = { 'Set-Cookie': 'wos-auth-verifier-abc=sealed; Path=/' };
const errorWithSSOCause = new Error('SSO Required', {
cause: { error: 'sso_required' },
});

refreshSession.mockRejectedValueOnce(errorWithSSOCause);
(authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce(authUrl);
(authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce({
url: authUrl,
headers: pkceHeaders,
});

const result = await switchToOrganization(request, organizationId);

expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalled();
expect(redirect).toHaveBeenCalledWith(authUrl);
expect(redirect).toHaveBeenCalledWith(authUrl, { headers: pkceHeaders });

assertIsResponse(result);
expect(result.status).toBe(302);
expect(result.headers.get('Location')).toBe(authUrl);
expect(result.headers.get('Set-Cookie')).toBe(pkceHeaders['Set-Cookie']);
});

it('should handle mfa_enrollment errors', async () => {
const authUrl = 'https://api.workos.com/sso/authorize';
const pkceHeaders = { 'Set-Cookie': 'wos-auth-verifier-abc=sealed; Path=/' };
const errorWithMFACause = new Error('MFA Enrollment Required', {
cause: { error: 'mfa_enrollment' },
});

refreshSession.mockRejectedValueOnce(errorWithMFACause);
(authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce(authUrl);
(authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce({
url: authUrl,
headers: pkceHeaders,
});

const result = await switchToOrganization(request, organizationId);

expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalled();
expect(redirect).toHaveBeenCalledWith(authUrl);
expect(redirect).toHaveBeenCalledWith(authUrl, { headers: pkceHeaders });

assertIsResponse(result);
expect(result.status).toBe(302);
Expand Down
25 changes: 21 additions & 4 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { LoaderFunctionArgs, data, redirect } from 'react-router';
import { getAuthorizationUrl } from './get-authorization-url.js';
import { getClaimsFromAccessToken, getSessionFromCookie, refreshSession, terminateSession } from './session.js';
import { NoUserInfo, UserInfo } from './interfaces.js';
import { GetAuthURLResult, NoUserInfo, UserInfo } from './interfaces.js';
import { getConfig } from './config.js';

export async function getSignInUrl(returnPathname?: string) {
/**
* Build a sign-in URL and the short-lived PKCE / CSRF cookie that must travel
* back to the browser on the redirect.
*
* @example
* const { url, headers } = await getSignInUrl('/dashboard');
* return redirect(url, { headers });
*/
export async function getSignInUrl(returnPathname?: string): Promise<GetAuthURLResult> {
return getAuthorizationUrl({ returnPathname, screenHint: 'sign-in' });
}

export async function getSignUpUrl(returnPathname?: string) {
/**
* Build a sign-up URL and the short-lived PKCE / CSRF cookie that must travel
* back to the browser on the redirect.
*
* @example
* const { url, headers } = await getSignUpUrl('/welcome');
* return redirect(url, { headers });
*/
export async function getSignUpUrl(returnPathname?: string): Promise<GetAuthURLResult> {
return getAuthorizationUrl({ returnPathname, screenHint: 'sign-up' });
}

Expand Down Expand Up @@ -121,7 +137,8 @@ export async function switchToOrganization(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorCause: any = error instanceof Error ? error.cause : null;
if (errorCause?.error === 'sso_required' || errorCause?.error === 'mfa_enrollment') {
return redirect(await getAuthorizationUrl({ organizationId }));
const { url, headers } = await getAuthorizationUrl({ organizationId });
return redirect(url, { headers });
}

return data(
Expand Down
Loading