cloudflare-next-intl 0.9.10 → 0.9.12

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 CHANGED
@@ -940,9 +940,11 @@ allowed to see the rows; call `.transaction(...)` on the handle either one
940
940
  hands your callback when a write needs more than one statement to succeed or
941
941
  fail together (see [Multi-statement transactions](#multi-statement-transactions-dbtransaction) above):
942
942
 
943
- - `withPublicDb(fn, dbOverride?)` — runs `fn` as the anonymous role: a pooled
944
- connection with no transaction/role switch in connection-string mode, or the
945
- anon key as the PostgREST bearer token in Supabase mode. No user id is
943
+ - `withPublicDb(fn, dbOverride?)` — runs `fn` as the `anon` Postgres role.
944
+ In connection-string mode (Hyperdrive) the connection-string user is
945
+ temporarily downgraded to `anon` via `SET LOCAL ROLE anon` before your
946
+ callback executes, so RLS policies apply identically to the Supabase-mode
947
+ path (where the anon key is the PostgREST bearer token). No user id is
946
948
  attached either way, so RLS policies that test `auth.jwt()->>'sub'` will
947
949
  deny access — use `withUserDb` for user-owned rows.
948
950
  - `withUserDb(fn, uid?, dbOverride?)` — runs `fn` as the signed-in user. In
@@ -3,3 +3,5 @@ export declare const isBotCookieKey = "__is_bot_key__";
3
3
  export declare const isDarkCookieKey = "__is_dark_key__";
4
4
  export declare const cookieConsentCookieKey = "__cookie_consent_key__";
5
5
  export declare const privacyPolicyDateCookieKey = "__privacy_policy_date_key__";
6
+ export declare const countryCookieKey = "__cf_country__";
7
+ export declare const timezoneCookieKey = "__cf_timezone__";
@@ -3,3 +3,5 @@ export const isBotCookieKey = '__is_bot_key__';
3
3
  export const isDarkCookieKey = '__is_dark_key__';
4
4
  export const cookieConsentCookieKey = '__cookie_consent_key__';
5
5
  export const privacyPolicyDateCookieKey = '__privacy_policy_date_key__';
6
+ export const countryCookieKey = '__cf_country__';
7
+ export const timezoneCookieKey = '__cf_timezone__';
@@ -1,9 +1,10 @@
1
1
  import { NextResponse } from 'next/server.js';
2
2
  import { languageDetecotr } from '../server/functions/get_user_locale.js';
3
3
  import config from './intl_config.js';
4
- import { isBotCookieKey, localeCookieName } from './cookie_key.js';
4
+ import { countryCookieKey, isBotCookieKey, localeCookieName, timezoneCookieKey } from './cookie_key.js';
5
5
  import { cache } from 'react';
6
6
  import reportError from '../error_handling/report_error.js';
7
+ import { defaultCountryHeaderNames, defaultTimezoneHeaderNames } from '../server/functions/geo.js';
7
8
  const sameSite = false;
8
9
  const defaultCookieOption = {
9
10
  path: '/',
@@ -24,6 +25,14 @@ async function getIsBotValue(userAgent) {
24
25
  const getIsBotValueCache = cache(getIsBotValue);
25
26
  export const localesSet = new Set(config.locales);
26
27
  let updateSessionModule;
28
+ function getFirstHeaderValue(headers, names) {
29
+ for (const name of names) {
30
+ const value = headers.get(name);
31
+ if (value)
32
+ return value;
33
+ }
34
+ return undefined;
35
+ }
27
36
  export default async function intlMiddleware(request, options) {
28
37
  try {
29
38
  let initialChosenLocale;
@@ -59,14 +68,19 @@ export default async function intlMiddleware(request, options) {
59
68
  pathWithoutLocale = pathname;
60
69
  }
61
70
  const effectiveLocaleForRequest = urlLocale ?? initialChosenLocale;
62
- const country = request.cf?.country ?? request.headers.get('cf-ipcountry') ?? request.headers.get('x-cf-country');
63
- if (country) {
64
- request.headers.set('x-cf-country', country);
65
- }
66
- const timezone = request.cf?.timezone ?? request.headers.get('cf-timezone') ?? request.headers.get('x-cf-timezone');
67
- if (timezone) {
68
- request.headers.set('x-cf-timezone', timezone);
69
- }
71
+ const cf = request.cf;
72
+ const countryHeaderNames = config.cookieConsent?.countryHeaderNames
73
+ ?? config.generate?.countryHeaderNames
74
+ ?? defaultCountryHeaderNames;
75
+ const timezoneHeaderNames = config.generate?.timezoneHeaderNames
76
+ ?? defaultTimezoneHeaderNames;
77
+ const countryCookieName = config.cookieConsent?.countryCookieName
78
+ ?? config.generate?.countryCookieName
79
+ ?? countryCookieKey;
80
+ const timezoneCookieName = config.generate?.timezoneCookieName
81
+ ?? timezoneCookieKey;
82
+ const country = cf?.country || getFirstHeaderValue(request.headers, countryHeaderNames);
83
+ const timezone = cf?.timezone || getFirstHeaderValue(request.headers, timezoneHeaderNames);
70
84
  const requestHeaders = new Headers(request.headers);
71
85
  requestHeaders.set('x-pathname', pathWithoutLocale);
72
86
  requestHeaders.set('x-search', search);
@@ -123,9 +137,23 @@ export default async function intlMiddleware(request, options) {
123
137
  response.headers.set('x-search', search);
124
138
  if (country) {
125
139
  response.headers.set('x-cf-country', country);
140
+ response.cookies.set(countryCookieName, country, {
141
+ path: '/',
142
+ maxAge: 86400,
143
+ httpOnly: false,
144
+ secure: process.env.NODE_ENV === 'production',
145
+ sameSite: 'lax',
146
+ });
126
147
  }
127
148
  if (timezone) {
128
149
  response.headers.set('x-cf-timezone', timezone);
150
+ response.cookies.set(timezoneCookieName, timezone, {
151
+ path: '/',
152
+ maxAge: 86400,
153
+ httpOnly: false,
154
+ secure: process.env.NODE_ENV === 'production',
155
+ sameSite: 'lax',
156
+ });
129
157
  }
130
158
  if (!isRedirect && config.firebaseAuth && config.firebaseAuth.middlewareEnabled !== false) {
131
159
  if (!updateSessionModule) {
@@ -1,6 +1,6 @@
1
1
  import type { CookieConsentContextType } from '../types.js';
2
2
  export declare const CookieConsentContext: import("react").Context<CookieConsentContextType | null>;
3
- export default function CookieConsentProvider({ requiresConsent, children }: {
3
+ export default function CookieConsentProvider({ requiresConsent: requiresConsentProp, children }: {
4
4
  requiresConsent?: boolean;
5
5
  children: React.ReactNode;
6
6
  }): React.ReactElement;
@@ -6,7 +6,8 @@ import config from '../../config/intl_config.js';
6
6
  import requireCookieConsentConfig from '../require_config.js';
7
7
  import getCookie from '../../client/functions/get_cookie.js';
8
8
  import setCookie from '../../client/functions/set_cookie.js';
9
- import { cookieConsentCookieKey, privacyPolicyDateCookieKey } from '../../config/cookie_key.js';
9
+ import { cookieConsentCookieKey, countryCookieKey, privacyPolicyDateCookieKey } from '../../config/cookie_key.js';
10
+ import { defaultGdprCountries } from '../gdpr_countries.js';
10
11
  export const CookieConsentContext = createContext(null);
11
12
  function parseConsent(raw) {
12
13
  if (raw === 'true')
@@ -15,8 +16,9 @@ function parseConsent(raw) {
15
16
  return false;
16
17
  return null;
17
18
  }
18
- export default function CookieConsentProvider({ requiresConsent = true, children }) {
19
- const { consentCookieName, dateCookieName, maxAge, policyDate, privacyPolicyPath, showPrivacyPolicy } = useMemo(() => {
19
+ const defaultGdprCountriesSet = new Set(defaultGdprCountries);
20
+ export default function CookieConsentProvider({ requiresConsent: requiresConsentProp = true, children }) {
21
+ const { consentCookieName, dateCookieName, maxAge, policyDate, privacyPolicyPath, showPrivacyPolicy, gdprCountries } = useMemo(() => {
20
22
  const cc = requireCookieConsentConfig(config.cookieConsent);
21
23
  return {
22
24
  consentCookieName: cc.consentCookieName ?? cookieConsentCookieKey,
@@ -25,17 +27,28 @@ export default function CookieConsentProvider({ requiresConsent = true, children
25
27
  policyDate: cc.privacyPolicyDate ? new Date(cc.privacyPolicyDate) : null,
26
28
  privacyPolicyPath: cc.privacyPolicyPath ?? '/privacy-policy',
27
29
  showPrivacyPolicy: cc.showPrivacyPolicy ?? true,
30
+ gdprCountries: cc.gdprCountries,
28
31
  };
29
32
  }, []);
30
33
  const [consent, setConsentState] = useState(null);
31
34
  const [privacyPolicyUpdated, setPrivacyPolicyUpdated] = useState(false);
32
35
  const [isMounted, setIsMounted] = useState(false);
36
+ const [requiresConsent, setRequiresConsent] = useState(requiresConsentProp);
33
37
  const pathname = usePathname();
34
38
  useEffect(() => {
35
39
  const rawConsent = getCookie(consentCookieName);
36
40
  const storedConsent = parseConsent(rawConsent);
37
41
  const isFirstVisit = rawConsent === null;
38
42
  setConsentState(storedConsent);
43
+ if (requiresConsentProp) {
44
+ const countryCookie = getCookie(countryCookieKey);
45
+ if (countryCookie) {
46
+ const gdprSet = gdprCountries
47
+ ? new Set(gdprCountries)
48
+ : defaultGdprCountriesSet;
49
+ setRequiresConsent(gdprSet.has(countryCookie));
50
+ }
51
+ }
39
52
  setIsMounted(true);
40
53
  if (isFirstVisit || !policyDate)
41
54
  return;
@@ -46,7 +59,7 @@ export default function CookieConsentProvider({ requiresConsent = true, children
46
59
  }
47
60
  const storedDate = new Date(storedDateRaw);
48
61
  setPrivacyPolicyUpdated(!Number.isNaN(storedDate.getTime()) && storedDate < policyDate);
49
- }, [consentCookieName, dateCookieName, maxAge, policyDate]);
62
+ }, [consentCookieName, dateCookieName, maxAge, policyDate, requiresConsentProp, gdprCountries]);
50
63
  const setConsent = useCallback((value) => {
51
64
  if (value === null) {
52
65
  setCookie({ name: consentCookieName, value: 'null', maxAge });
@@ -108,6 +108,7 @@ export async function withPublicDb(fn, dbOverride) {
108
108
  return await withDbClient(config, async (client) => {
109
109
  const { drizzle } = await import('drizzle-orm/node-postgres');
110
110
  const drizzleHandle = drizzle(client);
111
+ await client.query(`set local role anon`);
111
112
  return await fn(await postgresDb(drizzleHandle, client));
112
113
  });
113
114
  }
@@ -4,7 +4,6 @@ import { getMessage } from "../functions/server.js";
4
4
  import dynamic from "next/dynamic.js";
5
5
  import { localesSet } from "../../config/middleware.js";
6
6
  import config from "../../config/intl_config.js";
7
- import resolveRequiresConsent from "../../cookie_consent/gdpr_countries.js";
8
7
  import installConsoleErrorOverride from "../../error_handling/install_console_error_override.js";
9
8
  import reportError from "../../error_handling/report_error.js";
10
9
  const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider.js"));
@@ -43,9 +42,7 @@ export default async function LocationzationProvider({ language, messages, stati
43
42
  let requiresConsent = true;
44
43
  if (config.cookieConsent) {
45
44
  const isDevEnvironment = process.env.NODE_ENV === 'development';
46
- requiresConsent = !isDevEnvironment
47
- ? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling, config.cookieConsent.countryHeaderNames, config.generate)
48
- : false;
45
+ requiresConsent = !isDevEnvironment;
49
46
  const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
50
47
  if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
51
48
  if (config.cookieConsent.getAnalytics) {
@@ -4,7 +4,6 @@ import { getMessage } from "../functions/server.js";
4
4
  import dynamic from "next/dynamic.js";
5
5
  import { localesSet } from "../../config/middleware.js";
6
6
  import config from "../../config/intl_config.js";
7
- import resolveRequiresConsent from "../../cookie_consent/gdpr_countries.js";
8
7
  import installConsoleErrorOverride from "../../error_handling/install_console_error_override.js";
9
8
  import reportError from "../../error_handling/report_error.js";
10
9
  const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider_static.js"));
@@ -32,9 +31,7 @@ export default async function LocationzationProvider({ language, messages, child
32
31
  let requiresConsent = true;
33
32
  if (config.cookieConsent) {
34
33
  const isDevEnvironment = process.env.NODE_ENV === 'development';
35
- requiresConsent = !isDevEnvironment
36
- ? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling, config.cookieConsent.countryHeaderNames, config.generate)
37
- : false;
34
+ requiresConsent = !isDevEnvironment;
38
35
  const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
39
36
  if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
40
37
  if (config.cookieConsent.getAnalytics) {
@@ -1,3 +1,4 @@
1
+ import { countryCookieKey, timezoneCookieKey } from '../../config/cookie_key.js';
1
2
  export const defaultCountryHeaderNames = ['x-cf-country', 'cf-ipcountry'];
2
3
  export const defaultTimezoneHeaderNames = ['x-cf-timezone', 'cf-timezone'];
3
4
  function extractHeader(h, name) {
@@ -26,6 +27,28 @@ function extractFromHeaderNames(h, headerNames) {
26
27
  }
27
28
  return undefined;
28
29
  }
30
+ function extractCookieHeader(h, name) {
31
+ const cookieHeader = extractHeader(h, 'cookie');
32
+ if (!cookieHeader)
33
+ return undefined;
34
+ const parts = cookieHeader.split(';');
35
+ for (const part of parts) {
36
+ const index = part.indexOf('=');
37
+ if (index === -1)
38
+ continue;
39
+ const key = part.slice(0, index).trim();
40
+ if (key !== name)
41
+ continue;
42
+ const value = part.slice(index + 1).trim();
43
+ return value ? decodeURIComponent(value) : undefined;
44
+ }
45
+ return undefined;
46
+ }
47
+ function extractCookie(cookieStore, name) {
48
+ const cookie = cookieStore?.get?.(name);
49
+ const value = typeof cookie === 'string' ? cookie : cookie?.value;
50
+ return value || undefined;
51
+ }
29
52
  export async function getCountry(input, generate, headerNames) {
30
53
  const gen = generate ?? await configuredGenerate();
31
54
  const names = headerNames
@@ -36,11 +59,21 @@ export async function getCountry(input, generate, headerNames) {
36
59
  const country = extractFromHeaderNames(input.headers, names);
37
60
  if (country)
38
61
  return country;
62
+ const cookieCountry = extractCookieHeader(input.headers, countryCookieKey);
63
+ if (cookieCountry)
64
+ return cookieCountry;
39
65
  }
40
66
  else if (typeof input.get === 'function') {
41
67
  const country = extractFromHeaderNames(input, names);
42
68
  if (country)
43
69
  return country;
70
+ const cookieCountry = extractCookieHeader(input, countryCookieKey);
71
+ if (cookieCountry)
72
+ return cookieCountry;
73
+ }
74
+ const cookieCountry = extractCookie(input.cookies, countryCookieKey);
75
+ if (cookieCountry) {
76
+ return cookieCountry;
44
77
  }
45
78
  const cf = input.cf;
46
79
  if (cf?.country && typeof cf.country === 'string' && cf.country.length > 0) {
@@ -53,6 +86,18 @@ export async function getCountry(input, generate, headerNames) {
53
86
  const country = extractFromHeaderNames(h, names);
54
87
  if (country)
55
88
  return country;
89
+ const cookieCountry = extractCookieHeader(h, countryCookieKey);
90
+ if (cookieCountry)
91
+ return cookieCountry;
92
+ }
93
+ catch {
94
+ }
95
+ try {
96
+ const { cookies } = await import('next/headers.js');
97
+ const c = await cookies();
98
+ const country = extractCookie(c, countryCookieKey);
99
+ if (country)
100
+ return country;
56
101
  }
57
102
  catch {
58
103
  }
@@ -89,11 +134,21 @@ export async function getTimezone(input, fallback, generate, headerNames) {
89
134
  const tz = extractFromHeaderNames(input.headers, names);
90
135
  if (tz)
91
136
  return tz;
137
+ const cookieTimezone = extractCookieHeader(input.headers, timezoneCookieKey);
138
+ if (cookieTimezone)
139
+ return cookieTimezone;
92
140
  }
93
141
  else if (typeof input.get === 'function') {
94
142
  const tz = extractFromHeaderNames(input, names);
95
143
  if (tz)
96
144
  return tz;
145
+ const cookieTimezone = extractCookieHeader(input, timezoneCookieKey);
146
+ if (cookieTimezone)
147
+ return cookieTimezone;
148
+ }
149
+ const cookieTimezone = extractCookie(input.cookies, timezoneCookieKey);
150
+ if (cookieTimezone) {
151
+ return cookieTimezone;
97
152
  }
98
153
  const cf = input.cf;
99
154
  if (cf?.timezone && typeof cf.timezone === 'string' && cf.timezone.length > 0) {
@@ -106,6 +161,18 @@ export async function getTimezone(input, fallback, generate, headerNames) {
106
161
  const tz = extractFromHeaderNames(h, names);
107
162
  if (tz)
108
163
  return tz;
164
+ const cookieTimezone = extractCookieHeader(h, timezoneCookieKey);
165
+ if (cookieTimezone)
166
+ return cookieTimezone;
167
+ }
168
+ catch {
169
+ }
170
+ try {
171
+ const { cookies } = await import('next/headers.js');
172
+ const c = await cookies();
173
+ const tz = extractCookie(c, timezoneCookieKey);
174
+ if (tz)
175
+ return tz;
109
176
  }
110
177
  catch {
111
178
  }
@@ -29,10 +29,17 @@ export interface GenerateRoutingConfig {
29
29
  } | undefined);
30
30
  getCloudflareContext?: CookieConsentGetCloudflareContext;
31
31
  countryHeaderNames?: readonly string[];
32
+ countryCookieName?: string;
32
33
  timezoneHeaderNames?: readonly string[];
34
+ timezoneCookieName?: string;
33
35
  }
34
36
  export type RequestOrHeaders = Request | Headers | {
35
37
  headers?: Headers | Record<string, string | null | undefined>;
38
+ cookies?: {
39
+ get?: (name: string) => {
40
+ value?: string;
41
+ } | string | undefined;
42
+ };
36
43
  cf?: {
37
44
  country?: string;
38
45
  timezone?: string;
@@ -75,6 +82,7 @@ export interface CookieConsentRoutingConfig {
75
82
  autoAnalyticsEvents?: AutoAnalyticsEventsConfig;
76
83
  getCountryCode?: () => string | undefined | Promise<string | undefined>;
77
84
  countryHeaderNames?: readonly string[];
85
+ countryCookieName?: string;
78
86
  gdprCountries?: readonly string[];
79
87
  enableAnalyticsInDevMode?: boolean;
80
88
  autoWireDialogs?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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",