@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -102
- package/dist/esm/actions.js +35 -5
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +71 -21
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +90 -92
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +36 -15
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +17 -15
- package/dist/esm/components/impersonation.js.map +1 -1
- package/dist/esm/components/min-max-button.js +1 -1
- package/dist/esm/components/min-max-button.js.map +1 -1
- package/dist/esm/components/tokenStore.js +28 -19
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +1 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/components/useTokenClaims.js +1 -1
- package/dist/esm/components/useTokenClaims.js.map +1 -1
- package/dist/esm/cookie.js +20 -5
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/env-variables.js +6 -6
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +36 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +51 -12
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces.js +7 -1
- package/dist/esm/interfaces.js.map +1 -1
- package/dist/esm/middleware-helpers.js +102 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +52 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +82 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +1 -1
- package/dist/esm/test-helpers.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +7 -15
- package/dist/esm/types/components/authkit-provider.d.ts +6 -2
- package/dist/esm/types/components/impersonation.d.ts +2 -1
- package/dist/esm/types/cookie.d.ts +9 -0
- package/dist/esm/types/env-variables.d.ts +2 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/get-authorization-url.d.ts +2 -2
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/interfaces.d.ts +12 -0
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +27 -0
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +17 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/utils.d.ts +5 -0
- package/dist/esm/types/validate-api-key.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +10 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +16 -0
- package/dist/esm/validate-api-key.js.map +1 -0
- package/dist/esm/workos.js +1 -1
- package/package.json +33 -34
- package/src/actions.spec.ts +91 -18
- package/src/actions.ts +44 -6
- package/src/auth.spec.ts +79 -29
- package/src/auth.ts +74 -42
- package/src/authkit-callback-route.spec.ts +372 -58
- package/src/authkit-callback-route.ts +121 -103
- package/src/components/authkit-provider.spec.tsx +264 -70
- package/src/components/authkit-provider.tsx +40 -15
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +152 -35
- package/src/components/impersonation.tsx +37 -30
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +59 -44
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +82 -83
- package/src/components/useTokenClaims.spec.tsx +23 -22
- package/src/cookie.spec.ts +63 -9
- package/src/cookie.ts +35 -0
- package/src/env-variables.ts +2 -0
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +170 -15
- package/src/get-authorization-url.ts +69 -23
- package/src/index.ts +20 -2
- package/src/interfaces.ts +15 -0
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +238 -0
- package/src/middleware-helpers.ts +134 -0
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +146 -0
- package/src/pkce.ts +59 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +104 -27
- package/src/test-helpers.ts +1 -1
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +9 -0
- package/src/validate-api-key.spec.ts +111 -0
- package/src/validate-api-key.ts +19 -0
- package/src/workos.spec.ts +2 -2
- package/src/workos.ts +1 -1
package/src/auth.spec.ts
CHANGED
|
@@ -1,57 +1,63 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
2
|
-
|
|
3
1
|
import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js';
|
|
4
2
|
import * as session from './session.js';
|
|
5
3
|
import * as cache from 'next/cache';
|
|
6
4
|
import * as workosModule from './workos.js';
|
|
7
5
|
|
|
8
|
-
// These are mocked in
|
|
6
|
+
// These are mocked in vitest.setup.ts
|
|
9
7
|
import { cookies, headers } from 'next/headers';
|
|
10
8
|
import { redirect } from 'next/navigation';
|
|
11
9
|
import { generateSession, generateTestToken } from './test-helpers.js';
|
|
12
10
|
import { sealData } from 'iron-session';
|
|
13
11
|
import { getWorkOS } from './workos.js';
|
|
12
|
+
import { getStateFromPKCECookieValue } from './pkce.js';
|
|
14
13
|
|
|
15
14
|
const workos = getWorkOS();
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
const actual =
|
|
16
|
+
vi.mock('next/cache', async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof cache>('next/cache');
|
|
19
18
|
return {
|
|
20
19
|
...actual,
|
|
21
|
-
revalidateTag:
|
|
22
|
-
revalidatePath:
|
|
20
|
+
revalidateTag: vi.fn(),
|
|
21
|
+
revalidatePath: vi.fn(),
|
|
23
22
|
};
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
// Create a fake WorkOS instance that will be used only in the "on error" tests
|
|
27
26
|
const fakeWorkosInstance = {
|
|
28
27
|
userManagement: {
|
|
29
|
-
authenticateWithRefreshToken:
|
|
30
|
-
getAuthorizationUrl:
|
|
31
|
-
getJwksUrl:
|
|
32
|
-
getLogoutUrl:
|
|
28
|
+
authenticateWithRefreshToken: vi.fn(),
|
|
29
|
+
getAuthorizationUrl: vi.fn(),
|
|
30
|
+
getJwksUrl: vi.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
|
|
31
|
+
getLogoutUrl: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
pkce: {
|
|
34
|
+
generate: vi.fn().mockResolvedValue({
|
|
35
|
+
codeVerifier: 'test-code-verifier',
|
|
36
|
+
codeChallenge: 'test-code-challenge',
|
|
37
|
+
codeChallengeMethod: 'S256' as const,
|
|
38
|
+
}),
|
|
33
39
|
},
|
|
34
40
|
};
|
|
35
41
|
|
|
36
|
-
const revalidatePath =
|
|
37
|
-
const revalidateTag =
|
|
42
|
+
const revalidatePath = vi.mocked(cache.revalidatePath);
|
|
43
|
+
const revalidateTag = vi.mocked(cache.revalidateTag);
|
|
38
44
|
// We'll only use these in the "on error" tests
|
|
39
45
|
const authenticateWithRefreshToken = fakeWorkosInstance.userManagement.authenticateWithRefreshToken;
|
|
40
46
|
const getAuthorizationUrl = fakeWorkosInstance.userManagement.getAuthorizationUrl;
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
const actual =
|
|
48
|
+
vi.mock('../src/session', async () => {
|
|
49
|
+
const actual = await vi.importActual<typeof session>('../src/session');
|
|
44
50
|
|
|
45
51
|
return {
|
|
46
52
|
...actual,
|
|
47
|
-
refreshSession:
|
|
53
|
+
refreshSession: vi.fn(actual.refreshSession),
|
|
48
54
|
};
|
|
49
55
|
});
|
|
50
56
|
|
|
51
57
|
describe('auth.ts', () => {
|
|
52
58
|
beforeEach(async () => {
|
|
53
59
|
// Clear all mocks between tests
|
|
54
|
-
|
|
60
|
+
vi.clearAllMocks();
|
|
55
61
|
|
|
56
62
|
// Reset the cookie store
|
|
57
63
|
const nextCookies = await cookies();
|
|
@@ -76,6 +82,15 @@ describe('auth.ts', () => {
|
|
|
76
82
|
expect(url).toBeDefined();
|
|
77
83
|
expect(() => new URL(url)).not.toThrow();
|
|
78
84
|
});
|
|
85
|
+
|
|
86
|
+
it('should include returnTo as returnPathname in the state parameter', async () => {
|
|
87
|
+
const url = await getSignInUrl({ returnTo: '/dashboard' });
|
|
88
|
+
const parsedUrl = new URL(url);
|
|
89
|
+
const state = parsedUrl.searchParams.get('state');
|
|
90
|
+
expect(state).toBeDefined();
|
|
91
|
+
const decoded = await getStateFromPKCECookieValue(state!);
|
|
92
|
+
expect(decoded.returnPathname).toBe('/dashboard');
|
|
93
|
+
});
|
|
79
94
|
});
|
|
80
95
|
|
|
81
96
|
it('should not include prompt when not specified for getSignInUrl', async () => {
|
|
@@ -103,6 +118,15 @@ describe('auth.ts', () => {
|
|
|
103
118
|
const url = await getSignUpUrl({ prompt: 'consent' });
|
|
104
119
|
expect(url).toContain('prompt=consent');
|
|
105
120
|
});
|
|
121
|
+
|
|
122
|
+
it('should include returnTo as returnPathname in the state parameter', async () => {
|
|
123
|
+
const url = await getSignUpUrl({ returnTo: '/welcome' });
|
|
124
|
+
const parsedUrl = new URL(url);
|
|
125
|
+
const state = parsedUrl.searchParams.get('state');
|
|
126
|
+
expect(state).toBeDefined();
|
|
127
|
+
const decoded = await getStateFromPKCECookieValue(state!);
|
|
128
|
+
expect(decoded.returnPathname).toBe('/welcome');
|
|
129
|
+
});
|
|
106
130
|
});
|
|
107
131
|
|
|
108
132
|
describe('switchToOrganization', () => {
|
|
@@ -135,14 +159,21 @@ describe('auth.ts', () => {
|
|
|
135
159
|
nextHeaders.set('x-url', 'http://localhost/test');
|
|
136
160
|
await generateSession();
|
|
137
161
|
|
|
162
|
+
fakeWorkosInstance.pkce.generate.mockResolvedValue({
|
|
163
|
+
codeVerifier: 'test-code-verifier',
|
|
164
|
+
codeChallenge: 'test-code-challenge',
|
|
165
|
+
codeChallengeMethod: 'S256' as const,
|
|
166
|
+
});
|
|
167
|
+
|
|
138
168
|
// Create a WorkOS-like object that matches what our tests need
|
|
139
169
|
const mockWorkOS = {
|
|
140
170
|
userManagement: fakeWorkosInstance.userManagement,
|
|
171
|
+
pkce: fakeWorkosInstance.pkce,
|
|
141
172
|
// Add minimal properties to satisfy TypeScript
|
|
142
|
-
createHttpClient:
|
|
143
|
-
createWebhookClient:
|
|
144
|
-
createActionsClient:
|
|
145
|
-
createIronSessionProvider:
|
|
173
|
+
createHttpClient: vi.fn(),
|
|
174
|
+
createWebhookClient: vi.fn(),
|
|
175
|
+
createActionsClient: vi.fn(),
|
|
176
|
+
createIronSessionProvider: vi.fn(),
|
|
146
177
|
apiKey: 'test',
|
|
147
178
|
clientId: 'test',
|
|
148
179
|
host: 'test',
|
|
@@ -154,12 +185,12 @@ describe('auth.ts', () => {
|
|
|
154
185
|
|
|
155
186
|
// Apply the mock for these tests only
|
|
156
187
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
|
-
|
|
188
|
+
vi.spyOn(workosModule, 'getWorkOS').mockImplementation(() => mockWorkOS as any);
|
|
158
189
|
});
|
|
159
190
|
|
|
160
191
|
afterEach(() => {
|
|
161
192
|
// Restore all mocks after each test
|
|
162
|
-
|
|
193
|
+
vi.restoreAllMocks();
|
|
163
194
|
});
|
|
164
195
|
|
|
165
196
|
it('should redirect to sign in when error is "sso_required"', async () => {
|
|
@@ -243,11 +274,30 @@ describe('auth.ts', () => {
|
|
|
243
274
|
expect(sessionCookie).toBeUndefined();
|
|
244
275
|
});
|
|
245
276
|
|
|
277
|
+
it('should clear lingering PKCE verifier cookies (legacy and per-flow)', async () => {
|
|
278
|
+
const nextCookies = await cookies();
|
|
279
|
+
const nextHeaders = await headers();
|
|
280
|
+
|
|
281
|
+
nextHeaders.set('x-workos-middleware', 'true');
|
|
282
|
+
nextCookies.set('wos-session', 'foo');
|
|
283
|
+
nextCookies.set('wos-auth-verifier', 'legacy-state');
|
|
284
|
+
nextCookies.set('wos-auth-verifier-a1b2c3d4', 'flow-a-state');
|
|
285
|
+
nextCookies.set('wos-auth-verifier-deadbeef', 'flow-b-state');
|
|
286
|
+
nextCookies.set('unrelated-cookie', 'keep-me');
|
|
287
|
+
|
|
288
|
+
await signOut();
|
|
289
|
+
|
|
290
|
+
expect(nextCookies.get('wos-auth-verifier')).toBeUndefined();
|
|
291
|
+
expect(nextCookies.get('wos-auth-verifier-a1b2c3d4')).toBeUndefined();
|
|
292
|
+
expect(nextCookies.get('wos-auth-verifier-deadbeef')).toBeUndefined();
|
|
293
|
+
expect(nextCookies.get('unrelated-cookie')?.value).toBe('keep-me');
|
|
294
|
+
});
|
|
295
|
+
|
|
246
296
|
describe('when given a `returnTo` parameter', () => {
|
|
247
297
|
it('passes the `returnTo` through to the `getLogoutUrl` call', async () => {
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
|
|
298
|
+
vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
|
|
299
|
+
'https://user-management-logout.com/signed-out',
|
|
300
|
+
);
|
|
251
301
|
const mockSession = {
|
|
252
302
|
accessToken: await generateTestToken(),
|
|
253
303
|
sessionId: 'session_123',
|
|
@@ -306,9 +356,9 @@ describe('auth.ts', () => {
|
|
|
306
356
|
|
|
307
357
|
nextCookies.set('wos-session', encryptedSession);
|
|
308
358
|
|
|
309
|
-
|
|
310
|
-
.
|
|
311
|
-
|
|
359
|
+
vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
|
|
360
|
+
'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
|
|
361
|
+
);
|
|
312
362
|
|
|
313
363
|
await signOut();
|
|
314
364
|
|
package/src/auth.ts
CHANGED
|
@@ -5,41 +5,47 @@ import { revalidatePath, revalidateTag } from 'next/cache';
|
|
|
5
5
|
import { cookies, headers } from 'next/headers';
|
|
6
6
|
import { redirect } from 'next/navigation';
|
|
7
7
|
import { WORKOS_COOKIE_NAME } from './env-variables.js';
|
|
8
|
-
import { getCookieOptions } from './cookie.js';
|
|
8
|
+
import { getCookieOptions, getPKCECookieOptions } from './cookie.js';
|
|
9
9
|
import { getAuthorizationUrl } from './get-authorization-url.js';
|
|
10
|
-
import type { AccessToken, SwitchToOrganizationOptions, UserInfo } from './interfaces.js';
|
|
10
|
+
import type { AccessToken, GetAuthURLOptions, SwitchToOrganizationOptions, UserInfo } from './interfaces.js';
|
|
11
|
+
import { PKCE_COOKIE_NAME, setPKCECookie } from './pkce.js';
|
|
11
12
|
import { getSessionFromCookie, refreshSession, withAuth } from './session.js';
|
|
12
13
|
import { getWorkOS } from './workos.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A wrapper around revalidateTag to provide compatibility with previous versions.
|
|
17
|
+
* @param tag The tag to revalidate.
|
|
18
|
+
*/
|
|
19
|
+
function revalidateTagCompat(tag: string): void {
|
|
20
|
+
const fn = revalidateTag as (tag: string, profile: string) => void;
|
|
21
|
+
return fn(tag, 'max');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getAuthURLAndSetPKCECookie(options: GetAuthURLOptions): Promise<string> {
|
|
25
|
+
const { url, sealedState } = await getAuthorizationUrl(options);
|
|
26
|
+
await setPKCECookie(sealedState);
|
|
27
|
+
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type GetSignUrlOptions = Omit<GetAuthURLOptions, 'screenHint' | 'returnPathname'> & {
|
|
32
|
+
returnTo?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function getSignInUrl(authUrlOptions: GetSignUrlOptions = {}) {
|
|
36
|
+
return getAuthURLAndSetPKCECookie({
|
|
37
|
+
...authUrlOptions,
|
|
38
|
+
returnPathname: authUrlOptions.returnTo,
|
|
39
|
+
screenHint: 'sign-in',
|
|
40
|
+
});
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
export async function getSignUpUrl({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}: {
|
|
36
|
-
organizationId?: string;
|
|
37
|
-
loginHint?: string;
|
|
38
|
-
redirectUri?: string;
|
|
39
|
-
prompt?: 'consent';
|
|
40
|
-
state?: string;
|
|
41
|
-
} = {}) {
|
|
42
|
-
return getAuthorizationUrl({ organizationId, screenHint: 'sign-up', loginHint, redirectUri, prompt, state });
|
|
43
|
+
export async function getSignUpUrl(authUrlOptions: GetSignUrlOptions = {}) {
|
|
44
|
+
return getAuthURLAndSetPKCECookie({
|
|
45
|
+
...authUrlOptions,
|
|
46
|
+
returnPathname: authUrlOptions.returnTo,
|
|
47
|
+
screenHint: 'sign-up',
|
|
48
|
+
});
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/**
|
|
@@ -67,7 +73,30 @@ export async function signOut({ returnTo }: { returnTo?: string } = {}) {
|
|
|
67
73
|
const nextCookies = await cookies();
|
|
68
74
|
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
69
75
|
const { domain, path, sameSite, secure } = getCookieOptions();
|
|
70
|
-
|
|
76
|
+
try {
|
|
77
|
+
nextCookies.delete({ name: cookieName, domain, path, sameSite, secure });
|
|
78
|
+
} catch {
|
|
79
|
+
// Some environments (e.g., vinext) only accept a string cookie name
|
|
80
|
+
nextCookies.delete(cookieName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clear any lingering PKCE verifier cookies so orphans from abandoned
|
|
84
|
+
// flows don't accumulate toward HTTP 431 or confuse future sign-ins.
|
|
85
|
+
const pkceOptions = getPKCECookieOptions();
|
|
86
|
+
for (const { name } of nextCookies.getAll()) {
|
|
87
|
+
if (!name.startsWith(PKCE_COOKIE_NAME)) continue;
|
|
88
|
+
try {
|
|
89
|
+
nextCookies.delete({
|
|
90
|
+
name,
|
|
91
|
+
domain: pkceOptions.domain,
|
|
92
|
+
path: pkceOptions.path,
|
|
93
|
+
sameSite: pkceOptions.sameSite,
|
|
94
|
+
secure: pkceOptions.secure,
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
nextCookies.delete(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
71
100
|
|
|
72
101
|
if (sessionId) {
|
|
73
102
|
redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId, returnTo }));
|
|
@@ -98,22 +127,25 @@ export async function switchToOrganization(
|
|
|
98
127
|
redirect(cause.rawData.authkit_redirect_url);
|
|
99
128
|
} else {
|
|
100
129
|
if (cause?.error === 'sso_required' || cause?.error === 'mfa_enrollment') {
|
|
101
|
-
|
|
102
|
-
return redirect(url);
|
|
130
|
+
return redirect(await getAuthURLAndSetPKCECookie({ organizationId }));
|
|
103
131
|
}
|
|
104
132
|
throw error;
|
|
105
133
|
}
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
try {
|
|
137
|
+
switch (revalidationStrategy) {
|
|
138
|
+
case 'path':
|
|
139
|
+
revalidatePath(pathname);
|
|
140
|
+
break;
|
|
141
|
+
case 'tag':
|
|
142
|
+
for (const tag of revalidationTags) {
|
|
143
|
+
revalidateTagCompat(tag);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// revalidatePath/revalidateTag may not be available in non-Next.js environments (e.g., vinext)
|
|
117
149
|
}
|
|
118
150
|
if (revalidationStrategy !== 'none') {
|
|
119
151
|
redirect(pathname);
|