cloudflare-next-intl 0.6.28 → 0.6.30
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/dist/src/config/middleware.js +3 -1
- package/dist/src/error_handling/create_server_error_action.d.ts +6 -0
- package/dist/src/error_handling/create_server_error_action.js +35 -1
- package/dist/src/firebase_auth/middleware/update_session.d.ts +18 -1
- package/dist/src/firebase_auth/middleware/update_session.js +19 -2
- package/dist/src/firebase_auth/server/firebase_server.js +16 -2
- package/package.json +1 -1
|
@@ -141,7 +141,9 @@ export default async function intlMiddleware(request, options) {
|
|
|
141
141
|
if (!updateSessionModule) {
|
|
142
142
|
updateSessionModule = await import('../firebase_auth/middleware/update_session');
|
|
143
143
|
}
|
|
144
|
-
response = await updateSessionModule.default(request, response, effectiveLocaleForRequest)
|
|
144
|
+
response = await updateSessionModule.default(request, response, effectiveLocaleForRequest, (refreshedRequest) => rewriteUrl
|
|
145
|
+
? NextResponse.rewrite(rewriteUrl, { request: refreshedRequest })
|
|
146
|
+
: NextResponse.next({ request: refreshedRequest }));
|
|
145
147
|
}
|
|
146
148
|
return response;
|
|
147
149
|
}
|
|
@@ -26,6 +26,12 @@ import { type ReportErrorConfig } from './report_error';
|
|
|
26
26
|
* boundary (Next.js server actions serialize arguments; an `Error` instance
|
|
27
27
|
* doesn't survive that intact) and `isClient: true` is set automatically.
|
|
28
28
|
*
|
|
29
|
+
* Also attaches `requestContext: { path, userAgent, referer }` (best-effort,
|
|
30
|
+
* via `next/headers` — see `resolveRequestContext`) alongside your own
|
|
31
|
+
* `params`, so `onError`/the console report shows WHERE the error happened,
|
|
32
|
+
* not just what it was — useful when diagnosing a client error without a
|
|
33
|
+
* repro, since the page and browser are often the missing piece.
|
|
34
|
+
*
|
|
29
35
|
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
30
36
|
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
31
37
|
*/
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import reportError from './report_error';
|
|
2
2
|
import stringifyUnknown from './stringify_unknown';
|
|
3
|
+
/**
|
|
4
|
+
* Reads request context (page path, user agent, referer) via `next/headers`
|
|
5
|
+
* for a client-originated error report. `path` comes from `x-pathname`, set
|
|
6
|
+
* by `intlMiddleware` (this package's own middleware) — falls back to
|
|
7
|
+
* `undefined` when a header is missing (e.g. middleware didn't run for this
|
|
8
|
+
* request) rather than throwing.
|
|
9
|
+
*/
|
|
10
|
+
async function resolveRequestContext() {
|
|
11
|
+
try {
|
|
12
|
+
const { headers } = await import('next/headers');
|
|
13
|
+
const headerList = await headers();
|
|
14
|
+
return {
|
|
15
|
+
path: headerList.get('x-pathname') ?? undefined,
|
|
16
|
+
userAgent: headerList.get('user-agent') ?? undefined,
|
|
17
|
+
referer: headerList.get('referer') ?? undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
3
24
|
/**
|
|
4
25
|
* Builds a function that reports a client-originated error via
|
|
5
26
|
* `reportError`, meant to be re-exported directly from your OWN
|
|
@@ -26,15 +47,28 @@ import stringifyUnknown from './stringify_unknown';
|
|
|
26
47
|
* boundary (Next.js server actions serialize arguments; an `Error` instance
|
|
27
48
|
* doesn't survive that intact) and `isClient: true` is set automatically.
|
|
28
49
|
*
|
|
50
|
+
* Also attaches `requestContext: { path, userAgent, referer }` (best-effort,
|
|
51
|
+
* via `next/headers` — see `resolveRequestContext`) alongside your own
|
|
52
|
+
* `params`, so `onError`/the console report shows WHERE the error happened,
|
|
53
|
+
* not just what it was — useful when diagnosing a client error without a
|
|
54
|
+
* repro, since the page and browser are often the missing piece.
|
|
55
|
+
*
|
|
29
56
|
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
30
57
|
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
31
58
|
*/
|
|
32
59
|
export default function createServerErrorAction(config) {
|
|
33
60
|
return async function reportClientError(error, classOrMethodName, params) {
|
|
61
|
+
const requestContext = await resolveRequestContext();
|
|
62
|
+
const isPlainParamsObject = typeof params === 'object' && params !== null && !Array.isArray(params);
|
|
63
|
+
const mergedParams = params === undefined
|
|
64
|
+
? { requestContext }
|
|
65
|
+
: isPlainParamsObject
|
|
66
|
+
? { ...params, requestContext }
|
|
67
|
+
: { params, requestContext };
|
|
34
68
|
await reportError(config, {
|
|
35
69
|
error: stringifyUnknown(error, true),
|
|
36
70
|
classOrMethodName,
|
|
37
|
-
params,
|
|
71
|
+
params: mergedParams,
|
|
38
72
|
isClient: true,
|
|
39
73
|
});
|
|
40
74
|
};
|
|
@@ -4,6 +4,23 @@ export declare const defaultRefreshTokenCookieName = "__fa_refresh_token__";
|
|
|
4
4
|
export declare const defaultEmailVerifiedHintCookieName = "__fa_email_verified_hint__";
|
|
5
5
|
export declare const defaultAppCheckTokenCookieName = "__fa_app_check_token__";
|
|
6
6
|
export declare const defaultResetPasswordPath = "/reset-password";
|
|
7
|
+
export declare function isIdTokenExpired(token: string): boolean;
|
|
8
|
+
export type RefreshResult = {
|
|
9
|
+
status: 'refreshed';
|
|
10
|
+
idToken: string;
|
|
11
|
+
refreshToken: string;
|
|
12
|
+
} | {
|
|
13
|
+
status: 'invalid';
|
|
14
|
+
} | {
|
|
15
|
+
status: 'transient-failure';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Mints a fresh ID token from a stored refresh token via Google's Secure
|
|
19
|
+
* Token API. No `firebase/auth` import: this runs in the Edge middleware
|
|
20
|
+
* runtime, and `firebase/auth` pulls in Node-only APIs that break Edge
|
|
21
|
+
* bundles even though this function never touches that module.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refreshIdToken(apiKey: string, refreshToken: string): Promise<RefreshResult>;
|
|
7
24
|
/**
|
|
8
25
|
* Layers Firebase session-cookie validation/refresh and auth redirects onto
|
|
9
26
|
* an already-built middleware response. Called internally by `intlMiddleware`
|
|
@@ -22,4 +39,4 @@ export declare const defaultResetPasswordPath = "/reset-password";
|
|
|
22
39
|
* state still survives the redirect.
|
|
23
40
|
* @param locale The effective locale `intlMiddleware` resolved for this request.
|
|
24
41
|
*/
|
|
25
|
-
export default function updateSession(request: NextRequest, baseResponse: NextResponse, locale: string): Promise<NextResponse>;
|
|
42
|
+
export default function updateSession(request: NextRequest, baseResponse: NextResponse, locale: string, rebuildResponse?: (request: NextRequest) => NextResponse): Promise<NextResponse>;
|
|
@@ -43,6 +43,9 @@ const DEFAULT_REFRESH_MAX_AGE = 60 * 60 * 24 * 365;
|
|
|
43
43
|
// time can hand a client a token that dies moments after this check, forcing
|
|
44
44
|
// an extra round-trip on the very next request.
|
|
45
45
|
const CLOCK_SKEW_MARGIN_MS = 60 * 1000;
|
|
46
|
+
export function isIdTokenExpired(token) {
|
|
47
|
+
return isJwtExpired(token);
|
|
48
|
+
}
|
|
46
49
|
function isJwtExpired(token) {
|
|
47
50
|
const decoded = decodeJwtPayload(token);
|
|
48
51
|
return !decoded?.exp || decoded.exp * 1000 - CLOCK_SKEW_MARGIN_MS <= Date.now();
|
|
@@ -126,7 +129,7 @@ const INVALID_REFRESH_TOKEN_ERRORS = new Set([
|
|
|
126
129
|
* runtime, and `firebase/auth` pulls in Node-only APIs that break Edge
|
|
127
130
|
* bundles even though this function never touches that module.
|
|
128
131
|
*/
|
|
129
|
-
async function refreshIdToken(apiKey, refreshToken) {
|
|
132
|
+
export async function refreshIdToken(apiKey, refreshToken) {
|
|
130
133
|
const cached = await getCachedRefresh(refreshToken);
|
|
131
134
|
if (cached)
|
|
132
135
|
return { status: 'refreshed', ...cached };
|
|
@@ -183,7 +186,7 @@ async function refreshIdToken(apiKey, refreshToken) {
|
|
|
183
186
|
* state still survives the redirect.
|
|
184
187
|
* @param locale The effective locale `intlMiddleware` resolved for this request.
|
|
185
188
|
*/
|
|
186
|
-
export default async function updateSession(request, baseResponse, locale) {
|
|
189
|
+
export default async function updateSession(request, baseResponse, locale, rebuildResponse) {
|
|
187
190
|
const fa = config.firebaseAuth;
|
|
188
191
|
if (!fa || fa.middlewareEnabled === false)
|
|
189
192
|
return baseResponse;
|
|
@@ -354,6 +357,20 @@ export default async function updateSession(request, baseResponse, locale) {
|
|
|
354
357
|
response.cookies.delete(refreshTokenCookieName);
|
|
355
358
|
}
|
|
356
359
|
if (refreshedToken) {
|
|
360
|
+
// `response.cookies.set` only reaches the BROWSER — the current
|
|
361
|
+
// render still reads the old, expired token from `cookies()`, so
|
|
362
|
+
// `initializeServerApp` rejects it with `auth/invalid-user-token`.
|
|
363
|
+
// Writing to `request.cookies` and rebuilding the pass-through
|
|
364
|
+
// response from that request makes the fresh token visible to this
|
|
365
|
+
// render too.
|
|
366
|
+
request.cookies.set(sessionCookieName, refreshedToken.idToken);
|
|
367
|
+
request.cookies.set(refreshTokenCookieName, refreshedToken.refreshToken);
|
|
368
|
+
if (rebuildResponse && response === baseResponse) {
|
|
369
|
+
const rebuilt = rebuildResponse(request);
|
|
370
|
+
baseResponse.cookies.getAll().forEach((cookie) => rebuilt.cookies.set(cookie));
|
|
371
|
+
baseResponse.headers.forEach((value, key) => rebuilt.headers.set(key, value));
|
|
372
|
+
response = rebuilt;
|
|
373
|
+
}
|
|
357
374
|
response.cookies.set(sessionCookieName, refreshedToken.idToken, {
|
|
358
375
|
httpOnly: true,
|
|
359
376
|
secure: request.nextUrl.protocol === 'https',
|
|
@@ -2,7 +2,7 @@ import { cookies } from 'next/headers';
|
|
|
2
2
|
import { cache } from 'react';
|
|
3
3
|
import config from '@intl-config';
|
|
4
4
|
import requireFirebaseAuthConfig from '../require_config';
|
|
5
|
-
import { defaultAppCheckTokenCookieName, defaultSessionCookieName } from '../middleware/update_session';
|
|
5
|
+
import { defaultAppCheckTokenCookieName, defaultRefreshTokenCookieName, defaultSessionCookieName, isIdTokenExpired, refreshIdToken } from '../middleware/update_session';
|
|
6
6
|
import reportError from '../../error_handling/report_error';
|
|
7
7
|
import mintServerAppCheckToken from './mint_server_app_check_token';
|
|
8
8
|
let baseApp;
|
|
@@ -22,8 +22,22 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
|
|
|
22
22
|
requireFirebaseAuthConfig(fa);
|
|
23
23
|
const sessionCookieName = fa.sessionCookieName ?? defaultSessionCookieName;
|
|
24
24
|
const appCheckTokenCookieName = fa.appCheckTokenCookieName ?? defaultAppCheckTokenCookieName;
|
|
25
|
+
const refreshTokenCookieName = fa.refreshTokenCookieName ?? defaultRefreshTokenCookieName;
|
|
25
26
|
const cookieStore = await cookies();
|
|
26
|
-
|
|
27
|
+
let authIdToken = cookieStore.get(sessionCookieName)?.value;
|
|
28
|
+
// Safety net for the paths the middleware can't cover (prefetch requests,
|
|
29
|
+
// excluded matcher routes, server actions): an expired token would be
|
|
30
|
+
// rejected by `initializeServerApp` with `auth/invalid-user-token`, so
|
|
31
|
+
// mint a fresh one from the refresh-token cookie first. The refreshed
|
|
32
|
+
// token can't be written back to the cookie from here (RSC render), but
|
|
33
|
+
// the middleware persists it on the next request.
|
|
34
|
+
if (authIdToken && isIdTokenExpired(authIdToken)) {
|
|
35
|
+
const refreshToken = cookieStore.get(refreshTokenCookieName)?.value;
|
|
36
|
+
const result = refreshToken
|
|
37
|
+
? await refreshIdToken(fa.apiKey, refreshToken)
|
|
38
|
+
: undefined;
|
|
39
|
+
authIdToken = result?.status === 'refreshed' ? result.idToken : undefined;
|
|
40
|
+
}
|
|
27
41
|
if (!authIdToken) {
|
|
28
42
|
return { firebaseServerApp: null, currentUser: null };
|
|
29
43
|
}
|