cloudflare-next-intl 0.8.31 → 0.8.33

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.
@@ -2,9 +2,8 @@
2
2
  * Auto-rendered alongside `FirebaseAuthClientProvider` when
3
3
  * `firebaseAuth.performance` isn't `false` — records Firebase Performance
4
4
  * custom traces for Web Vitals metrics (`web_cls`, `web_fcp`, `web_fid`,
5
- * `web_lcp`, `web_ttfb`, `web_inp`) and SPA route changes (`route_change`),
6
- * alongside Firebase Performance's own automatic page-load/network traces.
7
- * No-ops if `firebaseAuth.performance` is disabled (`getFirebasePerformanceSync`
8
- * never resolves an instance).
5
+ * `web_lcp`, `web_ttfb`, `web_inp`), alongside Firebase Performance's own
6
+ * automatic page-load/network traces. No-ops if `firebaseAuth.performance`
7
+ * is disabled (`getFirebasePerformanceSync` never resolves an instance).
9
8
  */
10
9
  export default function AutoFirebasePerformanceEvents(): null;
@@ -1,7 +1,5 @@
1
1
  'use client';
2
- import { usePathname } from 'next/navigation';
3
2
  import { useReportWebVitals } from 'next/web-vitals';
4
- import { useEffect, useRef } from 'react';
5
3
  import reportError from '../../../error_handling/report_error';
6
4
  import { getFirebaseAuthClient, getFirebasePerformanceSync } from '../firebase_client';
7
5
  /**
@@ -28,84 +26,18 @@ async function recordFirebaseTrace(name, durationMs, attributes, metrics) {
28
26
  });
29
27
  }
30
28
  }
31
- const SLOW_RESOURCE_THRESHOLD_MS = 1000;
32
29
  /**
33
30
  * Auto-rendered alongside `FirebaseAuthClientProvider` when
34
31
  * `firebaseAuth.performance` isn't `false` — records Firebase Performance
35
32
  * custom traces for Web Vitals metrics (`web_cls`, `web_fcp`, `web_fid`,
36
- * `web_lcp`, `web_ttfb`, `web_inp`) and SPA route changes (`route_change`),
37
- * alongside Firebase Performance's own automatic page-load/network traces.
38
- * No-ops if `firebaseAuth.performance` is disabled (`getFirebasePerformanceSync`
39
- * never resolves an instance).
33
+ * `web_lcp`, `web_ttfb`, `web_inp`), alongside Firebase Performance's own
34
+ * automatic page-load/network traces. No-ops if `firebaseAuth.performance`
35
+ * is disabled (`getFirebasePerformanceSync` never resolves an instance).
40
36
  */
41
37
  export default function AutoFirebasePerformanceEvents() {
42
38
  useReportWebVitals((metric) => {
43
39
  const value = Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value);
44
40
  void recordFirebaseTrace(`web_${metric.name.toLowerCase()}`, value, { rating: metric.rating });
45
41
  });
46
- const path = usePathname();
47
- const isFirstRoute = useRef(true);
48
- const lastRouteChangeRef = useRef(Date.now());
49
- const longTaskStats = useRef({ count: 0, totalDuration: 0 });
50
- // Mount-only: registers a PerformanceObserver for long tasks (main-thread
51
- // blocks >= 50ms). Not all browsers support the 'longtask' entry type, so
52
- // this is a best-effort signal. The final route's tail of accumulated
53
- // long tasks between the last route change and unmount is intentionally
54
- // dropped rather than flushed here — this is a monitoring signal, not a
55
- // billing metric, and an occasional dropped tail sample is acceptable.
56
- useEffect(() => {
57
- if (typeof PerformanceObserver === 'undefined' || !PerformanceObserver.supportedEntryTypes?.includes('longtask'))
58
- return;
59
- const observer = new PerformanceObserver((list) => {
60
- for (const entry of list.getEntries()) {
61
- longTaskStats.current.count += 1;
62
- longTaskStats.current.totalDuration += entry.duration;
63
- }
64
- });
65
- observer.observe({ type: 'longtask', buffered: true });
66
- return () => observer.disconnect();
67
- }, []);
68
- // Mount-only: registers a PerformanceObserver for slow non-fetch/XHR
69
- // resources (scripts, images, stylesheets, fonts, etc). Firebase
70
- // Performance's own automatic network monitoring already instruments
71
- // fetch/XHR, so those are skipped here to avoid duplicate signals.
72
- // `buffered: true` also catches resources that loaded before this
73
- // component mounted (it only mounts after `AuthUserProvider`).
74
- useEffect(() => {
75
- if (typeof PerformanceObserver === 'undefined' || !PerformanceObserver.supportedEntryTypes?.includes('resource'))
76
- return;
77
- const observer = new PerformanceObserver((list) => {
78
- for (const entry of list.getEntries()) {
79
- if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
80
- continue;
81
- if (entry.duration < SLOW_RESOURCE_THRESHOLD_MS)
82
- continue;
83
- void recordFirebaseTrace('slow_resource', entry.duration, {
84
- initiator_type: entry.initiatorType,
85
- resource: entry.name.slice(-100),
86
- }, { transfer_size_bytes: Math.round(entry.transferSize ?? 0) });
87
- }
88
- });
89
- observer.observe({ type: 'resource', buffered: true });
90
- return () => observer.disconnect();
91
- }, []);
92
- useEffect(() => {
93
- const now = Date.now();
94
- const duration = now - lastRouteChangeRef.current;
95
- lastRouteChangeRef.current = now;
96
- if (isFirstRoute.current) {
97
- isFirstRoute.current = false;
98
- return;
99
- }
100
- // Approximates navigation duration as time between path-change commits —
101
- // App Router exposes no public "navigation start" event this package
102
- // can hook into, so this is not a precise navigation timing.
103
- void recordFirebaseTrace('route_change', duration, { path: path.slice(-100) });
104
- const { count, totalDuration } = longTaskStats.current;
105
- longTaskStats.current = { count: 0, totalDuration: 0 };
106
- if (count > 0) {
107
- void recordFirebaseTrace('route_long_tasks', totalDuration, { path: path.slice(-100) }, { long_task_count: count });
108
- }
109
- }, [path]);
110
42
  return null;
111
43
  }
@@ -62,7 +62,7 @@ export async function getFirebaseAuthClient() {
62
62
  measurementId: fa.measurementId,
63
63
  };
64
64
  const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
65
- if (fa.appCheck) {
65
+ if (fa.appCheck && typeof window !== 'undefined') {
66
66
  cachedAppCheck = await initializeFirebaseAppCheck(app, fa.appCheck);
67
67
  }
68
68
  if (perfModule) {
@@ -160,6 +160,7 @@ export async function refreshIdToken(apiKey, refreshToken, options) {
160
160
  method: 'POST',
161
161
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
162
162
  body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
163
+ cache: 'no-store',
163
164
  });
164
165
  if (!res.ok) {
165
166
  if (res.status === 400) {
@@ -243,7 +244,11 @@ export default async function updateSession(request, baseResponse, locale, rebui
243
244
  const mode = request.nextUrl.searchParams.get('mode');
244
245
  if (mode) {
245
246
  let target = resolveActionModePaths(fa)[mode];
246
- if (fa.followSameOriginContinueUrl !== false) {
247
+ // Already on the page this mode routes to: the link has arrived.
248
+ // Following `continueUrl` from here would send it back to the
249
+ // action URL, which forwards to this same target again — an
250
+ // endless 307 ping-pong.
251
+ if (fa.followSameOriginContinueUrl !== false && target !== path) {
247
252
  const continueUrl = request.nextUrl.searchParams.get('continueUrl');
248
253
  if (continueUrl) {
249
254
  try {
@@ -291,6 +296,15 @@ export default async function updateSession(request, baseResponse, locale, rebui
291
296
  if (target && target !== path) {
292
297
  const url = localeUrl(target);
293
298
  url.search = request.nextUrl.search;
299
+ // Staying on this origin: only `oobCode` (plus anything the app
300
+ // put there itself) is still needed. Dropping Firebase's own
301
+ // routing params keeps the landed URL clean and makes a second
302
+ // forwarding pass impossible.
303
+ if (fa.stripActionLinkQuery !== false) {
304
+ for (const key of ['mode', 'apiKey', 'lang', 'continueUrl']) {
305
+ url.searchParams.delete(key);
306
+ }
307
+ }
294
308
  return buildRedirect(baseResponse, url);
295
309
  }
296
310
  }
@@ -486,5 +500,7 @@ function buildRedirect(baseResponse, url) {
486
500
  const redirectResponse = NextResponse.redirect(url);
487
501
  baseResponse.cookies.getAll().forEach((cookie) => redirectResponse.cookies.set(cookie));
488
502
  baseResponse.headers.forEach((value, key) => redirectResponse.headers.set(key, value));
503
+ // Explicitly prevent OpenNext/Cloudflare from caching these auth redirects.
504
+ redirectResponse.headers.set('Cache-Control', 'private, no-cache, no-store, max-age=0, must-revalidate');
489
505
  return redirectResponse;
490
506
  }
@@ -546,14 +546,24 @@ export interface FirebaseAuthRoutingConfig {
546
546
  * auto-corrects a missing leading slash with a warning.
547
547
  */
548
548
  actionLinkPath?: string;
549
+ /**
550
+ * Whether a same-origin emailed-action-link forward strips Firebase's own
551
+ * `mode`/`apiKey`/`lang`/`continueUrl` params, landing the user on a clean
552
+ * `?oobCode=` URL. Defaults to `true`. Set `false` to keep the full query
553
+ * when the destination page reads those params itself. Cross-origin
554
+ * redirects always keep the full query.
555
+ */
556
+ stripActionLinkQuery?: boolean;
549
557
  /**
550
558
  * Whether the middleware's own redirects (`redirectAuthPath`, `homePath`,
551
559
  * `verifyEmailPath`) carry over the original request's query string —
552
560
  * e.g. `/login?ref=abc` stays `/login?ref=abc` after redirecting to
553
561
  * `homePath` for a signed-in user, instead of dropping to `/`. Defaults
554
562
  * to `true`. The emailed-action-link forward (see
555
- * {@link resetPasswordPath}) always preserves its query string
556
- * regardless of this setting, since `oobCode` must survive that hop.
563
+ * {@link resetPasswordPath}) always preserves `oobCode` regardless of
564
+ * this setting, since it must survive that hop; when the forward stays on
565
+ * this origin it drops Firebase's own `mode`/`apiKey`/`lang`/`continueUrl`
566
+ * params, which the destination page no longer needs.
557
567
  */
558
568
  preserveRedirectQuery?: boolean;
559
569
  /** Returns true if the given (locale-stripped) path is an auth page (login/signup/etc). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.8.31",
3
+ "version": "0.8.33",
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",