cloudflare-next-intl 0.10.16 → 0.10.17
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 +3 -1
- package/dist/src/client/components/client_provider.js +8 -2
- package/dist/src/firebase_auth/client/auth_user_provider.js +24 -2
- package/dist/src/firebase_auth/client/firebase_client.js +56 -10
- package/dist/src/server/components/helper_script.js +1 -4
- package/dist/src/types/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -512,10 +512,12 @@ Features include:
|
|
|
512
512
|
- `whiteListPaths`: Array of paths exempt from auth redirects (matches exact path or path-segment prefix, e.g. `/bonds` matches `/bonds/some-slug`).
|
|
513
513
|
- `protectedPaths`: `(path) => boolean` gate that inverts `whiteListPaths` — when set, every path passes through unless `protectedPaths` (or `isAuthPath`) returns true, so unrecognised URLs reach the app's own routing instead of being redirected to `redirectAuthPath`. Preferred over `whiteListPaths` for apps where most pages are public: both the middleware and `AuthUserProvider` (client) derive their whitelist from it directly, so you don't maintain two separate path lists that can drift out of sync. `whiteListPaths` is only consulted when `protectedPaths` isn't set.
|
|
514
514
|
- `actionLinkPath`: Pinned route for handling Firebase action links (e.g. `/auth/action`), prioritized during cross-origin action link redirects.
|
|
515
|
+
- `skipPopupRedirectResolver`: Skips the Auth SDK's default `browserPopupRedirectResolver`, which proactively opens the `<authDomain>/__/auth/iframe.js` relay iframe on every page load on mobile/Safari/iOS user agents. Safe even with Google/Apple/social sign-in — pass `browserPopupRedirectResolver` as the last argument to `signInWithPopup`/`signInWithRedirect`/`getRedirectResult`/`linkWithPopup`/`reauthenticateWithPopup` at the call site instead, so the iframe only loads on the click. Also skips the Auth emulator auto-connect and the experimental `authTokenSyncURL` cookie exchange that `getAuth` does — see the JSDoc on this option before enabling it against an emulator.
|
|
515
516
|
- `createForgotPasswordAction(locale, actionCodeSettings?)`: Accepts optional Firebase `AuthActionCodeSettings` (e.g. `url` redirect link).
|
|
516
517
|
- `sendVerificationEmail(actionCodeSettings?)` on `useAuthUser()`: Custom action email settings when resending email verification.
|
|
517
518
|
- `followSameOriginContinueUrl`: Automatically forwards emailed action links with `continueUrl` to the specified path (or external URL) directly from `intlMiddleware` (default `true`; set `false` on `firebaseAuth` config to disable). If `continueUrl` points to home root (`/`), it resolves to `actionLinkPath` (if set) or the mode target path (e.g. `/reset-password`).
|
|
518
|
-
- `appCheck`: Firebase App Check integration supporting reCAPTCHA Enterprise (`recaptchaEnterpriseSiteKey`) and reCAPTCHA v3 (`recaptchaV3SiteKey`). For reCAPTCHA v3, defaults to a `CustomProvider` using `
|
|
519
|
+
- `appCheck`: Firebase App Check integration supporting reCAPTCHA Enterprise (`recaptchaEnterpriseSiteKey`) and reCAPTCHA v3 (`recaptchaV3SiteKey`). For reCAPTCHA v3, defaults to a `CustomProvider` using an explicit `render=explicit` script tag to avoid iframe/webworker CDN integrity issues in private windows (`useExplicitRecaptchaScript: false` to opt out). That script is injected on first actual need (the first `getAppCheckToken()`/token mint) rather than added to `<head>` up front, so a page that never mints a token never pays its ~345KB cost. Supports server-side token minting via service accounts.
|
|
520
|
+
- `lazyInit`: defers App Check's own initialization (not just the script above) to that same first-need moment instead of running it as part of every `getFirebaseAuthClient()` call. Off by default — App Check reads the App Check provider off the app per-request and silently omits the `X-Firebase-AppCheck` header if it isn't registered yet, so deferring is only safe when App Check enforcement is off for every product this app calls (Auth, Firestore, Functions, etc).
|
|
519
521
|
- `resolveOptionalAuthUser()` (`cloudflare-next-intl/resolveOptionalAuthUser`): best-effort variant of `getAuthUser()` for callers that want to *attach* the current user when one happens to be known — error/telemetry reporting, analytics, logging — without failing or changing behavior when no request/session context is available. Swallows every failure and resolves `{ user: null }` instead of throwing. Use `getAuthUser()` (not this) whenever your own output actually depends on who's signed in — `checkDynamicPages` (below) treats `getAuthUser()`/`useAuthUser()`/`withUserDb()` as page-blocking dynamic-API signals but does not follow into this helper, so it's also the way to read "whoever's signed in, if anyone" from a page or a module it imports without forcing that page dynamic.
|
|
520
522
|
- `resolveErrorReportingUser(useAuthUser?)` (same subpath): gated form of `resolveOptionalAuthUser()` for an `onError` sink — pass `ErrorHandlingParams.useAuthUser` straight through. Defaults to `false`: when not `true`, resolves `{ user: null }` immediately without calling `getAuthUser()` at all, so a page reached only through your `onError` sink's default path stays static-eligible. A specific `reportError({ ..., useAuthUser: true })` call site opts in per-call when it actually wants the user on that report.
|
|
521
523
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables.js";
|
|
4
|
-
import { createContext, useMemo, Suspense } from "react";
|
|
4
|
+
import { createContext, useEffect, useMemo, Suspense } from "react";
|
|
5
5
|
import dynamic from "next/dynamic.js";
|
|
6
6
|
import useLazyWrappingProvider from "./use_lazy_wrapping_provider.js";
|
|
7
7
|
import config from "@intl-config";
|
|
@@ -14,7 +14,8 @@ const AutoFirebasePerformanceEvents = dynamic(() => import("../../firebase_auth/
|
|
|
14
14
|
const loadCookieConsentProvider = () => import("../../cookie_consent/client/cookie_consent_provider.js");
|
|
15
15
|
const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics.js"));
|
|
16
16
|
const AutoAnalyticsEvents = dynamic(() => import("../../cookie_consent/client/components/auto_analytics_events.js"));
|
|
17
|
-
const
|
|
17
|
+
const loadCookieConsentDialog = () => import("../../cookie_consent/client/components/cookie_consent_dialog.js");
|
|
18
|
+
const CookieConsentDialog = dynamic(loadCookieConsentDialog);
|
|
18
19
|
const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog.js"));
|
|
19
20
|
export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, autoAnalyticsEventsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
20
21
|
setLocaleCache(language);
|
|
@@ -23,6 +24,11 @@ export default function LocationzationClientProvider({ language, messages, initi
|
|
|
23
24
|
installGlobalErrorOverride(config);
|
|
24
25
|
const { Provider: AuthUserProvider } = useLazyWrappingProvider(loadAuthUserProvider);
|
|
25
26
|
const { Provider: CookieConsentProvider, isReady: cookieConsentReady } = useLazyWrappingProvider(loadCookieConsentProvider);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!config.cookieConsent || !autoWireDialogs)
|
|
29
|
+
return;
|
|
30
|
+
loadCookieConsentDialog().catch(() => undefined);
|
|
31
|
+
}, [autoWireDialogs]);
|
|
26
32
|
let providedChildren = children;
|
|
27
33
|
if (config.firebaseAuth && !skipAuthProvider) {
|
|
28
34
|
providedChildren = _jsx(AuthUserPendingProvider, { initialUser: initialAuthUser, children: _jsxs(AuthUserProvider, { initialUser: initialAuthUser, children: [children, config.firebaseAuth.performance !== false && (_jsx(Suspense, { fallback: null, children: _jsx(AutoFirebasePerformanceEvents, {}) }))] }) });
|
|
@@ -15,6 +15,8 @@ import setCookie from '../../client/functions/set_cookie.js';
|
|
|
15
15
|
import getCookie from '../../client/functions/get_cookie.js';
|
|
16
16
|
import clearSessionAction from '../server/clear_session_action.js';
|
|
17
17
|
import { AuthUserContext } from './auth_user_context.js';
|
|
18
|
+
const AUTH_SUBSCRIBE_IDLE_TIMEOUT_MS = 3000;
|
|
19
|
+
const AUTH_SUBSCRIBE_FALLBACK_DELAY_MS = 1500;
|
|
18
20
|
export { AuthUserContext };
|
|
19
21
|
function writeSessionCookie(sessionCookieName, idToken, maxAge) {
|
|
20
22
|
setCookie({ name: sessionCookieName, value: idToken, maxAge });
|
|
@@ -96,6 +98,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
96
98
|
const signInCallbackFired = useRef(initialUser !== null);
|
|
97
99
|
const signOutCallbackFired = useRef(initialUser === null);
|
|
98
100
|
const emailVerifiedRef = useRef(initialUser?.emailVerified ?? false);
|
|
101
|
+
const deferAuthSubscribe = initialUser === null && isWhiteListed && !isAuthPage && typeof window !== 'undefined';
|
|
99
102
|
useEffect(() => {
|
|
100
103
|
const { user, loading } = state;
|
|
101
104
|
if (loading || isWhiteListed)
|
|
@@ -114,7 +117,9 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
114
117
|
useEffect(() => {
|
|
115
118
|
let unsubscribe;
|
|
116
119
|
let cancelled = false;
|
|
117
|
-
|
|
120
|
+
let idleHandle;
|
|
121
|
+
let timeoutHandle;
|
|
122
|
+
const subscribe = () => getFirebaseAuthClient().then(async ({ auth }) => {
|
|
118
123
|
if (cancelled)
|
|
119
124
|
return;
|
|
120
125
|
const { onIdTokenChanged } = await getFirebaseAuthModule();
|
|
@@ -185,11 +190,28 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
185
190
|
}
|
|
186
191
|
});
|
|
187
192
|
});
|
|
193
|
+
if (deferAuthSubscribe) {
|
|
194
|
+
setState(current => (current.loading ? { ...current, loading: false } : current));
|
|
195
|
+
const idle = window.requestIdleCallback;
|
|
196
|
+
if (idle)
|
|
197
|
+
idleHandle = idle(() => { if (!cancelled)
|
|
198
|
+
subscribe(); }, { timeout: AUTH_SUBSCRIBE_IDLE_TIMEOUT_MS });
|
|
199
|
+
else
|
|
200
|
+
timeoutHandle = setTimeout(() => { if (!cancelled)
|
|
201
|
+
subscribe(); }, AUTH_SUBSCRIBE_FALLBACK_DELAY_MS);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
subscribe();
|
|
205
|
+
}
|
|
188
206
|
return () => {
|
|
189
207
|
cancelled = true;
|
|
208
|
+
if (idleHandle !== undefined)
|
|
209
|
+
window.cancelIdleCallback?.(idleHandle);
|
|
210
|
+
if (timeoutHandle !== undefined)
|
|
211
|
+
clearTimeout(timeoutHandle);
|
|
190
212
|
unsubscribe?.();
|
|
191
213
|
};
|
|
192
|
-
}, [router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
|
|
214
|
+
}, [deferAuthSubscribe, router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
|
|
193
215
|
const reloadUser = useCallback(async () => {
|
|
194
216
|
const { auth } = await getFirebaseAuthClient();
|
|
195
217
|
const user = auth.currentUser;
|
|
@@ -5,9 +5,22 @@ let cachedAppCheck;
|
|
|
5
5
|
let cachedPerformance;
|
|
6
6
|
const GRECAPTCHA_LOAD_TIMEOUT_MS = 15000;
|
|
7
7
|
const GRECAPTCHA_POLL_INTERVAL_MS = 50;
|
|
8
|
+
const RECAPTCHA_SCRIPT_SRC = 'https://www.google.com/recaptcha/api.js?render=explicit';
|
|
9
|
+
let recaptchaScriptFailed = false;
|
|
10
|
+
function ensureRecaptchaScript() {
|
|
11
|
+
if (document.querySelector(`script[src="${RECAPTCHA_SCRIPT_SRC}"]`))
|
|
12
|
+
return;
|
|
13
|
+
const script = document.createElement('script');
|
|
14
|
+
script.src = RECAPTCHA_SCRIPT_SRC;
|
|
15
|
+
script.async = true;
|
|
16
|
+
script.defer = true;
|
|
17
|
+
script.onerror = () => { recaptchaScriptFailed = true; };
|
|
18
|
+
document.head.appendChild(script);
|
|
19
|
+
}
|
|
8
20
|
function waitForGrecaptcha() {
|
|
9
21
|
if (window.grecaptcha)
|
|
10
22
|
return Promise.resolve(window.grecaptcha);
|
|
23
|
+
ensureRecaptchaScript();
|
|
11
24
|
return new Promise((resolve, reject) => {
|
|
12
25
|
const startedAt = Date.now();
|
|
13
26
|
const timer = setInterval(() => {
|
|
@@ -15,9 +28,9 @@ function waitForGrecaptcha() {
|
|
|
15
28
|
clearInterval(timer);
|
|
16
29
|
resolve(window.grecaptcha);
|
|
17
30
|
}
|
|
18
|
-
else if (Date.now() - startedAt >= GRECAPTCHA_LOAD_TIMEOUT_MS) {
|
|
31
|
+
else if (recaptchaScriptFailed || Date.now() - startedAt >= GRECAPTCHA_LOAD_TIMEOUT_MS) {
|
|
19
32
|
clearInterval(timer);
|
|
20
|
-
reject(new Error(
|
|
33
|
+
reject(new Error(`window.grecaptcha never loaded; ${RECAPTCHA_SCRIPT_SRC} failed to load or was blocked`));
|
|
21
34
|
}
|
|
22
35
|
}, GRECAPTCHA_POLL_INTERVAL_MS);
|
|
23
36
|
});
|
|
@@ -108,7 +121,34 @@ async function initializeFirebaseAppCheck(app, appCheckConfig) {
|
|
|
108
121
|
});
|
|
109
122
|
}
|
|
110
123
|
const APP_CHECK_TOKEN_TIMEOUT_MS = 10000;
|
|
124
|
+
let appCheckInitPromise;
|
|
125
|
+
function initAppCheckOnce(app) {
|
|
126
|
+
if (appCheckInitPromise)
|
|
127
|
+
return appCheckInitPromise;
|
|
128
|
+
const appCheckConfig = config.firebaseAuth?.appCheck;
|
|
129
|
+
if (!appCheckConfig || typeof window === 'undefined')
|
|
130
|
+
return Promise.resolve();
|
|
131
|
+
appCheckInitPromise = (async () => {
|
|
132
|
+
try {
|
|
133
|
+
cachedAppCheck = await initializeFirebaseAppCheck(app, appCheckConfig);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.warn('App Check initialization failed, continuing without it', error);
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
return appCheckInitPromise;
|
|
140
|
+
}
|
|
141
|
+
async function ensureAppCheck() {
|
|
142
|
+
if (appCheckInitPromise)
|
|
143
|
+
return appCheckInitPromise;
|
|
144
|
+
const appCheckConfig = config.firebaseAuth?.appCheck;
|
|
145
|
+
if (!appCheckConfig || typeof window === 'undefined')
|
|
146
|
+
return;
|
|
147
|
+
const { app } = await getFirebaseAuthClient();
|
|
148
|
+
return initAppCheckOnce(app);
|
|
149
|
+
}
|
|
111
150
|
export async function getAppCheckToken() {
|
|
151
|
+
await ensureAppCheck().catch(() => undefined);
|
|
112
152
|
if (!cachedAppCheck)
|
|
113
153
|
return undefined;
|
|
114
154
|
const { getToken } = await import('@firebase/app-check');
|
|
@@ -137,7 +177,8 @@ export async function getFirebaseAuthClient() {
|
|
|
137
177
|
import('@firebase/app'),
|
|
138
178
|
import('@firebase/auth'),
|
|
139
179
|
isPerformanceEnabled ? import('@firebase/performance') : Promise.resolve(null),
|
|
140
|
-
]).then(async ([{ getApp, getApps, initializeApp },
|
|
180
|
+
]).then(async ([{ getApp, getApps, initializeApp }, authModule, perfModule]) => {
|
|
181
|
+
const { getAuth, initializeAuth, indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence } = authModule;
|
|
141
182
|
const firebaseConfig = {
|
|
142
183
|
apiKey: fa.apiKey,
|
|
143
184
|
authDomain: fa.authDomain,
|
|
@@ -148,18 +189,23 @@ export async function getFirebaseAuthClient() {
|
|
|
148
189
|
measurementId: fa.measurementId,
|
|
149
190
|
};
|
|
150
191
|
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
|
|
151
|
-
if (fa.appCheck
|
|
192
|
+
if (!fa.appCheck?.lazyInit)
|
|
193
|
+
await initAppCheckOnce(app);
|
|
194
|
+
if (perfModule) {
|
|
195
|
+
cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
|
|
196
|
+
}
|
|
197
|
+
let auth;
|
|
198
|
+
if (fa.skipPopupRedirectResolver) {
|
|
152
199
|
try {
|
|
153
|
-
|
|
200
|
+
auth = initializeAuth(app, { persistence: [indexedDBLocalPersistence, browserLocalPersistence, browserSessionPersistence] });
|
|
154
201
|
}
|
|
155
|
-
catch
|
|
156
|
-
|
|
202
|
+
catch {
|
|
203
|
+
auth = getAuth(app);
|
|
157
204
|
}
|
|
158
205
|
}
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
else {
|
|
207
|
+
auth = getAuth(app);
|
|
161
208
|
}
|
|
162
|
-
const auth = getAuth(app);
|
|
163
209
|
cached = { app, auth };
|
|
164
210
|
return cached;
|
|
165
211
|
});
|
|
@@ -5,13 +5,10 @@ import ClientHelperScript from "../../client/components/client_helper_script.js"
|
|
|
5
5
|
import { defaultStaleDeployPatterns } from "../../error_handling/is_stale_deploy_error.js";
|
|
6
6
|
const isDev = process.env.NODE_ENV === 'development';
|
|
7
7
|
export const defaultReloadHtml = '<div style="position:fixed;inset:0;background:#ffffff;display:flex;align-items:center;justify-content:center;z-index:9999999;"><div style="width:36px;height:36px;border:3px solid #e5e7eb;border-top-color:#17181b;border-radius:50%;animation:cfni-spin 0.8s linear infinite;"></div><style>@keyframes cfni-spin{to{transform:rotate(360deg)}}</style></div>';
|
|
8
|
-
const appCheck = config.firebaseAuth?.appCheck;
|
|
9
|
-
const shouldLoadExplicitRecaptchaScript = !!appCheck?.recaptchaV3SiteKey && appCheck.useExplicitRecaptchaScript !== false;
|
|
10
8
|
const secureCookieAttribute = isDev ? '+ " Secure;"' : '';
|
|
11
9
|
export default function HelperScript() {
|
|
12
10
|
const reloadHtml = config.errorHandling?.staleDeployReloadHtml ?? defaultReloadHtml;
|
|
13
|
-
return _jsxs(_Fragment, { children: [
|
|
14
|
-
_jsx("script", { src: "https://www.google.com/recaptcha/api.js?render=explicit", async: true, defer: true }), !isDev &&
|
|
11
|
+
return _jsxs(_Fragment, { children: [!isDev &&
|
|
15
12
|
_jsx("script", { id: "stale-deploy-early-catch", dangerouslySetInnerHTML: {
|
|
16
13
|
__html: `(function() {
|
|
17
14
|
try {
|
|
@@ -134,6 +134,7 @@ export interface FirebaseAuthRoutingConfig {
|
|
|
134
134
|
measurementId?: string;
|
|
135
135
|
performance?: boolean;
|
|
136
136
|
appCheck?: FirebaseAppCheckConfig;
|
|
137
|
+
skipPopupRedirectResolver?: boolean;
|
|
137
138
|
redirectAuthPath: string;
|
|
138
139
|
homePath: string;
|
|
139
140
|
verifyEmailPath?: string;
|
|
@@ -164,6 +165,7 @@ export interface FirebaseAppCheckConfig {
|
|
|
164
165
|
recaptchaV3SiteKey?: string;
|
|
165
166
|
recaptchaEnterpriseSiteKey?: string;
|
|
166
167
|
useExplicitRecaptchaScript?: boolean;
|
|
168
|
+
lazyInit?: boolean;
|
|
167
169
|
debugToken?: boolean | string;
|
|
168
170
|
isTokenAutoRefreshEnabled?: boolean;
|
|
169
171
|
clientEmail: string;
|