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.
@@ -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
- const authIdToken = cookieStore.get(sessionCookieName)?.value;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.6.28",
3
+ "version": "0.6.30",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",