cloudflare-next-intl 0.5.7 → 0.6.1

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.
Files changed (28) hide show
  1. package/README.md +64 -5
  2. package/dist/src/client/components/client_provider.js +2 -0
  3. package/dist/src/cookie_consent/gdpr_countries.d.ts +2 -2
  4. package/dist/src/cookie_consent/gdpr_countries.js +15 -4
  5. package/dist/src/error_handling/default_ignored_console_errors.d.ts +13 -0
  6. package/dist/src/error_handling/default_ignored_console_errors.js +27 -0
  7. package/dist/src/error_handling/format_error_message.d.ts +9 -0
  8. package/dist/src/error_handling/format_error_message.js +23 -0
  9. package/dist/src/error_handling/index.d.ts +9 -0
  10. package/dist/src/error_handling/index.js +6 -0
  11. package/dist/src/error_handling/install_console_error_override.bench.d.ts +1 -0
  12. package/dist/src/error_handling/install_console_error_override.bench.js +10 -0
  13. package/dist/src/error_handling/install_console_error_override.d.ts +29 -0
  14. package/dist/src/error_handling/install_console_error_override.js +49 -0
  15. package/dist/src/error_handling/report_error.bench.d.ts +1 -0
  16. package/dist/src/error_handling/report_error.bench.js +25 -0
  17. package/dist/src/error_handling/report_error.d.ts +34 -0
  18. package/dist/src/error_handling/report_error.js +81 -0
  19. package/dist/src/error_handling/stringify_unknown.bench.d.ts +1 -0
  20. package/dist/src/error_handling/stringify_unknown.bench.js +29 -0
  21. package/dist/src/error_handling/stringify_unknown.d.ts +12 -0
  22. package/dist/src/error_handling/stringify_unknown.js +42 -0
  23. package/dist/src/error_handling/with_error_handling.d.ts +15 -0
  24. package/dist/src/error_handling/with_error_handling.js +18 -0
  25. package/dist/src/firebase_auth/server/firebase_server.js +3 -1
  26. package/dist/src/server/components/server_provider.js +15 -4
  27. package/dist/src/types/types.d.ts +145 -25
  28. package/package.json +21 -1
package/README.md CHANGED
@@ -11,6 +11,8 @@ and Cloudflare environment.
11
11
  Components.
12
12
  - **Fast and Efficient**: Low overhead and minimal bundle size.
13
13
  - **Tree-shaking**: Properly architected for optimal tree-shaking.
14
+ - **Error handling**: shared, opt-in `console.error` override and
15
+ `reportError`/`withErrorHandling` helpers, GDPR-aware (consent-gated).
14
16
 
15
17
  ## Installation
16
18
 
@@ -200,16 +202,21 @@ import { getCloudflareContext } from "@opennextjs/cloudflare";
200
202
  export default setIntlConfig({
201
203
  locales: ["en", "de"],
202
204
  defaultLocale: "en",
205
+ // Shared request-time resolvers — used by cookieConsent's GDPR gating
206
+ // below AND by error_handling's ctx.waitUntil backgrounding.
207
+ generate: {
208
+ // Pass @opennextjs/cloudflare's getCloudflareContext directly — its
209
+ // exact overloaded signature is accepted as-is, called internally
210
+ // with { async: true } (cookieConsent) or { async: false } (error_handling).
211
+ getCloudflareContext,
212
+ },
203
213
  cookieConsent: {
204
214
  privacyPolicyDate: "2026-01-01",
205
215
  // privacyPolicyPath: "/privacy-policy", // default; used by the
206
216
  // dialogs' auto-rendered link. Set false to disable that link.
207
217
  // Optional: gate the banner to GDPR-region visitors only. Omit both
208
- // getters to disable country-based gating (consent always implicit).
209
- // Pass @opennextjs/cloudflare's getCloudflareContext directly — its
210
- // exact overloaded signature is accepted as-is, called internally
211
- // with { async: true }.
212
- getCloudflareContext,
218
+ // getCountryCode and generate.getCloudflareContext to disable
219
+ // country-based gating (consent always implicit).
213
220
  // gdprCountries: [...], // defaults to EU/EEA + UK + Switzerland
214
221
  // enableAnalyticsInDevMode: true, // analytics stay off in dev otherwise
215
222
  // autoWireDialogs: false, // opt out and render the dialogs yourself
@@ -246,6 +253,58 @@ const { consent, setConsent } = useCookieConsent();
246
253
 
247
254
  See [`package/src/cookie_consent/README.md`](package/src/cookie_consent/README.md) for layout, customization, and gotchas.
248
255
 
256
+ ### Error handling
257
+
258
+ Every risky call this package makes internally (Cloudflare-context
259
+ resolution, Firebase server auth, `cookieConsent.getAnalytics()`) reports
260
+ through a shared `error_handling` submodule you can also use in your own
261
+ app code. Enabled by default — no config needed to get the default
262
+ `console.error`-based reporting; set `errorHandling.onError` to plug in
263
+ your own transport (Sentry, Telegram, etc).
264
+
265
+ ```typescript
266
+ // intl-config.ts
267
+ export default setIntlConfig({
268
+ locales: ["en", "de"],
269
+ defaultLocale: "en",
270
+ generate: { getCloudflareContext }, // reports background via ctx.waitUntil when set
271
+ errorHandling: {
272
+ // enable: false, // fully disable reporting (errors still rethrow from withErrorHandling)
273
+ onError: ({ formattedMessage, error, classOrMethodName, consent }) => {
274
+ // formattedMessage is a ready-to-print "[classOrMethodName] Error: ..." string
275
+ myErrorTracker.capture(formattedMessage);
276
+ },
277
+ // overrideConsoleError: true, // route every console.error(...) call through onError too
278
+ // ignoreConsoleErrors: [...], // defaults to defaultIgnoredConsoleErrors (this package's
279
+ // // own Firebase Auth codes for expected user-input failures);
280
+ // // pass [] to report everything, or your own list to replace it
281
+ // ignoreConsoleError: (message) => message.includes("known noisy warning"),
282
+ },
283
+ });
284
+ ```
285
+
286
+ Use `reportError`/`withErrorHandling` directly in your own code:
287
+
288
+ ```typescript
289
+ import { reportError, withErrorHandling } from "cloudflare-next-intl/errorHandling";
290
+ import config from "./intl-config";
291
+
292
+ // Wrap a function — reports then rethrows on failure.
293
+ const safeFetch = withErrorHandling(fetchSomething, "fetchSomething", { config });
294
+
295
+ // Or report manually inside your own try/catch.
296
+ try {
297
+ await riskyThing();
298
+ } catch (error) {
299
+ await reportError(config, { error, classOrMethodName: "riskyThing" });
300
+ }
301
+ ```
302
+
303
+ **GDPR note:** pass `consent` (from `useCookieConsent()` or your own
304
+ server-side resolution) on `ErrorHandlingParams` — reporting is skipped
305
+ whenever `consent` is set and not `true`, since sending error reports to a
306
+ third party without consent can itself be GDPR-relevant.
307
+
249
308
  ## License
250
309
 
251
310
  MIT
@@ -4,6 +4,7 @@ import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_va
4
4
  import { createContext, useMemo } from "react";
5
5
  import dynamic from "next/dynamic";
6
6
  import config from "@intl-config";
7
+ import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
7
8
  export const LocaleContext = createContext(undefined);
8
9
  // Hoisted to module scope — calling `dynamic()` inside the component body
9
10
  // creates a brand-new component identity every render, forcing React to
@@ -20,6 +21,7 @@ const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/cli
20
21
  export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
21
22
  setLocaleCache(language);
22
23
  setMessageForLocaleCache(language, messages);
24
+ installConsoleErrorOverride(config, true);
23
25
  // `LocaleContext.Provider` stays the outermost element here — the
24
26
  // client `AuthUserProvider` (and its descendants calling
25
27
  // usePathname()/useLocale()) must render as a CHILD of it, not a
@@ -1,4 +1,4 @@
1
- import type { CookieConsentGetCloudflareContext } from '../types/types';
1
+ import type { CookieConsentGetCloudflareContext, ErrorHandlingRoutingConfig } from '../types/types';
2
2
  /**
3
3
  * Default `cookieConsent.gdprCountries` — EU/EEA member states (GDPR),
4
4
  * Iceland/Liechtenstein/Norway (EEA), the UK (UK-GDPR), and Switzerland
@@ -15,4 +15,4 @@ export declare const defaultGdprCountries: readonly string[];
15
15
  * `gdprCountries` skips the banner. `getCountryCode` takes precedence
16
16
  * over `getCloudflareContext` when both are set.
17
17
  */
18
- export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined): Promise<boolean>;
18
+ export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined, errorHandlingConfig?: ErrorHandlingRoutingConfig): Promise<boolean>;
@@ -1,3 +1,4 @@
1
+ import reportError from '../error_handling/report_error';
1
2
  /**
2
3
  * Default `cookieConsent.gdprCountries` — EU/EEA member states (GDPR),
3
4
  * Iceland/Liechtenstein/Norway (EEA), the UK (UK-GDPR), and Switzerland
@@ -40,12 +41,22 @@ function getGdprCountriesSet(gdprCountries) {
40
41
  * `gdprCountries` skips the banner. `getCountryCode` takes precedence
41
42
  * over `getCloudflareContext` when both are set.
42
43
  */
43
- export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries) {
44
+ export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries, errorHandlingConfig) {
44
45
  if (!getCountryCode && !getCloudflareContext)
45
46
  return true;
46
- const countryCode = getCountryCode
47
- ? await getCountryCode()
48
- : (await getCloudflareContext({ async: true }))?.cf?.country;
47
+ let countryCode;
48
+ if (getCountryCode) {
49
+ countryCode = await getCountryCode();
50
+ }
51
+ else {
52
+ try {
53
+ countryCode = (await getCloudflareContext({ async: true }))?.cf?.country;
54
+ }
55
+ catch (error) {
56
+ await reportError({ errorHandling: errorHandlingConfig, generate: { getCloudflareContext } }, { error, classOrMethodName: 'resolveRequiresConsent' });
57
+ return true;
58
+ }
59
+ }
49
60
  if (typeof countryCode !== 'string' || !countryCode)
50
61
  return true;
51
62
  return getGdprCountriesSet(gdprCountries).has(countryCode);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Firebase Auth error codes this package's own `createLoginAction`/
3
+ * `createSignUpAction`/`createForgotPasswordAction` already catch and
4
+ * translate into a localized message (see
5
+ * `firebase_auth/error_messages/firebase_auth_error_helper.ts`) — expected
6
+ * user-input failures (wrong password, email already in use, etc.), not
7
+ * bugs. They never reach `console.error`/`reportError` through this
8
+ * package's own code; this list exists as defense-in-depth for consumers
9
+ * whose own code logs one of these codes directly. Passing your own
10
+ * `ignoreConsoleError` array replaces this default entirely — pass `[]` to
11
+ * report everything.
12
+ */
13
+ export declare const defaultIgnoredConsoleErrors: readonly string[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Firebase Auth error codes this package's own `createLoginAction`/
3
+ * `createSignUpAction`/`createForgotPasswordAction` already catch and
4
+ * translate into a localized message (see
5
+ * `firebase_auth/error_messages/firebase_auth_error_helper.ts`) — expected
6
+ * user-input failures (wrong password, email already in use, etc.), not
7
+ * bugs. They never reach `console.error`/`reportError` through this
8
+ * package's own code; this list exists as defense-in-depth for consumers
9
+ * whose own code logs one of these codes directly. Passing your own
10
+ * `ignoreConsoleError` array replaces this default entirely — pass `[]` to
11
+ * report everything.
12
+ */
13
+ export const defaultIgnoredConsoleErrors = [
14
+ 'auth/invalid-email',
15
+ 'auth/user-disabled',
16
+ 'auth/user-not-found',
17
+ 'auth/wrong-password',
18
+ 'auth/invalid-credential',
19
+ 'auth/email-already-in-use',
20
+ 'auth/weak-password',
21
+ 'auth/too-many-requests',
22
+ 'auth/network-request-failed',
23
+ 'auth/requires-recent-login',
24
+ 'auth/expired-action-code',
25
+ 'auth/invalid-action-code',
26
+ 'auth/user-token-expired',
27
+ ];
@@ -0,0 +1,9 @@
1
+ import type { ErrorHandlingParams } from '../types/types';
2
+ /**
3
+ * Builds a human-readable one-string summary of an `ErrorHandlingParams` —
4
+ * `[classOrMethodName] Error: <message>` followed by non-empty `Params`/
5
+ * `IsClient` sections. Never throws (`stringifyUnknown` is safe). Used as
6
+ * `ErrorHandlingParams.formattedMessage` — read this instead of `error`/
7
+ * `params` directly when you just want something printable.
8
+ */
9
+ export default function formatErrorMessage(params: ErrorHandlingParams): string;
@@ -0,0 +1,23 @@
1
+ import stringifyUnknown from './stringify_unknown';
2
+ function formatSection(title, value, isClient) {
3
+ if (value === undefined)
4
+ return '';
5
+ const text = stringifyUnknown(value, isClient, true);
6
+ if (!text || text === '{}' || text === '[]')
7
+ return '';
8
+ return `\n${title}: ${text}`;
9
+ }
10
+ /**
11
+ * Builds a human-readable one-string summary of an `ErrorHandlingParams` —
12
+ * `[classOrMethodName] Error: <message>` followed by non-empty `Params`/
13
+ * `IsClient` sections. Never throws (`stringifyUnknown` is safe). Used as
14
+ * `ErrorHandlingParams.formattedMessage` — read this instead of `error`/
15
+ * `params` directly when you just want something printable.
16
+ */
17
+ export default function formatErrorMessage(params) {
18
+ const { error, classOrMethodName, params: extraParams, isClient } = params;
19
+ const errorText = stringifyUnknown(error, isClient);
20
+ const paramsSection = formatSection('Params', extraParams, isClient);
21
+ const clientSection = isClient ? '\nSource: client' : '';
22
+ return `[${classOrMethodName}] Error: ${errorText}${paramsSection}${clientSection}`;
23
+ }
@@ -0,0 +1,9 @@
1
+ export { default as withErrorHandling } from './with_error_handling';
2
+ export type { WithErrorHandlingOptions } from './with_error_handling';
3
+ export { default as reportError } from './report_error';
4
+ export type { ReportErrorConfig } from './report_error';
5
+ export { default as installConsoleErrorOverride } from './install_console_error_override';
6
+ export { default as stringifyUnknown } from './stringify_unknown';
7
+ export { default as formatErrorMessage } from './format_error_message';
8
+ export { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
9
+ export type { ErrorHandlingParams, ErrorHandlingRoutingConfig } from '../types/types';
@@ -0,0 +1,6 @@
1
+ export { default as withErrorHandling } from './with_error_handling';
2
+ export { default as reportError } from './report_error';
3
+ export { default as installConsoleErrorOverride } from './install_console_error_override';
4
+ export { default as stringifyUnknown } from './stringify_unknown';
5
+ export { default as formatErrorMessage } from './format_error_message';
6
+ export { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
@@ -0,0 +1,10 @@
1
+ import { bench, describe } from 'vitest';
2
+ import installConsoleErrorOverride from './install_console_error_override';
3
+ describe('installConsoleErrorOverride: repeated install calls (no-op after first)', () => {
4
+ const originalConsoleError = console.error;
5
+ console.error = () => { };
6
+ bench('install() called repeatedly (marker check short-circuits)', () => {
7
+ installConsoleErrorOverride({ errorHandling: { overrideConsoleError: true } });
8
+ });
9
+ console.error = originalConsoleError;
10
+ });
@@ -0,0 +1,29 @@
1
+ import { type ReportErrorConfig } from './report_error';
2
+ /**
3
+ * Replaces the global `console.error` so every `console.error(...)` call is
4
+ * also routed through `config.errorHandling.onError`/`reportError` — the
5
+ * original `console.error` still runs afterwards, nothing is swallowed.
6
+ * Safe to call more than once (a no-op after the first call in this JS
7
+ * realm — call it separately on the server and on the client, each has its
8
+ * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
9
+ * is `true`.
10
+ *
11
+ * A component stuck in a render-error loop calls `console.error` on every
12
+ * render — `reportError`'s own dedup/cap (on by default, see
13
+ * `errorHandling.dedup`/`maxReports`) is what stops that from reporting
14
+ * unboundedly; this function does not duplicate that cap itself.
15
+ *
16
+ * `config.errorHandling.ignoreConsoleErrors` (default
17
+ * `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
18
+ * codes for expected user-input failures) and `ignoreConsoleError` both
19
+ * skip reporting a matching call while still logging it normally.
20
+ *
21
+ * @param config Pass the relevant slices of your `RoutingConfig` directly —
22
+ * `{ errorHandling: config.errorHandling, generate: config.generate }`.
23
+ * @param isClient Passed through to every report's `ErrorHandlingParams.isClient`
24
+ * — set `true` when installing from client-side code (e.g. the client
25
+ * `LocationzationClientProvider`), omit/`false` on the server. There's no
26
+ * `getCloudflareContext`/`ctx.waitUntil` available in the browser, so
27
+ * client-side reports always await `onError` directly.
28
+ */
29
+ export default function installConsoleErrorOverride(config: ReportErrorConfig | undefined, isClient?: boolean): void;
@@ -0,0 +1,49 @@
1
+ import reportError from './report_error';
2
+ import stringifyUnknown from './stringify_unknown';
3
+ import { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
4
+ /**
5
+ * Replaces the global `console.error` so every `console.error(...)` call is
6
+ * also routed through `config.errorHandling.onError`/`reportError` — the
7
+ * original `console.error` still runs afterwards, nothing is swallowed.
8
+ * Safe to call more than once (a no-op after the first call in this JS
9
+ * realm — call it separately on the server and on the client, each has its
10
+ * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
11
+ * is `true`.
12
+ *
13
+ * A component stuck in a render-error loop calls `console.error` on every
14
+ * render — `reportError`'s own dedup/cap (on by default, see
15
+ * `errorHandling.dedup`/`maxReports`) is what stops that from reporting
16
+ * unboundedly; this function does not duplicate that cap itself.
17
+ *
18
+ * `config.errorHandling.ignoreConsoleErrors` (default
19
+ * `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
20
+ * codes for expected user-input failures) and `ignoreConsoleError` both
21
+ * skip reporting a matching call while still logging it normally.
22
+ *
23
+ * @param config Pass the relevant slices of your `RoutingConfig` directly —
24
+ * `{ errorHandling: config.errorHandling, generate: config.generate }`.
25
+ * @param isClient Passed through to every report's `ErrorHandlingParams.isClient`
26
+ * — set `true` when installing from client-side code (e.g. the client
27
+ * `LocationzationClientProvider`), omit/`false` on the server. There's no
28
+ * `getCloudflareContext`/`ctx.waitUntil` available in the browser, so
29
+ * client-side reports always await `onError` directly.
30
+ */
31
+ export default function installConsoleErrorOverride(config, isClient) {
32
+ if (config?.errorHandling?.overrideConsoleError !== true)
33
+ return;
34
+ if (console.error.__isErrorHandlingOverride)
35
+ return;
36
+ const originalConsoleError = console.error.bind(console);
37
+ const override = (message, ...optionalParams) => {
38
+ originalConsoleError(message, ...optionalParams);
39
+ const stringified = stringifyUnknown(message, isClient);
40
+ const ignoreList = config.errorHandling?.ignoreConsoleErrors ?? defaultIgnoredConsoleErrors;
41
+ if (ignoreList.some((ignored) => stringified.includes(ignored)))
42
+ return;
43
+ if (config.errorHandling?.ignoreConsoleError?.(stringified))
44
+ return;
45
+ void reportError(config, { error: message, classOrMethodName: 'Global Console Error Handler', params: optionalParams, isClient });
46
+ };
47
+ override.__isErrorHandlingOverride = true;
48
+ console.error = override;
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { bench, describe } from 'vitest';
2
+ import reportError from './report_error';
3
+ const noopOnError = () => { };
4
+ const waitUntil = () => { };
5
+ const fakeGetCloudflareContext = (() => ({ ctx: { waitUntil } }));
6
+ describe('reportError', () => {
7
+ bench('default console.error path (no config)', async () => {
8
+ const original = console.error;
9
+ console.error = noopOnError;
10
+ await reportError(undefined, { error: new Error('boom'), classOrMethodName: 'bench' });
11
+ console.error = original;
12
+ });
13
+ bench('custom onError, no getCloudflareContext (awaited inline)', async () => {
14
+ await reportError({ errorHandling: { onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench' });
15
+ });
16
+ bench('backgrounded via ctx.waitUntil', async () => {
17
+ await reportError({ errorHandling: { onError: noopOnError }, generate: { getCloudflareContext: fakeGetCloudflareContext } }, { error: new Error('boom'), classOrMethodName: 'bench' });
18
+ });
19
+ bench('skipped entirely (enable: false, no formatting cost paid)', async () => {
20
+ await reportError({ errorHandling: { enable: false, onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench' });
21
+ });
22
+ bench('skipped entirely (consent: false)', async () => {
23
+ await reportError({ errorHandling: { onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench', consent: false });
24
+ });
25
+ });
@@ -0,0 +1,34 @@
1
+ import type { ErrorHandlingParams, ErrorHandlingRoutingConfig, GenerateRoutingConfig } from '../types/types';
2
+ export interface ReportErrorConfig {
3
+ errorHandling?: ErrorHandlingRoutingConfig;
4
+ generate?: GenerateRoutingConfig;
5
+ }
6
+ /**
7
+ * Reports `params` via `config.errorHandling.onError` (default
8
+ * `console.error(params.formattedMessage)`), unless
9
+ * `config.errorHandling.enable === false`, `params.consent` is set and not
10
+ * `true` (reporting to a third party without cookie consent can itself be
11
+ * GDPR-relevant), or dedup throttles it (on by default — see
12
+ * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
13
+ * reporter must not mask the original error.
14
+ *
15
+ * Always overwrites `params.formattedMessage` with a fresh
16
+ * `formatErrorMessage(params)` before reporting — a human-readable one-line
17
+ * summary (`[classOrMethodName] Error: <message>` plus non-empty sections)
18
+ * instead of the raw `error`/`params` object, for a default reporter (or a
19
+ * simple `onError`) to print directly.
20
+ *
21
+ * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
22
+ * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
23
+ * Cloudflare Workers only extends the request's lifetime for work already
24
+ * registered with `waitUntil` by the time the handler returns; deferring
25
+ * that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
26
+ * risks the isolate tearing down the request before `waitUntil` is ever
27
+ * actually invoked, silently dropping the report. Falls back to awaiting
28
+ * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
29
+ * unavailable (e.g. outside a Cloudflare Worker).
30
+ *
31
+ * @param config Pass the relevant slices of your `RoutingConfig` directly —
32
+ * `{ errorHandling: config.errorHandling, generate: config.generate }`.
33
+ */
34
+ export default function reportError(config: ReportErrorConfig | undefined, params: ErrorHandlingParams): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import formatErrorMessage from './format_error_message';
2
+ import stringifyUnknown from './stringify_unknown';
3
+ const DEFAULT_THROTTLE_MS = 5000;
4
+ // Module-scope dedup state — safe by default only because a fresh JS realm
5
+ // (isolate/Worker instance) starts with it cleared. In a long-lived server
6
+ // process reused across many requests, pass `resetDedup: true` on the first
7
+ // `reportError` call of each request/cron tick, or one request's errors can
8
+ // suppress another's.
9
+ let lastDedupKey = null;
10
+ let lastReportedAt = 0;
11
+ function buildDedupKey(params) {
12
+ return params.dedupKey ?? `${params.classOrMethodName} ${stringifyUnknown(params.error, params.isClient)} ${params.params ? stringifyUnknown(params.params, params.isClient) : ''}`;
13
+ }
14
+ async function callOnError(config, params) {
15
+ const paramsWithFormattedMessage = { ...params, formattedMessage: formatErrorMessage(params) };
16
+ try {
17
+ if (config?.onError) {
18
+ await config.onError(paramsWithFormattedMessage);
19
+ }
20
+ else {
21
+ console.error(paramsWithFormattedMessage.formattedMessage);
22
+ }
23
+ }
24
+ catch {
25
+ console.error(paramsWithFormattedMessage.formattedMessage);
26
+ }
27
+ }
28
+ /**
29
+ * Reports `params` via `config.errorHandling.onError` (default
30
+ * `console.error(params.formattedMessage)`), unless
31
+ * `config.errorHandling.enable === false`, `params.consent` is set and not
32
+ * `true` (reporting to a third party without cookie consent can itself be
33
+ * GDPR-relevant), or dedup throttles it (on by default — see
34
+ * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
35
+ * reporter must not mask the original error.
36
+ *
37
+ * Always overwrites `params.formattedMessage` with a fresh
38
+ * `formatErrorMessage(params)` before reporting — a human-readable one-line
39
+ * summary (`[classOrMethodName] Error: <message>` plus non-empty sections)
40
+ * instead of the raw `error`/`params` object, for a default reporter (or a
41
+ * simple `onError`) to print directly.
42
+ *
43
+ * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
44
+ * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
45
+ * Cloudflare Workers only extends the request's lifetime for work already
46
+ * registered with `waitUntil` by the time the handler returns; deferring
47
+ * that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
48
+ * risks the isolate tearing down the request before `waitUntil` is ever
49
+ * actually invoked, silently dropping the report. Falls back to awaiting
50
+ * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
51
+ * unavailable (e.g. outside a Cloudflare Worker).
52
+ *
53
+ * @param config Pass the relevant slices of your `RoutingConfig` directly —
54
+ * `{ errorHandling: config.errorHandling, generate: config.generate }`.
55
+ */
56
+ export default async function reportError(config, params) {
57
+ const errorHandling = config?.errorHandling;
58
+ if (errorHandling?.resetDedup) {
59
+ lastDedupKey = null;
60
+ lastReportedAt = 0;
61
+ }
62
+ if (errorHandling?.enable === false)
63
+ return;
64
+ if (params.consent !== undefined && params.consent !== true)
65
+ return;
66
+ if (errorHandling?.dedup !== false) {
67
+ const throttleMs = errorHandling?.throttleMs ?? DEFAULT_THROTTLE_MS;
68
+ const dedupKey = buildDedupKey(params);
69
+ const now = Date.now();
70
+ if (dedupKey === lastDedupKey && now - lastReportedAt < throttleMs)
71
+ return;
72
+ lastDedupKey = dedupKey;
73
+ lastReportedAt = now;
74
+ }
75
+ const waitUntil = config?.generate?.getCloudflareContext?.({ async: false })?.ctx?.waitUntil;
76
+ if (waitUntil) {
77
+ waitUntil(callOnError(errorHandling, params));
78
+ return;
79
+ }
80
+ await callOnError(errorHandling, params);
81
+ }
@@ -0,0 +1,29 @@
1
+ import { bench, describe } from 'vitest';
2
+ import stringifyUnknown from './stringify_unknown';
3
+ const error = new Error('boom');
4
+ const plainObject = { a: 1, b: { c: 2, d: [1, 2, 3] } };
5
+ const circular = {};
6
+ circular.self = circular;
7
+ describe('stringifyUnknown', () => {
8
+ bench('string passthrough', () => {
9
+ stringifyUnknown('already a string');
10
+ });
11
+ bench('Error instance', () => {
12
+ stringifyUnknown(error);
13
+ });
14
+ bench('function-wrapped lazy error (server, resolved)', () => {
15
+ stringifyUnknown(() => 'lazy boom');
16
+ });
17
+ bench('function-wrapped lazy error (client, not resolved)', () => {
18
+ stringifyUnknown(() => 'lazy boom', true);
19
+ });
20
+ bench('plain object, pretty-printed', () => {
21
+ stringifyUnknown(plainObject);
22
+ });
23
+ bench('plain object, nested (compact)', () => {
24
+ stringifyUnknown(plainObject, false, true);
25
+ });
26
+ bench('circular object (falls back to [Unserializable value])', () => {
27
+ stringifyUnknown(circular, false, true);
28
+ });
29
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Safely converts an `unknown` value (typically `ErrorHandlingParams.error`)
3
+ * into a string, for logging/dedup-keying/display — never throws.
4
+ *
5
+ * @param isClient Skips resolving function-wrapped/lazy error values on the
6
+ * client (matches `ErrorHandlingParams.isClient`) — running arbitrary
7
+ * caught functions client-side isn't safe the way it is on the server.
8
+ * @param isNested Set when stringifying a value nested inside another
9
+ * object/array — falls back to a plain `JSON.stringify` (no pretty-print)
10
+ * so a single unserializable nested value can't crash the whole report.
11
+ */
12
+ export default function stringifyUnknown(value: unknown, isClient?: boolean, isNested?: boolean): string;
@@ -0,0 +1,42 @@
1
+ const MAX_FUNCTION_RESOLUTION_ATTEMPTS = 5;
2
+ function resolveFunctionError(value) {
3
+ let result = value;
4
+ try {
5
+ for (let i = 0; i < MAX_FUNCTION_RESOLUTION_ATTEMPTS && typeof result === 'function'; i++) {
6
+ result = result();
7
+ }
8
+ return result;
9
+ }
10
+ catch (error) {
11
+ return `Error during function resolution: ${String(error)}`;
12
+ }
13
+ }
14
+ /**
15
+ * Safely converts an `unknown` value (typically `ErrorHandlingParams.error`)
16
+ * into a string, for logging/dedup-keying/display — never throws.
17
+ *
18
+ * @param isClient Skips resolving function-wrapped/lazy error values on the
19
+ * client (matches `ErrorHandlingParams.isClient`) — running arbitrary
20
+ * caught functions client-side isn't safe the way it is on the server.
21
+ * @param isNested Set when stringifying a value nested inside another
22
+ * object/array — falls back to a plain `JSON.stringify` (no pretty-print)
23
+ * so a single unserializable nested value can't crash the whole report.
24
+ */
25
+ export default function stringifyUnknown(value, isClient, isNested = false) {
26
+ if (typeof value === 'string')
27
+ return value;
28
+ if (value instanceof Error)
29
+ return `${value.name}: ${value.message}\n\n${value.stack ?? ''}`;
30
+ if (typeof value === 'function') {
31
+ if (isClient)
32
+ return '[Function]';
33
+ const resolved = resolveFunctionError(value);
34
+ return typeof resolved !== 'function' ? stringifyUnknown(resolved, isClient) : '[Function]';
35
+ }
36
+ try {
37
+ return isNested ? JSON.stringify(value) : JSON.stringify(value, null, 2);
38
+ }
39
+ catch {
40
+ return '[Unserializable value]';
41
+ }
42
+ }
@@ -0,0 +1,15 @@
1
+ import type { ConsentValue } from '../cookie_consent/types';
2
+ import { type ReportErrorConfig } from './report_error';
3
+ export interface WithErrorHandlingOptions {
4
+ /** Pass the relevant slices of your `RoutingConfig` directly — `{ errorHandling: config.errorHandling, generate: config.generate }`. */
5
+ config?: ReportErrorConfig;
6
+ params?: unknown;
7
+ isClient?: boolean;
8
+ consent?: ConsentValue;
9
+ }
10
+ /**
11
+ * Wraps `fn`, reporting (via `options.config`) then rethrowing any error it
12
+ * throws or rejects with. Never swallows — callers keep their own
13
+ * catch/fallback behavior, this only adds reporting on top.
14
+ */
15
+ export default function withErrorHandling<Args extends unknown[], Result>(fn: (...args: Args) => Result | Promise<Result>, classOrMethodName: string, options?: WithErrorHandlingOptions): (...args: Args) => Promise<Result>;
@@ -0,0 +1,18 @@
1
+ import reportError from './report_error';
2
+ /**
3
+ * Wraps `fn`, reporting (via `options.config`) then rethrowing any error it
4
+ * throws or rejects with. Never swallows — callers keep their own
5
+ * catch/fallback behavior, this only adds reporting on top.
6
+ */
7
+ export default function withErrorHandling(fn, classOrMethodName, options = {}) {
8
+ const { config, params, isClient, consent } = options;
9
+ return async (...args) => {
10
+ try {
11
+ return await fn(...args);
12
+ }
13
+ catch (error) {
14
+ await reportError(config, { error, classOrMethodName, params, isClient, consent });
15
+ throw error;
16
+ }
17
+ };
18
+ }
@@ -3,6 +3,7 @@ import { cache } from 'react';
3
3
  import config from '@intl-config';
4
4
  import requireFirebaseAuthConfig from '../require_config';
5
5
  import { defaultSessionCookieName } from '../middleware/update_session';
6
+ import reportError from '../../error_handling/report_error';
6
7
  let baseApp;
7
8
  let firebaseAppModule;
8
9
  let firebaseAuthModule;
@@ -51,7 +52,8 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
51
52
  await auth.authStateReady();
52
53
  return { firebaseServerApp, currentUser: auth.currentUser };
53
54
  }
54
- catch {
55
+ catch (error) {
56
+ await reportError(config, { error, classOrMethodName: 'getAuthenticatedAppForUser' });
55
57
  return { firebaseServerApp: null, currentUser: null };
56
58
  }
57
59
  });
@@ -5,6 +5,8 @@ import dynamic from "next/dynamic";
5
5
  import { localesSet } from "../../config/middleware";
6
6
  import config from "../../config/intl_config";
7
7
  import resolveRequiresConsent from "../../cookie_consent/gdpr_countries";
8
+ import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
9
+ import reportError from "../../error_handling/report_error";
8
10
  const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider"));
9
11
  let authUserServerProviderModule;
10
12
  /**
@@ -48,6 +50,7 @@ export default async function LocationzationProvider({ language, messages, child
48
50
  setMessageForLocaleCache(language, messages);
49
51
  }
50
52
  const messagesValue = messages ?? await getMessage(language);
53
+ installConsoleErrorOverride(config);
51
54
  let initialAuthUser = null;
52
55
  const autoWireClientProvider = config.firebaseAuth?.autoWireClientProvider !== false;
53
56
  if (config.firebaseAuth && autoWireClientProvider) {
@@ -68,13 +71,21 @@ export default async function LocationzationProvider({ language, messages, child
68
71
  // `getCloudflareContext` path in dev; fail-safe to `true`
69
72
  // (banner shown) same as an unresolved country would.
70
73
  requiresConsent = !isDevEnvironment
71
- ? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.cookieConsent.getCloudflareContext, config.cookieConsent.gdprCountries)
74
+ ? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
72
75
  : false;
73
76
  const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
74
77
  if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
75
- analyticsConfig = config.cookieConsent.getAnalytics
76
- ? await config.cookieConsent.getAnalytics()
77
- : config.cookieConsent.analytics;
78
+ if (config.cookieConsent.getAnalytics) {
79
+ try {
80
+ analyticsConfig = await config.cookieConsent.getAnalytics();
81
+ }
82
+ catch (error) {
83
+ await reportError(config, { error, classOrMethodName: 'getAnalytics' });
84
+ }
85
+ }
86
+ else {
87
+ analyticsConfig = config.cookieConsent.analytics;
88
+ }
78
89
  }
79
90
  }
80
91
  return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, skipAuthProvider: !autoWireClientProvider, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
@@ -3,6 +3,7 @@ import type { Languages } from 'next/dist/lib/metadata/types/alternative-urls-ty
3
3
  import type { Videos } from 'next/dist/lib/metadata/types/metadata-types';
4
4
  import type { CookieConsentDialogProps } from '../cookie_consent/client/components/cookie_consent_dialog';
5
5
  import type { PrivacyPolicyUpdateDialogProps } from '../cookie_consent/client/components/privacy_policy_update_dialog';
6
+ import type { ConsentValue } from '../cookie_consent/types';
6
7
  /**
7
8
  * Custom middleware hook, run by `intlMiddleware` for your own logic
8
9
  * (e.g. auth, feature flags, A/B tests) — on top of the library's own
@@ -90,6 +91,137 @@ export interface RoutingConfig<AppLocales extends Locales, AppLocalePrefixMode e
90
91
  * error if called without this set.
91
92
  */
92
93
  cookieConsent?: CookieConsentRoutingConfig;
94
+ /**
95
+ * Request-time resolvers shared across submodules. Omit entirely to
96
+ * leave all of them unset.
97
+ */
98
+ generate?: GenerateRoutingConfig;
99
+ /**
100
+ * Configures the optional `error_handling` submodule (shared
101
+ * `withErrorHandling`/`reportError` helpers used internally by this
102
+ * package and available to your own app code). Omit entirely to keep
103
+ * the defaults: enabled, reporting via `console.error`.
104
+ */
105
+ errorHandling?: ErrorHandlingRoutingConfig;
106
+ }
107
+ export interface GenerateRoutingConfig {
108
+ /**
109
+ * Pass `getCloudflareContext` from `@opennextjs/cloudflare` directly
110
+ * (not a dependency of this package, so bring your own import) — its
111
+ * exact overloaded signature is accepted as-is; called internally with
112
+ * `{ async: true }`, so you never need to wrap it yourself. Only
113
+ * `cf.country` is read from the resolved context by `cookieConsent`.
114
+ *
115
+ * Country-based gating (via either `cookieConsent.getCountryCode` or
116
+ * this getter) decides whether the cookie-consent banner is required at
117
+ * all: visitors outside `gdprCountries` skip the banner and get
118
+ * analytics immediately (still gated by `enableAnalyticsInDevMode`).
119
+ * Omit BOTH to require consent for everyone (fail-safe default — the
120
+ * visitor's country can't be determined at all without either getter).
121
+ * Set one of the two getters to scope the banner to GDPR regions only.
122
+ */
123
+ getCloudflareContext?: CookieConsentGetCloudflareContext;
124
+ }
125
+ export interface ErrorHandlingParams {
126
+ /** The caught error, in whatever shape it was thrown/rejected with. */
127
+ error: unknown;
128
+ /** Name of the function/method the error was caught in, e.g. `"resolveRequiresConsent"`. */
129
+ classOrMethodName: string;
130
+ /** Extra context to include in the report (arguments, request info, etc). */
131
+ params?: unknown;
132
+ /** Whether this error originated in a client-side (browser) call. */
133
+ isClient?: boolean;
134
+ /**
135
+ * The visitor's current cookie-consent value (from `useCookieConsent()`
136
+ * or your own server-side resolution), when known. When passed and not
137
+ * `true`, `reportError`/`withErrorHandling` skip reporting entirely —
138
+ * sending error reports to a third party (Telegram, Sentry, etc.)
139
+ * without consent can itself be GDPR-relevant. Omit when consent isn't
140
+ * applicable (e.g. `cookieConsent` isn't configured at all).
141
+ */
142
+ consent?: ConsentValue;
143
+ /**
144
+ * Human-readable one-string summary — `[classOrMethodName] Error:
145
+ * <message>` plus non-empty `Params`/client-origin sections. Set by
146
+ * `reportError` before calling `onError`/`console.error`; read this
147
+ * instead of `error`/`params` directly when you just want something
148
+ * printable. Ignore when passing `params` to `withErrorHandling`
149
+ * yourself — it's always overwritten.
150
+ */
151
+ formattedMessage?: string;
152
+ /**
153
+ * Key used by `config.errorHandling.dedupGate` to dedup this report
154
+ * against the immediately preceding one. Defaults to
155
+ * `` `${classOrMethodName} ${stringifyUnknown(error)} ${stringifyUnknown(params ?? '')}` ``
156
+ * when omitted (built by `reportError` itself) — set this explicitly
157
+ * only if you want a coarser/different dedup key.
158
+ */
159
+ dedupKey?: string;
160
+ }
161
+ export interface ErrorHandlingRoutingConfig {
162
+ /**
163
+ * Whether errors caught by this package's `withErrorHandling`/
164
+ * `reportError` helpers are reported at all. Defaults to `true`. Set
165
+ * `false` to fully disable reporting (errors are still rethrown by
166
+ * `withErrorHandling`, just never reported).
167
+ */
168
+ enable?: boolean;
169
+ /**
170
+ * Called with the caught error whenever one is reported. Defaults to
171
+ * `console.error`. Use this to wire your own error-tracking/logging
172
+ * transport (Sentry, Telegram, etc).
173
+ */
174
+ onError?: (params: ErrorHandlingParams) => void | Promise<void>;
175
+ /**
176
+ * Whether `reportError`/`withErrorHandling` replace the global
177
+ * `console.error` so every `console.error(...)` call in your app is
178
+ * also routed through `onError` (the original `console.error` still
179
+ * runs afterwards — nothing is swallowed). Defaults to `false`; call
180
+ * `installConsoleErrorOverride()` (or pass this `true` and call
181
+ * `IntlProvider`/`setIntlConfig`'s setup) to install it. Off by default
182
+ * since this package is shared across apps and a global override is a
183
+ * bigger behavior change than a plain function call.
184
+ */
185
+ overrideConsoleError?: boolean;
186
+ /**
187
+ * Substrings matched against the stringified message of each
188
+ * `console.error(...)` call (only consulted when `overrideConsoleError`
189
+ * is `true`) — a match skips reporting it (it's still logged normally).
190
+ * Defaults to `defaultIgnoredConsoleErrors` (this package's own Firebase
191
+ * Auth error codes for expected user-input failures — wrong password,
192
+ * email already in use, etc). Pass your own array to replace the
193
+ * default entirely; pass `[]` to report everything.
194
+ */
195
+ ignoreConsoleErrors?: readonly string[];
196
+ /**
197
+ * Called with the stringified message of each `console.error(...)` call
198
+ * (only consulted when `overrideConsoleError` is `true`), in addition to
199
+ * `ignoreConsoleErrors` — return `true` to skip reporting it (it's still
200
+ * logged normally). Use this for custom filtering logic beyond a plain
201
+ * substring match.
202
+ */
203
+ ignoreConsoleError?: (message: string) => boolean;
204
+ /**
205
+ * Dedup: `reportError` skips reporting an error whose key (`dedupKey`,
206
+ * or a built-in key derived from `classOrMethodName`/`error`/`params`
207
+ * when omitted) matches the immediately preceding reported error's key,
208
+ * within `throttleMs`. On by default (matches this package's own
209
+ * internal call sites and `installConsoleErrorOverride`'s console-loop
210
+ * guard). Set `false` to report every distinct call with no dedup at
211
+ * all.
212
+ *
213
+ * The dedup state lives inside `reportError`'s own module — this is
214
+ * shared mutable state, safe by default only because a fresh JS realm
215
+ * (isolate/Worker instance) starts with it cleared; in a long-lived
216
+ * server process reused across many requests, pass `resetDedup: true`
217
+ * on the first `reportError` call of each request/cron tick to clear it
218
+ * (otherwise one request's errors can suppress another's).
219
+ */
220
+ dedup?: boolean;
221
+ /** Throttle window in ms: the same dedup key reported again within this window is skipped. Defaults to `5000`. Only consulted when `dedup` isn't `false`. */
222
+ throttleMs?: number;
223
+ /** Clears the dedup last-key/timestamp state before processing this call. Pass `true` on the first `reportError` call of each request/cron tick in a long-lived server process. */
224
+ resetDedup?: boolean;
93
225
  }
94
226
  export interface CookieConsentRoutingConfig {
95
227
  /**
@@ -145,27 +277,10 @@ export interface CookieConsentRoutingConfig {
145
277
  * over `getCloudflareContext` when both are set.
146
278
  */
147
279
  getCountryCode?: () => string | undefined | Promise<string | undefined>;
148
- /**
149
- * Pass `getCloudflareContext` from `@opennextjs/cloudflare` directly
150
- * (not a dependency of this package, so bring your own import) — its
151
- * exact overloaded signature is accepted as-is; called internally with
152
- * `{ async: true }`, so you never need to wrap it yourself. Only
153
- * `cf.country` is read from the resolved context. Ignored when
154
- * `getCountryCode` is also set.
155
- *
156
- * Country-based gating (via either `getCountryCode` or
157
- * `getCloudflareContext`) decides whether the cookie-consent banner is
158
- * required at all: visitors outside `gdprCountries` skip the banner and
159
- * get analytics immediately (still gated by `enableAnalyticsInDevMode`).
160
- * Omit BOTH to require consent for everyone (fail-safe default — the
161
- * visitor's country can't be determined at all without either getter).
162
- * Set one of the two getters to scope the banner to GDPR regions only.
163
- */
164
- getCloudflareContext?: CookieConsentGetCloudflareContext;
165
280
  /**
166
281
  * Country codes (ISO 3166-1 alpha-2) for which the cookie-consent banner
167
282
  * is required. Only consulted when `getCountryCode` or
168
- * `getCloudflareContext` is set. Defaults to the EU/EEA + UK +
283
+ * `generate.getCloudflareContext` is set. Defaults to the EU/EEA + UK +
169
284
  * Switzerland (GDPR/UK-GDPR/nFADP scope). A visitor whose resolved
170
285
  * country isn't in this set is treated as NOT requiring consent; a
171
286
  * country that couldn't be resolved still requires it (fail-safe:
@@ -203,16 +318,21 @@ export interface CookieConsentRoutingConfig {
203
318
  updateDialogProps?: PrivacyPolicyUpdateDialogProps;
204
319
  }
205
320
  /**
206
- * Minimal shape read from your `getCloudflareContext()` return value — only
207
- * `cf.country` is consulted (read defensively at the call site, since `cf`'s
208
- * real type — `@opennextjs/cloudflare`'s `CfProperties`, a union of the
209
- * incoming-request and request-init variants — only has `country` on one
210
- * branch). `cf` is typed loosely here so the real (generic) function is
211
- * assignable to `CookieConsentGetCloudflareContext` without a hard
212
- * dependency on that package.
321
+ * Minimal shape read from your `getCloudflareContext()` return value.
322
+ * `cf.country` is consulted by `cookieConsent` (read defensively at the call
323
+ * site, since `cf`'s real type — `@opennextjs/cloudflare`'s `CfProperties`,
324
+ * a union of the incoming-request and request-init variants — only has
325
+ * `country` on one branch); `ctx.waitUntil` is used by `error_handling` to
326
+ * background error reports instead of awaiting them inline. Typed loosely
327
+ * here so the real (generic) function is assignable to
328
+ * `CookieConsentGetCloudflareContext` without a hard dependency on that
329
+ * package.
213
330
  */
214
331
  export interface CookieConsentCloudflareContext {
215
332
  cf?: Record<string, unknown>;
333
+ ctx?: {
334
+ waitUntil?: (promise: Promise<unknown>) => void;
335
+ };
216
336
  }
217
337
  /**
218
338
  * Matches `@opennextjs/cloudflare`'s `getCloudflareContext` overloaded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.5.7",
3
+ "version": "0.6.1",
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",
@@ -139,6 +139,26 @@
139
139
  "./cookieConsentAnalytics": {
140
140
  "types": "./dist/src/cookie_consent/client/components/cookie_consent_analytics.d.ts",
141
141
  "import": "./dist/src/cookie_consent/client/components/cookie_consent_analytics.js"
142
+ },
143
+ "./errorHandling": {
144
+ "types": "./dist/src/error_handling/index.d.ts",
145
+ "import": "./dist/src/error_handling/index.js"
146
+ },
147
+ "./installConsoleErrorOverride": {
148
+ "types": "./dist/src/error_handling/install_console_error_override.d.ts",
149
+ "import": "./dist/src/error_handling/install_console_error_override.js"
150
+ },
151
+ "./stringifyUnknown": {
152
+ "types": "./dist/src/error_handling/stringify_unknown.d.ts",
153
+ "import": "./dist/src/error_handling/stringify_unknown.js"
154
+ },
155
+ "./formatErrorMessage": {
156
+ "types": "./dist/src/error_handling/format_error_message.d.ts",
157
+ "import": "./dist/src/error_handling/format_error_message.js"
158
+ },
159
+ "./defaultIgnoredConsoleErrors": {
160
+ "types": "./dist/src/error_handling/default_ignored_console_errors.d.ts",
161
+ "import": "./dist/src/error_handling/default_ignored_console_errors.js"
142
162
  }
143
163
  },
144
164
  "scripts": {