Skip to content

Commit 7851f63

Browse files
committed
fixing types
1 parent 496b263 commit 7851f63

File tree

8 files changed

+111
-48
lines changed

8 files changed

+111
-48
lines changed

package-lock.json

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
"jose": "^5.2.3"
2929
},
3030
"peerDependencies": {
31-
"react-router": "^7.2.0",
3231
"react": "^18.0 || ^19.0.0",
33-
"react-dom": "^18.0 || ^19.0.0"
32+
"react-dom": "^18.0 || ^19.0.0",
33+
"react-router": "^7.2.0"
3434
},
3535
"devDependencies": {
3636
"@testing-library/jest-dom": "^6.6.3",
@@ -44,6 +44,7 @@
4444
"jest": "^29.7.0",
4545
"jest-environment-jsdom": "^29.7.0",
4646
"prettier": "^3.3.3",
47+
"react-router": "^7.3.0",
4748
"ts-jest": "^29.2.5",
4849
"ts-node": "^10.9.2",
4950
"typescript": "^5.4.2",

src/authkit-callback-route.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { LoaderFunction } from 'react-router';
21
import { getWorkOS } from './workos.js';
32
import { authLoader } from './authkit-callback-route.js';
43
import {
@@ -7,6 +6,8 @@ import {
76
assertIsResponse,
87
} from './test-utils/test-helpers.js';
98
import { configureSessionStorage } from './sessionStorage.js';
9+
import { isDataWithResponseInit } from './utils.js';
10+
import { DataWithResponseInit } from './interfaces.js';
1011

1112
// Mock dependencies
1213
const fakeWorkosInstance = {
@@ -21,7 +22,7 @@ jest.mock('./workos.js', () => ({
2122
}));
2223

2324
describe('authLoader', () => {
24-
let loader: LoaderFunction;
25+
let loader: ReturnType<typeof authLoader>;
2526
let request: Request;
2627
const workos = getWorkOS();
2728
const authenticateWithCode = jest.mocked(workos.userManagement.authenticateWithCode);
@@ -58,17 +59,19 @@ describe('authLoader', () => {
5859
it('should handle authentication failure', async () => {
5960
authenticateWithCode.mockRejectedValue(new Error('Auth failed'));
6061
request = createRequestWithSearchParams(request, { code: 'invalid-code' });
61-
const response = (await loader({ request, params: {}, context: {} })) as Response;
62+
const response = (await loader({ request, params: {}, context: {} })) as DataWithResponseInit<unknown>;
63+
expect(isDataWithResponseInit(response)).toBeTruthy();
6264

63-
expect(response.status).toBe(500);
65+
expect(response?.init?.status).toBe(500);
6466
});
6567

6668
it('should handle authentication failure with string error', async () => {
6769
authenticateWithCode.mockRejectedValue('Auth failed');
6870
request = createRequestWithSearchParams(request, { code: 'invalid-code' });
69-
const response = (await loader({ request, params: {}, context: {} })) as Response;
71+
const response = (await loader({ request, params: {}, context: {} })) as DataWithResponseInit<unknown>;
72+
expect(isDataWithResponseInit(response)).toBeTruthy();
7073

71-
expect(response.status).toBe(500);
74+
expect(response?.init?.status).toBe(500);
7275
});
7376
});
7477

src/authkit-callback-route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LoaderFunctionArgs, data as json, redirect } from 'react-router';
1+
import { LoaderFunctionArgs, data, redirect } from 'react-router';
22
import { getConfig } from './config.js';
33
import { HandleAuthOptions } from './interfaces.js';
44
import { encryptSession } from './session.js';
@@ -86,7 +86,7 @@ export function authLoader(options: HandleAuthOptions = {}) {
8686
}
8787

8888
function errorResponse() {
89-
return json(
89+
return data(
9090
{
9191
error: {
9292
message: 'Something went wrong',

src/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { SessionStorage, SessionIdStorageStrategy } from 'react-router';
1+
import type { SessionStorage, SessionIdStorageStrategy, data } from 'react-router';
22
import type { OauthTokens, User } from '@workos-inc/node';
33

4+
export type DataWithResponseInit<T> = ReturnType<typeof data<T>>;
5+
46
export interface HandleAuthOptions {
57
returnPathname?: string;
68
onSuccess?: (data: AuthLoaderSuccessData) => void | Promise<void>;

src/session.spec.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ const getSessionStorage = jest.mocked(getSessionStorageMock);
4040
const configureSessionStorage = jest.mocked(configureSessionStorageMock);
4141
const jwtVerify = jest.mocked(jose.jwtVerify);
4242

43+
function getHeaderValue(headers: HeadersInit | undefined, name: string): string | null {
44+
if (!headers) {
45+
return null;
46+
}
47+
48+
if (headers instanceof Headers) {
49+
return headers.get(name);
50+
}
51+
52+
if (Array.isArray(headers)) {
53+
const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
54+
return pair?.[1] ?? null;
55+
}
56+
57+
return headers[name] ?? null;
58+
}
59+
4360
jest.mock('jose', () => ({
4461
createRemoteJWKSet: jest.fn(),
4562
jwtVerify: jest.fn(),
@@ -222,8 +239,7 @@ describe('session', () => {
222239
});
223240

224241
it('should return unauthorized data when no session exists', async () => {
225-
const response = await authkitLoader(createLoaderArgs(createMockRequest()));
226-
const data = await response.json();
242+
const { data } = await authkitLoader(createLoaderArgs(createMockRequest()));
227243

228244
expect(data).toEqual({
229245
user: null,
@@ -256,11 +272,14 @@ describe('session', () => {
256272
});
257273
const customLoader = jest.fn().mockReturnValue(redirectResponse);
258274

259-
const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
260-
261-
expect(response.status).toBe(302);
262-
expect(response.headers.get('Location')).toBe('/dashboard');
263-
expect(response.headers.get('X-Redirect-Reason')).toBe('test');
275+
try {
276+
await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
277+
} catch (response: unknown) {
278+
assertIsResponse(response);
279+
expect(response.status).toBe(302);
280+
expect(response.headers.get('Location')).toEqual('/dashboard');
281+
expect(response.headers.get('X-Redirect-Reason')).toEqual('test');
282+
}
264283
});
265284
});
266285

@@ -303,8 +322,7 @@ describe('session', () => {
303322
});
304323

305324
it('should return authorized data with session claims', async () => {
306-
const response = await authkitLoader(createLoaderArgs(createMockRequest()));
307-
const data = await response.json();
325+
const { data } = await authkitLoader(createLoaderArgs(createMockRequest()));
308326

309327
expect(data).toEqual({
310328
user: mockSessionData.user,
@@ -325,8 +343,7 @@ describe('session', () => {
325343
metadata: { key: 'value' },
326344
});
327345

328-
const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
329-
const data = await response.json();
346+
const { data } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
330347

331348
expect(data).toEqual(
332349
expect.objectContaining({
@@ -349,12 +366,11 @@ describe('session', () => {
349366
}),
350367
);
351368

352-
const response = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
369+
const { data, init } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
353370

354-
expect(response.headers.get('Custom-Header')).toBe('test-header');
355-
expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8');
371+
expect(getHeaderValue(init?.headers, 'Custom-Header')).toBe('test-header');
372+
expect(getHeaderValue(init?.headers, 'Content-Type')).toBe('application/json; charset=utf-8');
356373

357-
const data = await response.json();
358374
expect(data).toEqual(
359375
expect.objectContaining({
360376
customData: 'test-value',
@@ -437,8 +453,7 @@ describe('session', () => {
437453
});
438454

439455
it('should refresh session when access token is invalid', async () => {
440-
const response = await authkitLoader(createLoaderArgs(createMockRequest()));
441-
const data = await response.json();
456+
const { data, init } = await authkitLoader(createLoaderArgs(createMockRequest()));
442457

443458
// Verify the refresh token flow was triggered
444459
expect(authenticateWithRefreshToken).toHaveBeenCalledWith({
@@ -459,7 +474,7 @@ describe('session', () => {
459474
);
460475

461476
// Verify cookie was set
462-
expect(response.headers.get('Set-Cookie')).toBe('new-session-cookie');
477+
expect(getHeaderValue(init?.headers, 'Set-Cookie')).toBe('new-session-cookie');
463478
});
464479

465480
it('should redirect to root when refresh fails', async () => {

src/session.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { data as json, redirect, type LoaderFunctionArgs, type SessionData } from 'react-router';
1+
import { data, redirect, type LoaderFunctionArgs, type SessionData } from 'react-router';
22
import { getAuthorizationUrl } from './get-authorization-url.js';
3-
import type { AccessToken, AuthKitLoaderOptions, AuthorizedData, Session, UnauthorizedData } from './interfaces.js';
3+
import type {
4+
AccessToken,
5+
AuthKitLoaderOptions,
6+
AuthorizedData,
7+
DataWithResponseInit,
8+
Session,
9+
UnauthorizedData,
10+
} from './interfaces.js';
411
import { getWorkOS } from './workos.js';
512

613
import { sealData, unsealData } from 'iron-session';
@@ -94,24 +101,24 @@ type AuthorizedAuthLoader<Data> = (args: LoaderFunctionArgs & { auth: Authorized
94101
async function authkitLoader(
95102
loaderArgs: LoaderFunctionArgs,
96103
options: AuthKitLoaderOptions & { ensureSignedIn: true },
97-
): Promise<TypedResponse<AuthorizedData>>;
104+
): Promise<DataWithResponseInit<AuthorizedData> | Response>;
98105

99106
async function authkitLoader(
100107
loaderArgs: LoaderFunctionArgs,
101108
options?: AuthKitLoaderOptions,
102-
): Promise<TypedResponse<AuthorizedData | UnauthorizedData>>;
109+
): Promise<DataWithResponseInit<AuthorizedData | UnauthorizedData>>;
103110

104111
async function authkitLoader<Data = unknown>(
105112
loaderArgs: LoaderFunctionArgs,
106113
loader: AuthorizedAuthLoader<Data>,
107114
options: AuthKitLoaderOptions & { ensureSignedIn: true },
108-
): Promise<TypedResponse<Data & AuthorizedData>>;
115+
): Promise<DataWithResponseInit<Data & AuthorizedData>>;
109116

110117
async function authkitLoader<Data = unknown>(
111118
loaderArgs: LoaderFunctionArgs,
112119
loader: AuthLoader<Data>,
113120
options?: AuthKitLoaderOptions,
114-
): Promise<TypedResponse<Data & (AuthorizedData | UnauthorizedData)>>;
121+
): Promise<DataWithResponseInit<Data & (AuthorizedData | UnauthorizedData)>>;
115122

116123
async function authkitLoader<Data = unknown>(
117124
loaderArgs: LoaderFunctionArgs,
@@ -195,7 +202,7 @@ async function handleAuthLoader(
195202
session?: Session,
196203
) {
197204
if (!loader) {
198-
return json(auth, session ? { headers: { ...session.headers } } : undefined);
205+
return data(auth, session ? { headers: { ...session.headers } } : undefined);
199206
}
200207

201208
// If there's a custom loader, get the resulting data and return it with our
@@ -209,7 +216,7 @@ async function handleAuthLoader(
209216
}
210217

211218
const newResponse = new Response(loaderResult.body, loaderResult);
212-
const data = await newResponse.json();
219+
const responseData = await newResponse.json();
213220

214221
// Set the content type in case the user returned a Response instead of the
215222
// json helper method
@@ -218,12 +225,12 @@ async function handleAuthLoader(
218225
newResponse.headers.append('Set-Cookie', session.headers['Set-Cookie']);
219226
}
220227

221-
return json({ ...data, ...auth }, newResponse);
228+
return data({ ...responseData, ...auth }, newResponse);
222229
}
223230

224231
// If the loader returns a non-Response, assume it's a data object
225232
// istanbul ignore next
226-
return json({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined);
233+
return data({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined);
227234
}
228235

229236
async function terminateSession(request: Request) {

src/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { DataWithResponseInit } from './interfaces.js';
2+
13
/**
24
* Returns a function that can only be called once.
35
* Subsequent calls will return the result of the first call.
@@ -16,3 +18,35 @@ export function lazy<T>(fn: () => T): () => T {
1618
return result;
1719
};
1820
}
21+
22+
/**
23+
* Returns true if the response is a redirect.
24+
* @param res - The response to check.
25+
* @returns True if the response is a redirect.
26+
*/
27+
export function isRedirect(res: Response) {
28+
return res.status >= 300 && res.status < 400;
29+
}
30+
31+
/**
32+
* Returns true if the response is a response.
33+
* @param response - The response to check.
34+
* @returns True if the response is a response.
35+
*/
36+
export function isResponse(response: unknown): response is Response {
37+
return response instanceof Response;
38+
}
39+
40+
/**
41+
* Returns true if the data is a DataWithResponseInit object.
42+
*/
43+
export function isDataWithResponseInit(data: unknown): data is DataWithResponseInit<unknown> {
44+
return (
45+
typeof data === 'object' &&
46+
data != null &&
47+
'type' in data &&
48+
'data' in data &&
49+
'init' in data &&
50+
data.type === 'DataWithResponseInit'
51+
);
52+
}

0 commit comments

Comments
 (0)