cloudflare-next-intl 0.10.15 → 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 +4 -1
- package/dist/src/client/components/client_provider.js +8 -2
- package/dist/src/firebase_auth/client/auth_user_provider.js +27 -3
- 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
|
@@ -510,11 +510,14 @@ bundled — for apps that never set `firebaseAuth`.
|
|
|
510
510
|
|
|
511
511
|
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
|
+
- `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.
|
|
513
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.
|
|
514
516
|
- `createForgotPasswordAction(locale, actionCodeSettings?)`: Accepts optional Firebase `AuthActionCodeSettings` (e.g. `url` redirect link).
|
|
515
517
|
- `sendVerificationEmail(actionCodeSettings?)` on `useAuthUser()`: Custom action email settings when resending email verification.
|
|
516
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`).
|
|
517
|
-
- `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).
|
|
518
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.
|
|
519
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.
|
|
520
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 });
|
|
@@ -76,7 +78,9 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
76
78
|
const router = useRouter();
|
|
77
79
|
const pathname = usePathname();
|
|
78
80
|
const isAuthPage = fa.isAuthPath(pathname);
|
|
79
|
-
const isWhiteListed =
|
|
81
|
+
const isWhiteListed = fa.protectedPaths
|
|
82
|
+
? !fa.protectedPaths(pathname) && !isAuthPage
|
|
83
|
+
: isWhitelisted(pathname, fa.whiteListPaths);
|
|
80
84
|
const maxAge = fa.sessionCookieMaxAge ?? 60 * 60 * 24 * 5;
|
|
81
85
|
const sessionCookieName = fa.sessionCookieName ?? defaultSessionCookieName;
|
|
82
86
|
const refreshTokenMaxAge = fa.refreshTokenCookieMaxAge ?? 60 * 60 * 24 * 365;
|
|
@@ -94,6 +98,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
94
98
|
const signInCallbackFired = useRef(initialUser !== null);
|
|
95
99
|
const signOutCallbackFired = useRef(initialUser === null);
|
|
96
100
|
const emailVerifiedRef = useRef(initialUser?.emailVerified ?? false);
|
|
101
|
+
const deferAuthSubscribe = initialUser === null && isWhiteListed && !isAuthPage && typeof window !== 'undefined';
|
|
97
102
|
useEffect(() => {
|
|
98
103
|
const { user, loading } = state;
|
|
99
104
|
if (loading || isWhiteListed)
|
|
@@ -112,7 +117,9 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
112
117
|
useEffect(() => {
|
|
113
118
|
let unsubscribe;
|
|
114
119
|
let cancelled = false;
|
|
115
|
-
|
|
120
|
+
let idleHandle;
|
|
121
|
+
let timeoutHandle;
|
|
122
|
+
const subscribe = () => getFirebaseAuthClient().then(async ({ auth }) => {
|
|
116
123
|
if (cancelled)
|
|
117
124
|
return;
|
|
118
125
|
const { onIdTokenChanged } = await getFirebaseAuthModule();
|
|
@@ -183,11 +190,28 @@ export default function AuthUserProvider({ initialUser = null, children }) {
|
|
|
183
190
|
}
|
|
184
191
|
});
|
|
185
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
|
+
}
|
|
186
206
|
return () => {
|
|
187
207
|
cancelled = true;
|
|
208
|
+
if (idleHandle !== undefined)
|
|
209
|
+
window.cancelIdleCallback?.(idleHandle);
|
|
210
|
+
if (timeoutHandle !== undefined)
|
|
211
|
+
clearTimeout(timeoutHandle);
|
|
188
212
|
unsubscribe?.();
|
|
189
213
|
};
|
|
190
|
-
}, [router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
|
|
214
|
+
}, [deferAuthSubscribe, router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge, fa]);
|
|
191
215
|
const reloadUser = useCallback(async () => {
|
|
192
216
|
const { auth } = await getFirebaseAuthClient();
|
|
193
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;
|